From ecd60853c60a55332356e6cb842dbfc65e9c1ef5 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 10:16:01 +0200 Subject: [PATCH 01/85] Readd "crud for later" This reverts commit f5b7bab5b4156c4dad92fad47ea5b7f32b7df4a7. --- .../exporters/schematic/kicad/__init__.py | 0 .../exporters/schematic/kicad/bboxes.py | 326 ++ .../exporters/schematic/kicad/constants.py | 16 + .../exporters/schematic/kicad/debug_draw.py | 338 ++ .../exporters/schematic/kicad/geometry.py | 485 +++ .../exporters/schematic/kicad/net_terminal.py | 65 + src/faebryk/exporters/schematic/kicad/node.py | 354 ++ .../exporters/schematic/kicad/place.py | 1554 ++++++++ .../exporters/schematic/kicad/route.py | 3413 +++++++++++++++++ .../exporters/schematic/kicad/schematic.py | 797 ++++ 10 files changed, 7348 insertions(+) create mode 100644 src/faebryk/exporters/schematic/kicad/__init__.py create mode 100644 src/faebryk/exporters/schematic/kicad/bboxes.py create mode 100644 src/faebryk/exporters/schematic/kicad/constants.py create mode 100644 src/faebryk/exporters/schematic/kicad/debug_draw.py create mode 100644 src/faebryk/exporters/schematic/kicad/geometry.py create mode 100644 src/faebryk/exporters/schematic/kicad/net_terminal.py create mode 100644 src/faebryk/exporters/schematic/kicad/node.py create mode 100644 src/faebryk/exporters/schematic/kicad/place.py create mode 100644 src/faebryk/exporters/schematic/kicad/route.py create mode 100644 src/faebryk/exporters/schematic/kicad/schematic.py diff --git a/src/faebryk/exporters/schematic/kicad/__init__.py b/src/faebryk/exporters/schematic/kicad/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/faebryk/exporters/schematic/kicad/bboxes.py b/src/faebryk/exporters/schematic/kicad/bboxes.py new file mode 100644 index 00000000..88368986 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/bboxes.py @@ -0,0 +1,326 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Calculate bounding boxes for part symbols and hierarchical sheets. +""" + +from collections import namedtuple + +from skidl.logger import active_logger +from skidl.schematics.geometry import ( + Tx, + BBox, + Point, + Vector, + tx_rot_0, + tx_rot_90, + tx_rot_180, + tx_rot_270, +) +from skidl.utilities import export_to_all +from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE +from skidl.schematics.geometry import BBox, Point, Tx, Vector +from .draw_objs import * + + +@export_to_all +def calc_symbol_bbox(part, **options): + """ + Return the bounding box of the part symbol. + + Args: + part: Part object for which an SVG symbol will be created. + options (dict): Various options to control bounding box calculation: + graphics_only (boolean): If true, compute bbox of graphics (no text). + + Returns: List of BBoxes for all units in the part symbol. + + Note: V5 library format: https://www.compuphase.com/electronics/LibraryFileFormats.pdf + """ + + # Named tuples for part KiCad V5 DRAW primitives. + + def make_pin_dir_tbl(abs_xoff=20): + # abs_xoff is the absolute distance of name/num from the end of the pin. + rel_yoff_num = -0.15 # Relative distance of number above pin line. + rel_yoff_name = ( + 0.2 # Relative distance that places name midline even with pin line. + ) + + # Tuple for storing information about pins in each of four directions: + # direction: The direction the pin line is drawn from start to end. + # side: The side of the symbol the pin is on. (Opposite of the direction.) + # angle: The angle of the name/number text for the pin (usually 0, -90.). + # num_justify: Text justification of the pin number. + # name_justify: Text justification of the pin name. + # num_offset: (x,y) offset of the pin number w.r.t. the end of the pin. + # name_offset: (x,y) offset of the pin name w.r.t. the end of the pin. + PinDir = namedtuple( + "PinDir", + "direction side angle num_justify name_justify num_offset name_offset net_offset", + ) + + return { + "U": PinDir( + Point(0, 1), + "bottom", + -90, + "end", + "start", + Point(-abs_xoff, rel_yoff_num), + Point(abs_xoff, rel_yoff_name), + Point(abs_xoff, rel_yoff_num), + ), + "D": PinDir( + Point(0, -1), + "top", + -90, + "start", + "end", + Point(abs_xoff, rel_yoff_num), + Point(-abs_xoff, rel_yoff_name), + Point(-abs_xoff, rel_yoff_num), + ), + "L": PinDir( + Point(-1, 0), + "right", + 0, + "start", + "end", + Point(abs_xoff, rel_yoff_num), + Point(-abs_xoff, rel_yoff_name), + Point(-abs_xoff, rel_yoff_num), + ), + "R": PinDir( + Point(1, 0), + "left", + 0, + "end", + "start", + Point(-abs_xoff, rel_yoff_num), + Point(abs_xoff, rel_yoff_name), + Point(abs_xoff, rel_yoff_num), + ), + } + + default_pin_name_offset = 20 + + # Go through each graphic object that makes up the component symbol. + for obj in part.draw: + obj_bbox = BBox() # Bounding box of all the component objects. + thickness = 0 + + if isinstance(obj, DrawDef): + def_ = obj + # Make pin direction table with symbol-specific name offset. + pin_dir_tbl = make_pin_dir_tbl(def_.name_offset or default_pin_name_offset) + # Make structures for holding info on each part unit. + num_units = def_.num_units + unit_bboxes = [BBox() for _ in range(num_units + 1)] + + elif isinstance(obj, DrawF0) and not options.get("graphics_only", False): + # obj attributes: x y size orientation visibility halign valign + # Skip if the object is invisible. + if obj.visibility.upper() == "I": + continue + + # Calculate length and height of part reference. + # Use ref from the SKiDL part since the ref in the KiCAD part + # hasn't been updated from its generic value. + length = len(part.ref) * obj.size + height = obj.size + + # Create bbox with lower-left point at (0, 0). + bbox = BBox(Point(0, 0), Point(length, height)) + + # Rotate bbox around origin. + rot_tx = {"H": Tx(), "V": tx_rot_90}[obj.orientation.upper()] + bbox *= rot_tx + + # Horizontally align bbox. + halign = obj.halign.upper() + if halign == "L": + pass + elif halign == "R": + bbox *= Tx().move(Point(-bbox.w, 0)) + elif halign == "C": + bbox *= Tx().move(Point(-bbox.w / 2, 0)) + else: + raise Exception("Inconsistent horizontal alignment: {}".format(halign)) + + # Vertically align bbox. + valign = obj.valign[:1].upper() # valign is first letter. + if valign == "B": + pass + elif valign == "T": + bbox *= Tx().move(Point(0, -bbox.h)) + elif valign == "C": + bbox *= Tx().move(Point(0, -bbox.h / 2)) + else: + raise Exception("Inconsistent vertical alignment: {}".format(valign)) + + bbox *= Tx().move(Point(obj.x, obj.y)) + obj_bbox.add(bbox) + + elif isinstance(obj, DrawF1) and not options.get("graphics_only", False): + # Skip if the object is invisible. + if obj.visibility.upper() == "I": + continue + + # Calculate length and height of part value. + # Use value from the SKiDL part since the value in the KiCAD part + # hasn't been updated from its generic value. + length = len(str(part.value)) * obj.size + height = obj.size + + # Create bbox with lower-left point at (0, 0). + bbox = BBox(Point(0, 0), Point(length, height)) + + # Rotate bbox around origin. + rot_tx = {"H": Tx(), "V": tx_rot_90}[obj.orientation.upper()] + bbox *= rot_tx + + # Horizontally align bbox. + halign = obj.halign.upper() + if halign == "L": + pass + elif halign == "R": + bbox *= Tx().move(Point(-bbox.w, 0)) + elif halign == "C": + bbox *= Tx().move(Point(-bbox.w / 2, 0)) + else: + raise Exception("Inconsistent horizontal alignment: {}".format(halign)) + + # Vertically align bbox. + valign = obj.valign[:1].upper() # valign is first letter. + if valign == "B": + pass + elif valign == "T": + bbox *= Tx().move(Point(0, -bbox.h)) + elif valign == "C": + bbox *= Tx().move(Point(0, -bbox.h / 2)) + else: + raise Exception("Inconsistent vertical alignment: {}".format(valign)) + + bbox *= Tx().move(Point(obj.x, obj.y)) + obj_bbox.add(bbox) + + elif isinstance(obj, DrawArc): + arc = obj + center = Point(arc.cx, arc.cy) + thickness = arc.thickness + radius = arc.radius + start = Point(arc.startx, arc.starty) + end = Point(arc.endx, arc.endy) + start_angle = arc.start_angle / 10 + end_angle = arc.end_angle / 10 + clock_wise = int(end_angle < start_angle) + large_arc = int(abs(end_angle - start_angle) > 180) + radius_pt = Point(radius, radius) + obj_bbox.add(center - radius_pt) + obj_bbox.add(center + radius_pt) + + elif isinstance(obj, DrawCircle): + circle = obj + center = Point(circle.cx, circle.cy) + thickness = circle.thickness + radius = circle.radius + radius_pt = Point(radius, radius) + obj_bbox.add(center - radius_pt) + obj_bbox.add(center + radius_pt) + + elif isinstance(obj, DrawPoly): + poly = obj + thickness = obj.thickness + pts = [Point(x, y) for x, y in zip(poly.points[0::2], poly.points[1::2])] + path = [] + for pt in pts: + obj_bbox.add(pt) + + elif isinstance(obj, DrawRect): + rect = obj + thickness = obj.thickness + start = Point(rect.x1, rect.y1) + end = Point(rect.x2, rect.y2) + obj_bbox.add(start) + obj_bbox.add(end) + + elif isinstance(obj, DrawText) and not options.get("graphics_only", False): + pass + + elif isinstance(obj, DrawPin): + pin = obj + + try: + visible = pin.shape[0] != "N" + except IndexError: + visible = True # No pin shape given, so it is visible by default. + + if visible: + # Draw pin if it's not invisible. + + # Create line for pin lead. + dir = pin_dir_tbl[pin.orientation].direction + start = Point(pin.x, pin.y) + l = dir * pin.length + end = start + l + obj_bbox.add(start) + obj_bbox.add(end) + + else: + active_logger.error( + "Unknown graphical object {} in part symbol {}.".format( + type(obj), part.name + ) + ) + + # REMOVE: Maybe we shouldn't do this? + # Expand bounding box to account for object line thickness. + # obj_bbox.resize(Vector(round(thickness / 2), round(thickness / 2))) + + # Enter the current object into the SVG for this part. + unit = getattr(obj, "unit", 0) + if unit == 0: + for bbox in unit_bboxes: + bbox.add(obj_bbox) + else: + unit_bboxes[unit].add(obj_bbox) + + # End of loop through all the component objects. + + return unit_bboxes + + +@export_to_all +def calc_hier_label_bbox(label, dir): + """Calculate the bounding box for a hierarchical label. + + Args: + label (str): String for the label. + dir (str): Orientation ("U", "D", "L", "R"). + + Returns: + BBox: Bounding box for the label and hierarchical terminal. + """ + + # Rotation matrices for each direction. + lbl_tx = { + "U": tx_rot_90, # Pin on bottom pointing upwards. + "D": tx_rot_270, # Pin on top pointing down. + "L": tx_rot_180, # Pin on right pointing left. + "R": tx_rot_0, # Pin on left pointing right. + } + + # Calculate length and height of label + hierarchical marker. + lbl_len = len(label) * PIN_LABEL_FONT_SIZE + HIER_TERM_SIZE + lbl_hgt = max(PIN_LABEL_FONT_SIZE, HIER_TERM_SIZE) + + # Create bbox for label on left followed by marker on right. + bbox = BBox(Point(0, lbl_hgt / 2), Point(-lbl_len, -lbl_hgt / 2)) + + # Rotate the bbox in the given direction. + bbox *= lbl_tx[dir] + + return bbox diff --git a/src/faebryk/exporters/schematic/kicad/constants.py b/src/faebryk/exporters/schematic/kicad/constants.py new file mode 100644 index 00000000..a7de8044 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/constants.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Constants used when generating schematics. +""" + +# Constants for KiCad. +GRID = 50 +PIN_LABEL_FONT_SIZE = 50 +BOX_LABEL_FONT_SIZE = 50 +BLK_INT_PAD = 2 * GRID +BLK_EXT_PAD = 2 * GRID +DRAWING_BOX_RESIZE = 100 +HIER_TERM_SIZE = 50 diff --git a/src/faebryk/exporters/schematic/kicad/debug_draw.py b/src/faebryk/exporters/schematic/kicad/debug_draw.py new file mode 100644 index 00000000..f9b0eaec --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/debug_draw.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Drawing routines used for debugging place & route. +""" + +from collections import defaultdict +from random import randint + +from .geometry import BBox, Point, Segment, Tx, Vector + +# Dictionary for storing colors to visually distinguish routed nets. +net_colors = defaultdict(lambda: (randint(0, 200), randint(0, 200), randint(0, 200))) + + +def draw_box(bbox, scr, tx, color=(192, 255, 192), thickness=0): + """Draw a box in the drawing area. + + Args: + bbox (BBox): Bounding box for the box. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + color (tuple, optional): Box color. Defaults to (192, 255, 192). + + Returns: + None. + """ + import pygame + + bbox = bbox * tx + corners = ( + (bbox.min.x, bbox.min.y), + (bbox.min.x, bbox.max.y), + (bbox.max.x, bbox.max.y), + (bbox.max.x, bbox.min.y), + ) + pygame.draw.polygon(scr, color, corners, thickness) + + +def draw_endpoint(pt, scr, tx, color=(100, 100, 100), dot_radius=10): + """Draw a line segment endpoint in the drawing area. + + Args: + pt (Point): A point with (x,y) coords. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + color (tuple, optional): Segment color. Defaults to (192, 255, 192). + dot_Radius (int, optional): Endpoint dot radius. Defaults to 3. + """ + import pygame + + pt = pt * tx # Convert to drawing coords. + + # Draw diamond for terminal. + sz = dot_radius / 2 * tx.a # Scale for drawing coords. + corners = ( + (pt.x, pt.y + sz), + (pt.x + sz, pt.y), + (pt.x, pt.y - sz), + (pt.x - sz, pt.y), + ) + pygame.draw.polygon(scr, color, corners, 0) + + # Draw dot for terminal. + radius = dot_radius * tx.a + pygame.draw.circle(scr, color, (pt.x, pt.y), radius) + + +def draw_seg(seg, scr, tx, color=(100, 100, 100), thickness=5, dot_radius=10): + """Draw a line segment in the drawing area. + + Args: + seg (Segment, Interval, NetInterval): An object with two endpoints. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + color (tuple, optional): Segment color. Defaults to (192, 255, 192). + seg_thickness (int, optional): Segment line thickness. Defaults to 5. + dot_Radius (int, optional): Endpoint dot radius. Defaults to 3. + """ + import pygame + + # Use net color if object has a net. Otherwise use input color. + try: + color = net_colors[seg.net] + except AttributeError: + pass + + # draw endpoints. + draw_endpoint(seg.p1, scr, tx, color=color, dot_radius=dot_radius) + draw_endpoint(seg.p2, scr, tx, color=color, dot_radius=dot_radius) + + # Transform segment coords to screen coords. + seg = seg * tx + + # Draw segment. + pygame.draw.line( + scr, color, (seg.p1.x, seg.p1.y), (seg.p2.x, seg.p2.y), width=thickness + ) + + +def draw_text(txt, pt, scr, tx, font, color=(100, 100, 100), real=True): + """Render text in drawing area. + + Args: + txt (str): Text string to be rendered. + pt (Point): Real or screen coord for start of rendered text. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (tuple, optional): Segment color. Defaults to (100,100,100). + real (Boolean): If true, transform real pt to screen coords. Otherwise, pt is screen coords. + """ + + # Transform real text starting point to screen coords. + if real: + pt = pt * tx + + # Render text. + font.render_to(scr, (pt.x, pt.y), txt, color) + + +def draw_part(part, scr, tx, font): + """Draw part bounding box. + + Args: + part (Part): Part to draw. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + """ + tx_bbox = ( + getattr(part, "lbl_bbox", getattr(part, "place_bbox", Vector(0, 0))) * part.tx + ) + draw_box(tx_bbox, scr, tx, color=(180, 255, 180), thickness=0) + draw_box(tx_bbox, scr, tx, color=(90, 128, 90), thickness=5) + draw_text(part.ref, tx_bbox.ctr, scr, tx, font) + try: + for pin in part: + if hasattr(pin, "place_pt"): + pt = pin.place_pt * part.tx + draw_endpoint(pt, scr, tx, color=(200, 0, 200), dot_radius=10) + except TypeError: + # Probably trying to draw a block of parts which has no pins and can't iterate thru them. + pass + + +def draw_net(net, parts, scr, tx, font, color=(0, 0, 0), thickness=2, dot_radius=5): + """Draw net and connected terminals. + + Args: + net (Net): Net of conmnected terminals. + parts (list): List of parts to which net will be drawn. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (tuple, optional): Segment color. Defaults to (0,0,0). + thickness (int, optional): Thickness of net line. Defaults to 2. + dot_radius (int, optional): Radius of terminals on net. Defaults to 5. + """ + pts = [] + for pin in net.pins: + part = pin.part + if part in parts: + pt = pin.route_pt * part.tx + pts.append(pt) + for pt1, pt2 in zip(pts[:-1], pts[1:]): + draw_seg( + Segment(pt1, pt2), + scr, + tx, + color=color, + thickness=thickness, + dot_radius=dot_radius, + ) + + +def draw_force(part, force, scr, tx, font, color=(128, 0, 0)): + """Draw force vector affecting a part. + + Args: + part (Part): The part affected by the force. + force (Vector): The force vector. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (tuple, optional): Segment color. Defaults to (0,0,0). + """ + force *= 1 + anchor = part.place_bbox.ctr * part.tx + draw_seg( + Segment(anchor, anchor + force), scr, tx, color=color, thickness=5, dot_radius=5 + ) + + +def draw_placement(parts, nets, scr, tx, font): + """Draw placement of parts and interconnecting nets. + + Args: + parts (list): List of Part objects. + nets (list): List of Net objects. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + """ + draw_clear(scr) + for part in parts: + draw_part(part, scr, tx, font) + draw_force(part, getattr(part, "force", Vector(0, 0)), scr, tx, font) + for net in nets: + draw_net(net, parts, scr, tx, font) + draw_redraw() + + +def draw_routing(node, bbox, parts, *other_stuff, **options): + """Draw routing for debugging purposes. + + Args: + node (Node): Hierarchical node. + bbox (BBox): Bounding box of drawing area. + node (Node): The Node being routed. + parts (list): List of Parts. + other_stuff (list): Other stuff with a draw() method. + options (dict, optional): Dictionary of options and values. Defaults to {}. + """ + + # Initialize drawing area. + draw_scr, draw_tx, draw_font = draw_start(bbox) + + # Draw parts. + for part in parts: + draw_part(part, draw_scr, draw_tx, draw_font) + + # Draw wiring. + for wires in node.wires.values(): + for wire in wires: + draw_seg(wire, draw_scr, draw_tx, (255, 0, 255), 3, dot_radius=10) + + # Draw other stuff (global routes, switchbox routes, etc.) that has a draw() method. + for stuff in other_stuff: + for obj in stuff: + obj.draw(draw_scr, draw_tx, draw_font, **options) + + draw_end() + + +def draw_clear(scr, color=(255, 255, 255)): + """Clear drawing area. + + Args: + scr (PyGame screen): Screen object to be cleared. + color (tuple, optional): Background color. Defaults to (255, 255, 255). + """ + scr.fill(color) + + +def draw_start(bbox): + """ + Initialize PyGame drawing area. + + Args: + bbox: Bounding box of object to be drawn. + + Returns: + scr: PyGame screen that is drawn upon. + tx: Matrix to transform from real coords to screen coords. + font: PyGame font for rendering text. + """ + + # Only import pygame if drawing is being done to avoid the startup message. + import pygame + import pygame.freetype + + # Screen drawing area. + scr_bbox = BBox(Point(0, 0), Point(2000, 1500)) + + # Place a blank region around the object by expanding it's bounding box. + border = max(bbox.w, bbox.h) / 20 + bbox = bbox.resize(Vector(border, border)) + bbox = bbox.round() + + # Compute the scaling from real to screen coords. + scale = min(scr_bbox.w / bbox.w, scr_bbox.h / bbox.h) + scale_tx = Tx(a=scale, d=scale) + + # Flip the Y coord. + flip_tx = Tx(d=-1) + + # Compute the translation of the object center to the drawing area center + new_bbox = bbox * scale_tx * flip_tx # Object bbox transformed to screen coords. + move = scr_bbox.ctr - new_bbox.ctr # Vector to move object ctr to drawing ctr. + move_tx = Tx(dx=move.x, dy=move.y) + + # The final transformation matrix will scale the object's real coords, + # flip the Y coord, and then move the object to the center of the drawing area. + tx = scale_tx * flip_tx * move_tx + + # Initialize drawing area. + pygame.init() + scr = pygame.display.set_mode((scr_bbox.w, scr_bbox.h)) + + # Set font for text rendering. + font = pygame.freetype.SysFont("consolas", 24) + + # Clear drawing area. + draw_clear(scr) + + # Return drawing screen, transformation matrix, and font. + return scr, tx, font + + +def draw_redraw(): + """Redraw the PyGame display.""" + import pygame + pygame.display.flip() + + +def draw_pause(): + """Pause drawing and then resume after button press.""" + import pygame + + # Display drawing. + draw_redraw() + + # Wait for user to press a key or close PyGame window. + running = True + while running: + for event in pygame.event.get(): + if event.type in (pygame.KEYDOWN, pygame.QUIT): + running = False + + +def draw_end(): + """Display drawing and wait for user to close PyGame window.""" + import pygame + draw_pause() + pygame.quit() diff --git a/src/faebryk/exporters/schematic/kicad/geometry.py b/src/faebryk/exporters/schematic/kicad/geometry.py new file mode 100644 index 00000000..3006c069 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/geometry.py @@ -0,0 +1,485 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +from math import sqrt, sin, cos, radians +from copy import copy + +from ..utilities import export_to_all + +__all__ = [ + "mms_per_mil", + "mils_per_mm", + "Vector", + "tx_rot_0", + "tx_rot_90", + "tx_rot_180", + "tx_rot_270", + "tx_flip_x", + "tx_flip_y", +] + + +""" +Stuff for handling geometry: + transformation matrices, + points, + bounding boxes, + line segments. +""" + +# Millimeters/thousandths-of-inch conversion factor. +mils_per_mm = 39.37008 +mms_per_mil = 0.0254 + + +@export_to_all +def to_mils(mm): + """Convert millimeters to thousandths-of-inch and return.""" + return mm * mils_per_mm + + +@export_to_all +def to_mms(mils): + """Convert thousandths-of-inch to millimeters and return.""" + return mils * mms_per_mil + + +@export_to_all +class Tx: + def __init__(self, a=1, b=0, c=0, d=1, dx=0, dy=0): + """Create a transformation matrix. + tx = [ + a b 0 + c d 0 + dx dy 1 + ] + x' = a*x + c*y + dx + y' = b*x + d*y + dy + """ + self.a = a + self.b = b + self.c = c + self.d = d + self.dx = dx + self.dy = dy + + @classmethod + def from_symtx(cls, symtx: str): + """Return a Tx() object that implements the "HVLR" geometric operation sequence. + + Args: + symtx (str): A string of H, V, L, R operations that are applied in sequence left-to-right. + + Returns: + Tx: A transformation matrix that implements the sequence of geometric operations. + """ + op_dict = { + "H": Tx(a=-1, c=0, b=0, d=1), # Horizontal flip. + "V": Tx(a=1, c=0, b=0, d=-1), # Vertical flip. + "L": Tx(a=0, c=-1, b=1, d=0), # Rotate 90 degrees left (counter-clockwise). + "R": Tx(a=0, c=1, b=-1, d=0), # Rotate 90 degrees right (clockwise). + } + + tx = Tx() + for op in symtx.upper(): + tx *= op_dict[op] + return tx + + def __repr__(self): + return "{self.__class__}({self.a}, {self.b}, {self.c}, {self.d}, {self.dx}, {self.dy})".format( + self=self + ) + + def __str__(self): + return "[{self.a}, {self.b}, {self.c}, {self.d}, {self.dx}, {self.dy}]".format( + self=self + ) + + def __mul__(self, m): + """Return the product of two transformation matrices.""" + if isinstance(m, Tx): + tx = m + else: + # Assume m is a scalar, so convert it to a scaling Tx matrix. + tx = Tx(a=m, d=m) + return Tx( + a=self.a * tx.a + self.b * tx.c, + b=self.a * tx.b + self.b * tx.d, + c=self.c * tx.a + self.d * tx.c, + d=self.c * tx.b + self.d * tx.d, + dx=self.dx * tx.a + self.dy * tx.c + tx.dx, + dy=self.dx * tx.b + self.dy * tx.d + tx.dy, + ) + + @property + def origin(self): + """Return the (dx, dy) translation as a Point.""" + return Point(self.dx, self.dy) + + # This setter doesn't work in Python 2.7.18. + # @origin.setter + # def origin(self, pt): + # """Set the (dx, dy) translation from an (x,y) Point.""" + # self.dx, self.dy = pt.x, pt.y + + @property + def scale(self): + """Return the scaling factor.""" + return (Point(1, 0) * self - Point(0, 0) * self).magnitude + + def move(self, vec): + """Return Tx with movement vector applied.""" + return self * Tx(dx=vec.x, dy=vec.y) + + def rot_90cw(self): + """Return Tx with 90-deg clock-wise rotation around (0, 0).""" + return self * Tx(a=0, b=1, c=-1, d=0) + + def rot(self, degs): + """Return Tx rotated by the given angle (in degrees).""" + rads = radians(degs) + return self * Tx(a=cos(rads), b=sin(rads), c=-sin(rads), d=cos(rads)) + + def flip_x(self): + """Return Tx with X coords flipped around (0, 0).""" + return self * Tx(a=-1) + + def flip_y(self): + """Return Tx with Y coords flipped around (0, 0).""" + return self * Tx(d=-1) + + def no_translate(self): + """Return Tx with translation set to (0,0).""" + return Tx(a=self.a, b=self.b, c=self.c, d=self.d) + + +# Some common rotations. +tx_rot_0 = Tx(a=1, b=0, c=0, d=1) +tx_rot_90 = Tx(a=0, b=1, c=-1, d=0) +tx_rot_180 = Tx(a=-1, b=0, c=0, d=-1) +tx_rot_270 = Tx(a=0, b=-1, c=1, d=0) + +# Some common flips. +tx_flip_x = Tx(a=-1, b=0, c=0, d=1) +tx_flip_y = Tx(a=1, b=0, c=0, d=-1) + + +@export_to_all +class Point: + def __init__(self, x, y): + """Create a Point with coords x,y.""" + self.x = x + self.y = y + + def __hash__(self): + """Return hash of X,Y tuple.""" + return hash((self.x, self.y)) + + def __eq__(self, other): + """Return true if (x,y) tuples of self and other are the same.""" + return (self.x, self.y) == (other.x, other.y) + + def __lt__(self, other): + """Return true if (x,y) tuple of self compares as less than (x,y) tuple of other.""" + return (self.x, self.y) < (other.x, other.y) + + def __ne__(self, other): + """Return true if (x,y) tuples of self and other differ.""" + return not (self == other) + + def __add__(self, pt): + """Add the x,y coords of pt to self and return the resulting Point.""" + if not isinstance(pt, Point): + pt = Point(pt, pt) + return Point(self.x + pt.x, self.y + pt.y) + + def __sub__(self, pt): + """Subtract the x,y coords of pt from self and return the resulting Point.""" + if not isinstance(pt, Point): + pt = Point(pt, pt) + return Point(self.x - pt.x, self.y - pt.y) + + def __mul__(self, m): + """Apply transformation matrix or scale factor to a point and return a point.""" + if isinstance(m, Tx): + return Point( + self.x * m.a + self.y * m.c + m.dx, self.x * m.b + self.y * m.d + m.dy + ) + elif isinstance(m, Point): + return Point(self.x * m.x, self.y * m.y) + else: + return Point(m * self.x, m * self.y) + + def __rmul__(self, m): + if isinstance(m, Tx): + raise ValueError + else: + return self * m + + def xprod(self, pt): + """Cross-product of two 2D vectors returns scalar in Z coord.""" + return self.x * pt.y - self.y * pt.x + + def mask(self, msk): + """Multiply the X & Y coords by the elements of msk.""" + return Point(self.x * msk[0], self.y * msk[1]) + + def __neg__(self): + """Negate both coords.""" + return Point(-self.x, -self.y) + + def __truediv__(self, d): + """Divide the x,y coords by d.""" + return Point(self.x / d, self.y / d) + + def __div__(self, d): + """Divide the x,y coords by d.""" + return Point(self.x / d, self.y / d) + + def round(self): + return Point(int(round(self.x)), int(round(self.y))) + + def __str__(self): + return "{} {}".format(self.x, self.y) + + def snap(self, grid_spacing): + """Snap point x,y coords to the given grid spacing.""" + snap_func = lambda x: int(grid_spacing * round(x / grid_spacing)) + return Point(snap_func(self.x), snap_func(self.y)) + + def min(self, pt): + """Return a Point with coords that are the min x,y of both points.""" + return Point(min(self.x, pt.x), min(self.y, pt.y)) + + def max(self, pt): + """Return a Point with coords that are the max x,y of both points.""" + return Point(max(self.x, pt.x), max(self.y, pt.y)) + + @property + def magnitude(self): + """Get the distance of the point from the origin.""" + return sqrt(self.x**2 + self.y**2) + + @property + def norm(self): + """Return a unit vector pointing from the origin to the point.""" + try: + return self / self.magnitude + except ZeroDivisionError: + return Point(0, 0) + + def flip_xy(self): + """Flip X-Y coordinates of point.""" + self.x, self.y = self.y, self.x + + def __repr__(self): + return "{self.__class__}({self.x}, {self.y})".format(self=self) + + def __str__(self): + return "({}, {})".format(self.x, self.y) + + +Vector = Point + + +@export_to_all +class BBox: + def __init__(self, *pts): + """Create a bounding box surrounding the given points.""" + inf = float("inf") + self.min = Point(inf, inf) + self.max = Point(-inf, -inf) + self.add(*pts) + + def __add__(self, obj): + """Return the merged BBox of two BBoxes or a BBox and a Point.""" + sum_ = BBox() + if isinstance(obj, Point): + sum_.min = self.min.min(obj) + sum_.max = self.max.max(obj) + elif isinstance(obj, BBox): + sum_.min = self.min.min(obj.min) + sum_.max = self.max.max(obj.max) + else: + raise NotImplementedError + return sum_ + + def __iadd__(self, obj): + """Update BBox bt adding another Point or BBox""" + sum_ = self + obj + self.min = sum_.min + self.max = sum_.max + return self + + def add(self, *objs): + """Update the bounding box size by adding Point/BBox objects.""" + for obj in objs: + self += obj + return self + + def __mul__(self, m): + return BBox(self.min * m, self.max * m) + + def round(self): + return BBox(self.min.round(), self.max.round()) + + def is_inside(self, pt): + """Return True if point is inside bounding box.""" + return (self.min.x <= pt.x <= self.max.x) and (self.min.y <= pt.y <= self.max.y) + + def intersects(self, bbox): + """Return True if the two bounding boxes intersect.""" + return ( + (self.min.x < bbox.max.x) + and (self.max.x > bbox.min.x) + and (self.min.y < bbox.max.y) + and (self.max.y > bbox.min.y) + ) + + def intersection(self, bbox): + """Return the bounding box of the intersection between the two bounding boxes.""" + if not self.intersects(bbox): + return None + corner1 = self.min.max(bbox.min) + corner2 = self.max.min(bbox.max) + return BBox(corner1, corner2) + + def resize(self, vector): + """Expand/contract the bounding box by applying vector to its corner points.""" + return BBox(self.min - vector, self.max + vector) + + def snap_resize(self, grid_spacing): + """Resize bbox so max and min points are on grid. + + Args: + grid_spacing (float): Grid spacing. + """ + bbox = self.resize(Point(grid_spacing - 1, grid_spacing - 1)) + bbox.min = bbox.min.snap(grid_spacing) + bbox.max = bbox.max.snap(grid_spacing) + return bbox + + @property + def area(self): + """Return area of bounding box.""" + return self.w * self.h + + @property + def w(self): + """Return the bounding box width.""" + return abs(self.max.x - self.min.x) + + @property + def h(self): + """Return the bounding box height.""" + return abs(self.max.y - self.min.y) + + @property + def ctr(self): + """Return center point of bounding box.""" + return (self.max + self.min) / 2 + + @property + def ll(self): + """Return lower-left point of bounding box.""" + return Point(self.min.x, self.min.y) + + @property + def lr(self): + """Return lower-right point of bounding box.""" + return Point(self.max.x, self.min.y) + + @property + def ul(self): + """Return upper-left point of bounding box.""" + return Point(self.min.x, self.max.y) + + @property + def ur(self): + """Return upper-right point of bounding box.""" + return Point(self.max.x, self.max.y) + + def __repr__(self): + return "{self.__class__}(Point({self.min}), Point({self.max}))".format( + self=self + ) + + def __str__(self): + return "[{}, {}]".format(self.min, self.max) + + +@export_to_all +class Segment: + def __init__(self, p1, p2): + "Create a line segment between two points." + self.p1 = copy(p1) + self.p2 = copy(p2) + + def __mul__(self, m): + """Apply transformation matrix to a segment and return a segment.""" + return Segment(self.p1 * m, self.p2 * m) + + def round(self): + return Segment(self.p1.round(), self.p2.round()) + + def __str__(self): + return "{} {}".format(str(self.p1), str(self.p2)) + + def flip_xy(self): + """Flip the X-Y coordinates of the segment.""" + self.p1.flip_xy() + self.p2.flip_xy() + + def intersects(self, other): + """Return true if the segments intersect.""" + + # FIXME: This fails if the segments are parallel! + raise NotImplementedError + + # Given two segments: + # self: p1 + (p2-p1) * t1 + # other: p3 + (p4-p3) * t2 + # Look for a solution t1, t2 that solves: + # p1x + (p2x-p1x)*t1 = p3x + (p4x-p3x)*t2 + # p1y + (p2y-p1y)*t1 = p3y + (p4y-p3y)*t2 + # If t1 and t2 are both in range [0,1], then the two segments intersect. + + p1x, p1y, p2x, p2y = self.p1.x, self.p1.y, self.p2.x, self.p2.y + p3x, p3y, p4x, p4y = other.p1.x, other.p1.y, other.p2.x, other.p2.y + + # denom = p1x*p3y - p1x*p4y - p1y*p3x + p1y*p4x - p2x*p3y + p2x*p4y + p2y*p3x - p2y*p4x + # denom = p1x * (p3y - p4y) + p1y * (p4x - p3x) + p2x * (p4y - p3y) + p2y * (p3x - p4x) + denom = (p1x - p2x) * (p3y - p4y) + (p1y - p2y) * (p4x - p3x) + + try: + # t1 = (p1x*p3y - p1x*p4y - p1y*p3x + p1y*p4x + p3x*p4y - p3y*p4x) / denom + # t2 = (-p1x*p2y + p1x*p3y + p1y*p2x - p1y*p3x - p2x*p3y + p2y*p3x) / denom + t1 = ((p1y - p3y) * (p4x - p3x) - (p1x - p3x) * (p4y - p3y)) / denom + t2 = ((p1y - p3y) * (p2x - p3x) - (p1x - p3x) * (p2y - p3y)) / denom + except ZeroDivisionError: + return False + + return (0 <= t1 <= 1) and (0 <= t2 <= 1) + + def shadows(self, other): + """Return true if two segments overlap each other even if they aren't on the same horiz or vertical track.""" + + if self.p1.x == self.p2.x and other.p1.x == other.p2.x: + # Horizontal segments. See if their vertical extents overlap. + self_min = min(self.p1.y, self.p2.y) + self_max = max(self.p1.y, self.p2.y) + other_min = min(other.p1.y, other.p2.y) + other_max = max(other.p1.y, other.p2.y) + elif self.p1.y == self.p2.y and other.p1.y == other.p2.y: + # Verttical segments. See if their horizontal extents overlap. + self_min = min(self.p1.x, self.p2.x) + self_max = max(self.p1.x, self.p2.x) + other_min = min(other.p1.x, other.p2.x) + other_max = max(other.p1.x, other.p2.x) + else: + # Segments aren't horizontal or vertical, so neither can shadow the other. + return False + + # Overlap conditions based on segment endpoints. + return other_min < self_max and other_max > self_min diff --git a/src/faebryk/exporters/schematic/kicad/net_terminal.py b/src/faebryk/exporters/schematic/kicad/net_terminal.py new file mode 100644 index 00000000..e1d9e869 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/net_terminal.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +from skidl import Part, Pin +from skidl.utilities import export_to_all +from .geometry import Point, Tx, Vector + + +""" +Net_Terminal class for handling net labels. +""" + + +@export_to_all +class NetTerminal(Part): + def __init__(self, net, tool_module): + """Specialized Part with a single pin attached to a net. + + This is intended for attaching to nets to label them, typically when + the net spans across levels of hierarchical nodes. + """ + + # Create a Part. + from skidl import SKIDL + + super().__init__(name="NT", ref_prefix="NT", tool=SKIDL) + + # Set a default transformation matrix for this part. + self.tx = Tx() + + # Add a single pin to the part. + pin = Pin(num="1", name="~") + self.add_pins(pin) + + # Connect the pin to the net. + pin += net + + # Set the pin at point (0,0) and pointing leftward toward the part body + # (consisting of just the net label for this type of part) so any attached routing + # will go to the right. + pin.x, pin.y = 0, 0 + pin.pt = Point(pin.x, pin.y) + pin.orientation = "L" + + # Calculate the bounding box, but as if the pin were pointed right so + # the associated label text would go to the left. + self.bbox = tool_module.calc_hier_label_bbox(net.name, "R") + + # Resize bbox so it's an integer number of GRIDs. + self.bbox = self.bbox.snap_resize(tool_module.constants.GRID) + + # Extend the bounding box a bit so any attached routing will come straight in. + self.bbox.max += Vector(tool_module.constants.GRID, 0) + self.lbl_bbox = self.bbox + + # Flip the NetTerminal horizontally if it is an output net (label on the right). + netio = getattr(net, "netio", "").lower() + self.orientation_locked = bool(netio in ("i", "o")) + if getattr(net, "netio", "").lower() == "o": + origin = Point(0, 0) + term_origin = self.tx.origin + self.tx = ( + self.tx.move(origin - term_origin).flip_x().move(term_origin - origin) + ) diff --git a/src/faebryk/exporters/schematic/kicad/node.py b/src/faebryk/exporters/schematic/kicad/node.py new file mode 100644 index 00000000..9c658051 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/node.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +import re +from collections import defaultdict +from itertools import chain + +from skidl.utilities import export_to_all, rmv_attr +from .geometry import BBox, Point, Tx, Vector +from .place import Placer +from .route import Router + + +""" +Node class for storing circuit hierarchy. +""" + + +@export_to_all +class Node(Placer, Router): + """Data structure for holding information about a node in the circuit hierarchy.""" + + filename_sz = 20 + name_sz = 40 + + def __init__( + self, + circuit=None, + tool_module=None, + filepath=".", + top_name="", + title="", + flatness=0.0, + ): + self.parent = None + self.children = defaultdict( + lambda: Node(None, tool_module, filepath, top_name, title, flatness) + ) + self.filepath = filepath + self.top_name = top_name + self.sheet_name = None + self.sheet_filename = None + self.title = title + self.flatness = flatness + self.flattened = False + self.tool_module = tool_module # Backend tool. + self.parts = [] + self.wires = defaultdict(list) + self.junctions = defaultdict(list) + self.tx = Tx() + self.bbox = BBox() + + if circuit: + self.add_circuit(circuit) + + def find_node_with_part(self, part): + """Find the node that contains the part based on its hierarchy. + + Args: + part (Part): The part being searched for in the node hierarchy. + + Returns: + Node: The Node object containing the part. + """ + + from skidl.circuit import HIER_SEP + + level_names = part.hierarchy.split(HIER_SEP) + node = self + for lvl_nm in level_names[1:]: + node = node.children[lvl_nm] + assert part in node.parts + return node + + def add_circuit(self, circuit): + """Add parts in circuit to node and its children. + + Args: + circuit (Circuit): Circuit object. + """ + + # Build the circuit node hierarchy by adding the parts. + for part in circuit.parts: + self.add_part(part) + + # Add terminals to nodes in the hierarchy for nets that span across nodes. + for net in circuit.nets: + # Skip nets that are stubbed since there will be no wire to attach to the NetTerminal. + if getattr(net, "stub", False): + continue + + # Search for pins in different nodes. + for pin1, pin2 in zip(net.pins[:-1], net.pins[1:]): + if pin1.part.hierarchy != pin2.part.hierarchy: + # Found pins in different nodes, so break and add terminals to nodes below. + break + else: + if len(net.pins) == 1: + # Single pin on net and not stubbed, so add a terminal to it below. + pass + elif not net.is_implicit(): + # The net has a user-assigned name, so add a terminal to it below. + pass + else: + # No need for net terminal because there are multiple pins + # and they are all in the same node. + continue + + # Add a single terminal to each node that contains one or more pins of the net. + visited = [] + for pin in net.pins: + # A stubbed pin can't be used to add NetTerminal since there is no explicit wire. + if pin.stub: + continue + + part = pin.part + + if part.hierarchy in visited: + # Already added a terminal to this node, so don't add another. + continue + + # Add NetTerminal to the node with this part/pin. + self.find_node_with_part(part).add_terminal(net) + + # Record that this hierarchical node was visited. + visited.append(part.hierarchy) + + # Flatten the hierarchy as specified by the flatness parameter. + self.flatten(self.flatness) + + def add_part(self, part, level=0): + """Add a part to the node at the appropriate level of the hierarchy. + + Args: + part (Part): Part to be added to this node or one of its children. + level (int, optional): The current level (depth) of the node in the hierarchy. Defaults to 0. + """ + + from skidl.circuit import HIER_SEP + + # Get list of names of hierarchical levels (in order) leading to this part. + level_names = part.hierarchy.split(HIER_SEP) + + # Get depth in hierarchy for this part. + part_level = len(level_names) - 1 + assert part_level >= level + + # Node name is the name assigned to this level of the hierarchy. + self.name = level_names[level] + + # File name for storing the schematic for this node. + base_filename = "_".join([self.top_name] + level_names[0 : level + 1]) + ".sch" + self.sheet_filename = base_filename + + if part_level == level: + # Add part to node at this level in the hierarchy. + if not part.unit: + # Monolithic part so just add it to the node. + self.parts.append(part) + else: + # Multi-unit part so add each unit to the node. + # FIXME: Some part units might be split into other nodes. + for p in part.unit.values(): + self.parts.append(p) + else: + # Part is at a level below the current node. Get the child node using + # the name of the next level in the hierarchy for this part. + child_node = self.children[level_names[level + 1]] + + # Attach the child node to this node. (It may have just been created.) + child_node.parent = self + + # Add part to the child node (or one of its children). + child_node.add_part(part, level + 1) + + def add_terminal(self, net): + """Add a terminal for this net to the node. + + Args: + net (Net): The net to be added to this node. + """ + + from skidl.circuit import HIER_SEP + from .net_terminal import NetTerminal + + nt = NetTerminal(net, self.tool_module) + self.parts.append(nt) + + def external_bbox(self): + """Return the bounding box of a hierarchical sheet as seen by its parent node.""" + bbox = BBox(Point(0, 0), Point(500, 500)) + bbox.add(Point(len("File: " + self.sheet_filename) * self.filename_sz, 0)) + bbox.add(Point(len("Sheet: " + self.name) * self.name_sz, 0)) + + # Pad the bounding box for extra spacing when placed. + bbox = bbox.resize(Vector(100, 100)) + + return bbox + + def internal_bbox(self): + """Return the bounding box for the circuitry contained within this node.""" + + # The bounding box is determined by the arrangement of the node's parts and child nodes. + bbox = BBox() + for obj in chain(self.parts, self.children.values()): + tx_bbox = obj.bbox * obj.tx + bbox.add(tx_bbox) + + # Pad the bounding box for extra spacing when placed. + bbox = bbox.resize(Vector(100, 100)) + + return bbox + + def calc_bbox(self): + """Compute the bounding box for the node in the circuit hierarchy.""" + + if self.flattened: + self.bbox = self.internal_bbox() + else: + # Use hierarchical bounding box if node has not been flattened. + self.bbox = self.external_bbox() + + return self.bbox + + def flatten(self, flatness=0.0): + """Flatten node hierarchy according to flatness parameter. + + Args: + flatness (float, optional): Degree of hierarchical flattening (0=completely hierarchical, 1=totally flat). Defaults to 0.0. + + Create hierarchical sheets for the node and its child nodes. Complexity (or size) of a node + and its children is the total number of part pins they contain. The sum of all the child sizes + multiplied by the flatness is the number of part pins that can be shown on the schematic + page before hierarchy is used. The instances of each type of child are flattened and placed + directly in the sheet as long as the sum of their sizes is below the slack. Otherwise, the + children are included using hierarchical sheets. The children are handled in order of + increasing size so small children are more likely to be flattened while large, complicated + children are included using hierarchical sheets. + """ + + # Create sheets and compute complexity for any circuitry in hierarchical child nodes. + for child in self.children.values(): + child.flatten(flatness) + + # Complexity of the parts directly instantiated at this hierarchical level. + self.complexity = sum((len(part) for part in self.parts)) + + # Sum the child complexities and use it to compute the number of pins that can be + # shown before hierarchical sheets are used. + child_complexity = sum((child.complexity for child in self.children.values())) + slack = child_complexity * flatness + + # Group the children according to what types of modules they are by removing trailing instance ids. + child_types = defaultdict(list) + for child_id, child in self.children.items(): + child_types[re.sub(r"\d+$", "", child_id)].append(child) + + # Compute the total size of each type of children. + child_type_sizes = dict() + for child_type, children in child_types.items(): + child_type_sizes[child_type] = sum((child.complexity for child in children)) + + # Sort the groups from smallest total size to largest. + sorted_child_type_sizes = sorted( + child_type_sizes.items(), key=lambda item: item[1] + ) + + # Flatten each instance in a group until the slack is used up. + for child_type, child_type_size in sorted_child_type_sizes: + if child_type_size <= slack: + # Include the circuitry of each child instance directly in the sheet. + for child in child_types[child_type]: + child.flattened = True + # Reduce the slack by the sum of the child sizes. + slack -= child_type_size + else: + # Not enough slack left. Add these children as hierarchical sheets. + for child in child_types[child_type]: + child.flattened = False + + def get_internal_nets(self): + """Return a list of nets that have at least one pin on a part in this node.""" + + processed_nets = [] + internal_nets = [] + for part in self.parts: + for part_pin in part: + # No explicit wire for pins connected to labeled stub nets. + if part_pin.stub: + continue + + # No explicit wires if the pin is not connected to anything. + if not part_pin.is_connected(): + continue + + net = part_pin.net + + # Skip nets that have already been processed. + if net in processed_nets: + continue + + processed_nets.append(net) + + # Skip stubbed nets. + if getattr(net, "stub", False) is True: + continue + + # Add net to collection if at least one pin is on one of the parts of the node. + for net_pin in net.pins: + if net_pin.part in self.parts: + internal_nets.append(net) + break + + return internal_nets + + def get_internal_pins(self, net): + """Return the pins on the net that are on parts in the node. + + Args: + net (Net): The net whose pins are being examined. + + Returns: + list: List of pins on the net that are on parts in this node. + """ + + # Skip pins on stubbed nets. + if getattr(net, "stub", False) is True: + return [] + + return [pin for pin in net.pins if pin.stub is False and pin.part in self.parts] + + def collect_stats(self, **options): + """Return comma-separated string with place & route statistics of a schematic.""" + + def get_wire_length(node): + """Return the sum of the wire segment lengths between parts in a routed node.""" + + wire_length = 0 + + # Sum wire lengths for child nodes. + for child in node.children.values(): + wire_length += get_wire_length(child) + + # Add the wire lengths between parts in the top node. + for wire_segs in node.wires.values(): + for seg in wire_segs: + len_x = abs(seg.p1.x - seg.p2.x) + len_y = abs(seg.p1.y - seg.p2.y) + wire_length += len_x + len_y + + return wire_length + + return "{}\n".format(get_wire_length(self)) diff --git a/src/faebryk/exporters/schematic/kicad/place.py b/src/faebryk/exporters/schematic/kicad/place.py new file mode 100644 index 00000000..38e7b1b0 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/place.py @@ -0,0 +1,1554 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Autoplacer for arranging symbols in a schematic. +""" + +import functools +import itertools +import math +import random +import sys +from collections import defaultdict +from copy import copy + +from skidl import Pin, rmv_attr + +import faebryk.library._F as F + +from .debug_draw import ( + draw_pause, + draw_placement, + draw_redraw, + draw_start, + draw_text, +) +from .geometry import BBox, Point, Tx, Vector + +__all__ = [ + "PlacementFailure", +] + + +################################################################### +# +# OVERVIEW OF AUTOPLACER +# +# The input is a Node containing child nodes and parts. The parts in +# each child node are placed, and then the blocks for each child are +# placed along with the parts in this node. +# +# The individual parts in a node are separated into groups: +# 1) multiple groups of parts that are all interconnected by one or +# more nets, and 2) a single group of parts that are not connected +# by any explicit nets (i.e., floating parts). +# +# Each group of connected parts are placed using force-directed placement. +# Each net exerts an attractive force pulling parts together, and +# any overlap of parts exerts a repulsive force pushing them apart. +# Initially, the attractive force is dominant but, over time, it is +# decreased while the repulsive force is increased using a weighting +# factor. After that, any part overlaps are cleared and the parts +# are aligned to the routing grid. +# +# Force-directed placement is also used with the floating parts except +# the non-existent net forces are replaced by a measure of part similarity. +# This collects similar parts (such as bypass capacitors) together. +# +# The child-node blocks are then arranged with the blocks of connected +# and floating parts to arrive at a total placement for this node. +# +################################################################### + + +class PlacementFailure(Exception): + """Exception raised when parts or blocks could not be placed.""" + + pass + + +# Small functions for summing Points and Vectors. +pt_sum = lambda pts: sum(pts, Point(0, 0)) +force_sum = lambda forces: sum(forces, Vector(0, 0)) + + +def is_net_terminal(part): + from skidl.schematics.net_terminal import NetTerminal + + return isinstance(part, NetTerminal) + + +def get_snap_pt(part_or_blk): + """Get the point for snapping the Part or PartBlock to the grid. + + Args: + part_or_blk (Part | PartBlock): Object with snap point. + + Returns: + Point: Point for snapping to grid or None if no point found. + """ + try: + return part_or_blk.pins[0].pt + except AttributeError: + try: + return part_or_blk.snap_pt + except AttributeError: + return None + + +def snap_to_grid(part_or_blk): + """Snap Part or PartBlock to grid. + + Args: + part (Part | PartBlk): Object to snap to grid. + """ + + # Get the position of the current snap point. + pt = get_snap_pt(part_or_blk) * part_or_blk.tx + + # This is where the snap point should be on the grid. + snap_pt = pt.snap(GRID) + + # This is the required movement to get on-grid. + mv = snap_pt - pt + + # Update the object's transformation matrix. + snap_tx = Tx(dx=mv.x, dy=mv.y) + part_or_blk.tx *= snap_tx + + +def add_placement_bboxes(parts, **options): + """Expand part bounding boxes to include space for subsequent routing.""" + from skidl.schematics.net_terminal import NetTerminal + + for part in parts: + # Placement bbox starts off with the part bbox (including any net labels). + part.place_bbox = BBox() + part.place_bbox.add(part.lbl_bbox) + + # Compute the routing area for each side based on the number of pins on each side. + padding = {"U": 1, "D": 1, "L": 1, "R": 1} # Min padding of 1 channel per side. + for pin in part: + if pin.stub is False and pin.is_connected(): + padding[pin.orientation] += 1 + + # expansion_factor > 1 is used to expand the area for routing around each part, + # usually in response to a failed routing phase. But don't expand the routing + # around NetTerminals since those are just used to label wires. + if isinstance(part, NetTerminal): + expansion_factor = 1 + else: + expansion_factor = options.get("expansion_factor", 1.0) + + # Add padding for routing to the right and upper sides. + part.place_bbox.add( + part.place_bbox.max + + (Point(padding["L"], padding["D"]) * GRID * expansion_factor) + ) + + # Add padding for routing to the left and lower sides. + part.place_bbox.add( + part.place_bbox.min + - (Point(padding["R"], padding["U"]) * GRID * expansion_factor) + ) + + +def get_enclosing_bbox(parts): + """Return bounding box that encloses all the parts.""" + return BBox().add(*(part.place_bbox * part.tx for part in parts)) + + +def add_anchor_pull_pins(parts, nets, **options): + """Add positions of anchor and pull pins for attractive net forces between parts. + + Args: + part (list): List of movable parts. + nets (list): List of attractive nets between parts. + options (dict): Dict of options and values that enable/disable functions. + """ + + def add_place_pt(part, pin): + """Add the point for a pin on the placement boundary of a part.""" + + pin.route_pt = pin.pt # For drawing of nets during debugging. + pin.place_pt = Point(pin.pt.x, pin.pt.y) + if pin.orientation == "U": + pin.place_pt.y = part.place_bbox.min.y + elif pin.orientation == "D": + pin.place_pt.y = part.place_bbox.max.y + elif pin.orientation == "L": + pin.place_pt.x = part.place_bbox.max.x + elif pin.orientation == "R": + pin.place_pt.x = part.place_bbox.min.x + else: + raise RuntimeError("Unknown pin orientation.") + + # Remove any existing anchor and pull pins before making new ones. + rmv_attr(parts, ("anchor_pins", "pull_pins")) + + # Add dicts for anchor/pull pins and pin centroids to each movable part. + for part in parts: + part.anchor_pins = defaultdict(list) + part.pull_pins = defaultdict(list) + part.pin_ctrs = dict() + + if nets: + # If nets exist, then these parts are interconnected so + # assign pins on each net to part anchor and pull pin lists. + for net in nets: + # Get net pins that are on movable parts. + pins = {pin for pin in net.pins if pin.part in parts} + + # Get the set of parts with pins on the net. + net.parts = {pin.part for pin in pins} + + # Add each pin as an anchor on the part that contains it and + # as a pull pin on all the other parts that will be pulled by this part. + for pin in pins: + pin.part.anchor_pins[net].append(pin) + add_place_pt(pin.part, pin) + for part in net.parts - {pin.part}: + # NetTerminals are pulled towards connected parts, but + # those parts are not attracted towards NetTerminals. + if not is_net_terminal(pin.part): + part.pull_pins[net].append(pin) + + # For each net, assign the centroid of the part's anchor pins for that net. + for net in nets: + for part in net.parts: + if part.anchor_pins[net]: + part.pin_ctrs[net] = pt_sum( + pin.place_pt for pin in part.anchor_pins[net] + ) / len(part.anchor_pins[net]) + + else: + # There are no nets so these parts are floating freely. + # Floating parts are all pulled by each other. + all_pull_pins = [] + for part in parts: + try: + # Set anchor at top-most pin so floating part tops will align. + anchor_pull_pin = max(part.pins, key=lambda pin: pin.pt.y) + add_place_pt(part, anchor_pull_pin) + except ValueError: + # Set anchor for part with no pins at all. + anchor_pull_pin = Pin() + anchor_pull_pin.place_pt = part.place_bbox.max + part.anchor_pins["similarity"] = [anchor_pull_pin] + part.pull_pins["similarity"] = all_pull_pins + all_pull_pins.append(anchor_pull_pin) + + +def save_anchor_pull_pins(parts): + """Save anchor/pull pins for each part before they are changed.""" + for part in parts: + part.saved_anchor_pins = copy(part.anchor_pins) + part.saved_pull_pins = copy(part.pull_pins) + + +def restore_anchor_pull_pins(parts): + """Restore the original anchor/pull pin lists for each Part.""" + + for part in parts: + if hasattr(part, "saved_anchor_pins"): + # Saved pin lists exist, so restore them to the original anchor/pull pin lists. + part.anchor_pins = part.saved_anchor_pins + part.pull_pins = part.saved_pull_pins + + # Remove the attributes where the original lists were saved. + rmv_attr(parts, ("saved_anchor_pins", "saved_pull_pins")) + + +def adjust_orientations(parts, **options): + """Adjust orientation of parts. + + Args: + parts (list): List of Parts to adjust. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + bool: True if one or more part orientations were changed. Otherwise, False. + """ + + def find_best_orientation(part): + """Each part has 8 possible orientations. Find the best of the 7 alternatives from the starting one.""" + + # Store starting orientation. + part.prev_tx = copy(part.tx) + + # Get centerpoint of part for use when doing rotations/flips. + part_ctr = (part.place_bbox * part.tx).ctr + + # Now find the orientation that has the largest decrease (or smallest increase) in cost. + # Go through four rotations, then flip the part and go through the rotations again. + best_delta_cost = float("inf") + calc_starting_cost = True + for i in range(2): + for j in range(4): + if calc_starting_cost: + # Calculate the cost of the starting orientation before any changes in orientation. + starting_cost = net_tension(part, **options) + # Skip the starting orientation but set flag to process the others. + calc_starting_cost = False + else: + # Calculate the cost of the current orientation. + delta_cost = net_tension(part, **options) - starting_cost + if delta_cost < best_delta_cost: + # Save the largest decrease in cost and the associated orientation. + best_delta_cost = delta_cost + best_tx = copy(part.tx) + + # Proceed to the next rotation. + part.tx = part.tx.move(-part_ctr).rot_90cw().move(part_ctr) + + # Flip the part and go through the rotations again. + part.tx = part.tx.move(-part_ctr).flip_x().move(part_ctr) + + # Save the largest decrease in cost and the associated orientation. + part.delta_cost = best_delta_cost + part.delta_cost_tx = best_tx + + # Restore the original orientation. + part.tx = part.prev_tx + + # Get the list of parts that don't have their orientations locked. + movable_parts = [part for part in parts if not part.orientation_locked] + + if not movable_parts: + # No movable parts, so exit without doing anything. + return + + # Kernighan-Lin algorithm for finding near-optimal part orientations. + # Because of the way the tension for part alignment is computed based on + # the nearest part, it is possible for an infinite loop to occur. + # Hence the ad-hoc loop limit. + for iter_cnt in range(10): + # Find the best part to move and move it until there are no more parts to move. + moved_parts = [] + unmoved_parts = movable_parts[:] + while unmoved_parts: + # Find the best current orientation for each unmoved part. + for part in unmoved_parts: + find_best_orientation(part) + + # Find the part that has the largest decrease in cost. + part_to_move = min(unmoved_parts, key=lambda p: p.delta_cost) + + # Reorient the part with the Tx that created the largest decrease in cost. + part_to_move.tx = part_to_move.delta_cost_tx + + # Transfer the part from the unmoved to the moved part list. + unmoved_parts.remove(part_to_move) + moved_parts.append(part_to_move) + + # Find the point at which the cost reaches its lowest point. + # delta_cost at location i is the change in cost *before* part i is moved. + # Start with cost change of zero before any parts are moved. + delta_costs = [ + 0, + ] + delta_costs.extend((part.delta_cost for part in moved_parts)) + try: + cost_seq = list(itertools.accumulate(delta_costs)) + except AttributeError: + # Python 2.7 doesn't have itertools.accumulate(). + cost_seq = list(delta_costs) + for i in range(1, len(cost_seq)): + cost_seq[i] = cost_seq[i - 1] + cost_seq[i] + min_cost = min(cost_seq) + min_index = cost_seq.index(min_cost) + + # Move all the parts after that point back to their starting positions. + for part in moved_parts[min_index:]: + part.tx = part.prev_tx + + # Terminate the search if no part orientations were changed. + if min_index == 0: + break + + rmv_attr(parts, ("prev_tx", "delta_cost", "delta_cost_tx")) + + # Return True if one or more iterations were done, indicating part orientations were changed. + return iter_cnt > 0 + + +def net_tension_dist(part, **options): + """Calculate the tension of the nets trying to rotate/flip the part. + + Args: + part (Part): Part affected by forces from other connected parts. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + float: Total tension on the part. + """ + + # Compute the force for each net attached to the part. + tension = 0.0 + for net, anchor_pins in part.anchor_pins.items(): + pull_pins = part.pull_pins[net] + + if not anchor_pins or not pull_pins: + # Skip nets without pulling or anchor points. + continue + + # Compute the net force acting on each anchor point on the part. + for anchor_pin in anchor_pins: + # Compute the anchor point's (x,y). + anchor_pt = anchor_pin.place_pt * anchor_pin.part.tx + + # Find the dist from the anchor point to each pulling point. + dists = [ + (anchor_pt - pp.place_pt * pp.part.tx).magnitude for pp in pull_pins + ] + + # Only the closest pulling point affects the tension since that is + # probably where the wire routing will go to. + tension += min(dists) + + return tension + + +def net_torque_dist(part, **options): + """Calculate the torque of the nets trying to rotate/flip the part. + + Args: + part (Part): Part affected by forces from other connected parts. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + float: Total torque on the part. + """ + + # Part centroid for computing torque. + ctr = part.place_bbox.ctr * part.tx + + # Get the force multiplier applied to point-to-point nets. + pt_to_pt_mult = options.get("pt_to_pt_mult", 1) + + # Compute the torque for each net attached to the part. + torque = 0.0 + for net, anchor_pins in part.anchor_pins.items(): + pull_pins = part.pull_pins[net] + + if not anchor_pins or not pull_pins: + # Skip nets without pulling or anchor points. + continue + + pull_pin_pts = [pin.place_pt * pin.part.tx for pin in pull_pins] + + # Multiply the force exerted by point-to-point nets. + force_mult = pt_to_pt_mult if len(pull_pin_pts) <= 1 else 1 + + # Compute the net torque acting on each anchor point on the part. + for anchor_pin in anchor_pins: + # Compute the anchor point's (x,y). + anchor_pt = anchor_pin.place_pt * part.tx + + # Compute torque around part center from force between anchor & pull pins. + normalize = len(pull_pin_pts) + lever_norm = (anchor_pt - ctr).norm + for pull_pt in pull_pin_pts: + frc_norm = (pull_pt - anchor_pt).norm + torque += lever_norm.xprod(frc_norm) * force_mult / normalize + + return abs(torque) + + +# Select the net tension method used for the adjusting the orientation of parts. +net_tension = net_tension_dist +# net_tension = net_torque_dist + + +def net_force_dist(part, **options): + """Compute attractive force on a part from all the other parts connected to it. + + Args: + part (Part): Part affected by forces from other connected parts. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Force upon given part. + """ + + # Get the anchor and pull pins for each net connected to this part. + anchor_pins = part.anchor_pins + pull_pins = part.pull_pins + + # Get the force multiplier applied to point-to-point nets. + pt_to_pt_mult = options.get("pt_to_pt_mult", 1) + + # Compute the total force on the part from all the anchor/pulling points on each net. + total_force = Vector(0, 0) + + # Parts with a lot of pins can accumulate large net forces that move them very quickly. + # Accumulate the number of individual net forces and use that to attenuate + # the total force, effectively normalizing the forces between large & small parts. + net_normalizer = 0 + + # Compute the force for each net attached to the part. + for net in anchor_pins.keys(): + if not anchor_pins[net] or not pull_pins[net]: + # Skip nets without pulling or anchor points. + continue + + # Multiply the force exerted by point-to-point nets. + force_mult = pt_to_pt_mult if len(pull_pins[net]) <= 1 else 1 + + # Initialize net force. + net_force = Vector(0, 0) + + pin_normalizer = 0 + + # Compute the anchor and pulling point (x,y)s for the net. + anchor_pts = [pin.place_pt * pin.part.tx for pin in anchor_pins[net]] + pull_pts = [pin.place_pt * pin.part.tx for pin in pull_pins[net]] + + # Compute the net force acting on each anchor point on the part. + for anchor_pt in anchor_pts: + # Sum the forces from each pulling point on the anchor point. + for pull_pt in pull_pts: + # Get the distance from the pull pt to the anchor point. + dist_vec = pull_pt - anchor_pt + + # Add the force on the anchor pin from the pulling pin. + net_force += dist_vec + + # Increment the normalizer for every pull force added to the net force. + pin_normalizer += 1 + + if options.get("pin_normalize"): + # Normalize the net force across all the anchor & pull pins. + pin_normalizer = pin_normalizer or 1 # Prevent div-by-zero. + net_force /= pin_normalizer + + # Accumulate force from this net into the total force on the part. + # Multiply force if the net meets stated criteria. + total_force += net_force * force_mult + + # Increment the normalizer for every net force added to the total force. + net_normalizer += 1 + + if options.get("net_normalize"): + # Normalize the total force across all the nets. + net_normalizer = net_normalizer or 1 # Prevent div-by-zero. + total_force /= net_normalizer + + return total_force + + +# Select the net force method used for the attraction of parts during placement. +attractive_force = net_force_dist + + +def overlap_force(part, parts, **options): + """Compute the repulsive force on a part from overlapping other parts. + + Args: + part (Part): Part affected by forces from other overlapping parts. + parts (list): List of parts to check for overlaps. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Force upon given part. + """ + + # Bounding box of given part. + part_bbox = part.place_bbox * part.tx + + # Compute the overlap force of the bbox of this part with every other part. + total_force = Vector(0, 0) + for other_part in set(parts) - {part}: + other_part_bbox = other_part.place_bbox * other_part.tx + + # No force unless parts overlap. + if part_bbox.intersects(other_part_bbox): + # Compute the movement needed to separate the bboxes in left/right/up/down directions. + # Add some small random offset to break symmetry when parts exactly overlay each other. + # Move right edge of part to the left of other part's left edge, etc... + moves = [] + rnd = Vector(random.random() - 0.5, random.random() - 0.5) + for edges, dir in ( + (("ll", "lr"), Vector(1, 0)), + (("ul", "ll"), Vector(0, 1)), + ): + move = ( + getattr(other_part_bbox, edges[0]) + - getattr(part_bbox, edges[1]) + - rnd + ) * dir + moves.append([move.magnitude, move]) + # Flip edges... + move = ( + getattr(other_part_bbox, edges[1]) + - getattr(part_bbox, edges[0]) + - rnd + ) * dir + moves.append([move.magnitude, move]) + + # Select the smallest move that separates the parts. + move = min(moves, key=lambda m: m[0]) + + # Add the move to the total force on the part. + total_force += move[1] + + return total_force + + +def overlap_force_rand(part, parts, **options): + """Compute the repulsive force on a part from overlapping other parts. + + Args: + part (Part): Part affected by forces from other overlapping parts. + parts (list): List of parts to check for overlaps. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Force upon given part. + """ + + # Bounding box of given part. + part_bbox = part.place_bbox * part.tx + + # Compute the overlap force of the bbox of this part with every other part. + total_force = Vector(0, 0) + for other_part in set(parts) - {part}: + other_part_bbox = other_part.place_bbox * other_part.tx + + # No force unless parts overlap. + if part_bbox.intersects(other_part_bbox): + # Compute the movement needed to clear the bboxes in left/right/up/down directions. + # Add some small random offset to break symmetry when parts exactly overlay each other. + # Move right edge of part to the left of other part's left edge. + moves = [] + rnd = Vector(random.random() - 0.5, random.random() - 0.5) + for edges, dir in ( + (("ll", "lr"), Vector(1, 0)), + (("lr", "ll"), Vector(1, 0)), + (("ul", "ll"), Vector(0, 1)), + (("ll", "ul"), Vector(0, 1)), + ): + move = ( + getattr(other_part_bbox, edges[0]) + - getattr(part_bbox, edges[1]) + - rnd + ) * dir + moves.append([move.magnitude, move]) + accum = 0 + for move in moves: + accum += move[0] + for move in moves: + move[0] = accum - move[0] + new_accum = 0 + for move in moves: + move[0] += new_accum + new_accum = move[0] + select = new_accum * random.random() + for move in moves: + if move[0] >= select: + total_force += move[1] + break + + return total_force + + +# Select the overlap force method used for the repulsion of parts during placement. +repulsive_force = overlap_force +# repulsive_force = overlap_force_rand + + +def scale_attractive_repulsive_forces(parts, force_func, **options): + """Set scaling between attractive net forces and repulsive part overlap forces.""" + + # Store original part placement. + for part in parts: + part.original_tx = copy(part.tx) + + # Find attractive forces when they are maximized by random part placement. + random_placement(parts, **options) + attractive_forces_sum = sum( + force_func(p, parts, alpha=0, scale=1, **options).magnitude for p in parts + ) + + # Find repulsive forces when they are maximized by compacted part placement. + central_placement(parts, **options) + repulsive_forces_sum = sum( + force_func(p, parts, alpha=1, scale=1, **options).magnitude for p in parts + ) + + # Restore original part placement. + for part in parts: + part.tx = part.original_tx + rmv_attr(parts, ["original_tx"]) + + # Return scaling factor that makes attractive forces about the same as repulsive forces. + try: + return repulsive_forces_sum / attractive_forces_sum + except ZeroDivisionError: + # No attractive forces, so who cares about scaling? Set it to 1. + return 1 + + +def total_part_force(part, parts, scale, alpha, **options): + """Compute the total of the attractive net and repulsive overlap forces on a part. + + Args: + part (Part): Part affected by forces from other overlapping parts. + parts (list): List of parts to check for overlaps. + scale (float): Scaling factor for net forces to make them equivalent to overlap forces. + alpha (float): Fraction of the total that is the overlap force (range [0,1]). + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Weighted total of net attractive and overlap repulsion forces. + """ + force = scale * (1 - alpha) * attractive_force( + part, **options + ) + alpha * repulsive_force(part, parts, **options) + part.force = force # For debug drawing. + return force + + +def similarity_force(part, parts, similarity, **options): + """Compute attractive force on a part from all the other parts connected to it. + + Args: + part (Part): Part affected by similarity forces with other parts. + similarity (dict): Similarity score for any pair of parts used as keys. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Force upon given part. + """ + + # Get the single anchor point for similarity forces affecting this part. + anchor_pt = part.anchor_pins["similarity"][0].place_pt * part.tx + + # Compute the combined force of all the similarity pulling points. + total_force = Vector(0, 0) + for pull_pin in part.pull_pins["similarity"]: + pull_pt = pull_pin.place_pt * pull_pin.part.tx + # Force from pulling to anchor point is proportional to part similarity and distance. + total_force += (pull_pt - anchor_pt) * similarity[part][pull_pin.part] + + return total_force + + +def total_similarity_force(part, parts, similarity, scale, alpha, **options): + """Compute the total of the attractive similarity and repulsive overlap forces on a part. + + Args: + part (Part): Part affected by forces from other overlapping parts. + parts (list): List of parts to check for overlaps. + similarity (dict): Similarity score for any pair of parts used as keys. + scale (float): Scaling factor for similarity forces to make them equivalent to overlap forces. + alpha (float): Proportion of the total that is the overlap force (range [0,1]). + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Weighted total of net attractive and overlap repulsion forces. + """ + force = scale * (1 - alpha) * similarity_force( + part, parts, similarity, **options + ) + alpha * repulsive_force(part, parts, **options) + part.force = force # For debug drawing. + return force + + +def define_placement_bbox(parts, **options): + """Return a bounding box big enough to hold the parts being placed.""" + + # Compute appropriate size to hold the parts based on their areas. + area = 0 + for part in parts: + area += part.place_bbox.area + side = 3 * math.sqrt(area) # HACK: Multiplier is ad-hoc. + return BBox(Point(0, 0), Point(side, side)) + + +def central_placement(parts, **options): + """Cluster all part centroids onto a common point. + + Args: + parts (list): List of Parts. + options (dict): Dict of options and values that enable/disable functions. + """ + + if len(parts) <= 1: + # No need to do placement if there's less than two parts. + return + + # Find the centroid of all the parts. + ctr = get_enclosing_bbox(parts).ctr + + # Collapse all the parts to the centroid. + for part in parts: + mv = ctr - part.place_bbox.ctr * part.tx + part.tx *= Tx(dx=mv.x, dy=mv.y) + + +def random_placement(parts, **options): + """Randomly place parts within an appropriately-sized area. + + Args: + parts (list): List of Parts to place. + """ + + # Compute appropriate size to hold the parts based on their areas. + bbox = define_placement_bbox(parts, **options) + + # Place parts randomly within area. + for part in parts: + pt = Point(random.random() * bbox.w, random.random() * bbox.h) + part.tx = part.tx.move(pt) + + +def push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options): + """Move parts under influence of attractive nets and repulsive part overlaps. + + Args: + anchored_parts (list): Set of immobile Parts whose position affects placement. + mobile_parts (list): Set of Parts that can be moved. + nets (list): List of nets that interconnect parts. + force_func: Function for calculating forces between parts. + options (dict): Dict of options and values that enable/disable functions. + """ + + if not options.get("use_push_pull"): + # Abort if push & pull of parts is disabled. + return + + if not mobile_parts: + # No need to do placement if there's nothing to move. + return + + def cost(parts, alpha): + """Cost function for use in debugging. Should decrease as parts move.""" + for part in parts: + part.force = force_func(part, parts, scale=scale, alpha=alpha, **options) + return sum((part.force.magnitude for part in parts)) + + # Get PyGame screen, real-to-screen coord Tx matrix, font for debug drawing. + scr = options.get("draw_scr") + tx = options.get("draw_tx") + font = options.get("draw_font") + txt_org = Point(10, 10) + + # Create the total set of parts exerting forces on each other. + parts = anchored_parts + mobile_parts + + # If there are no anchored parts, then compute the overall drift force + # across all the parts. This will be subtracted so the + # entire group of parts doesn't just continually drift off in one direction. + # This only needs to be done if ALL parts are mobile (i.e., no anchored parts). + rmv_drift = not anchored_parts + + # Set scale factor between attractive net forces and repulsive part overlap forces. + scale = scale_attractive_repulsive_forces(parts, force_func, **options) + + # Setup the schedule for adjusting the alpha coefficient that weights the + # combination of the attractive net forces and the repulsive part overlap forces. + # Start at 0 (all attractive) and gradually progress to 1 (all repulsive). + # Also, set parameters for determining when parts are stable and for restricting + # movements in the X & Y directions when parts are being aligned. + force_schedule = [ + (0.50, 0.0, 0.1, False, (1, 1)), # Attractive forces only. + (0.25, 0.0, 0.01, False, (1, 1)), # Attractive forces only. + # (0.25, 0.2, 0.01, False, (1,1)), # Some repulsive forces. + (0.25, 0.4, 0.1, False, (1, 1)), # More repulsive forces. + # (0.25, 0.6, 0.01, False, (1,1)), # More repulsive forces. + (0.25, 0.8, 0.1, False, (1, 1)), # More repulsive forces. + # (0.25, 0.7, 0.01, True, (1,0)), # Align parts horiz. + # (0.25, 0.7, 0.01, True, (0,1)), # Align parts vert. + # (0.25, 0.7, 0.01, True, (1,0)), # Align parts horiz. + # (0.25, 0.7, 0.01, True, (0,1)), # Align parts vert. + (0.25, 1.0, 0.01, False, (1, 1)), # Remove any part overlaps. + ] + # N = 7 + # force_schedule = [(0.50, i/N, 0.01, False, (1,1)) for i in range(N+1)] + + # Step through the alpha sequence going from all-attractive to all-repulsive forces. + for speed, alpha, stability_coef, align_parts, force_mask in force_schedule: + if align_parts: + # Align parts by only using forces between the closest anchor/pull pins. + retain_closest_anchor_pull_pins(mobile_parts) + else: + # For general placement, use forces between all anchor/pull pins. + restore_anchor_pull_pins(mobile_parts) + + # This stores the threshold below which all the parts are assumed to be stabilized. + # Since it can never be negative, set it to -1 to indicate it's uninitialized. + stable_threshold = -1 + + # Move parts for this alpha until they all settle into fixed positions. + # Place an iteration limit to prevent an infinite loop. + for _ in range(1000): # HACK: Ad-hoc iteration limit. + # Compute forces exerted on the parts by each other. + sum_of_forces = 0 + for part in mobile_parts: + part.force = force_func( + part, parts, scale=scale, alpha=alpha, **options + ) + # Mask X or Y component of force during part alignment. + part.force = part.force.mask(force_mask) + sum_of_forces += part.force.magnitude + + if rmv_drift: + # Calculate the drift force across all parts and subtract it from each part + # to prevent them from continually drifting in one direction. + drift_force = force_sum([part.force for part in mobile_parts]) / len( + mobile_parts + ) + for part in mobile_parts: + part.force -= drift_force + + # Apply movements to part positions. + for part in mobile_parts: + part.mv = part.force * speed + part.tx *= Tx(dx=part.mv.x, dy=part.mv.y) + + # Keep iterating until all the parts are still. + if stable_threshold < 0: + # Set the threshold after the first iteration. + initial_sum_of_forces = sum_of_forces + stable_threshold = sum_of_forces * stability_coef + elif sum_of_forces <= stable_threshold: + # Part positions have stabilized if forces have dropped below threshold. + break + elif sum_of_forces > 10 * initial_sum_of_forces: + # If the forces are getting higher, then that usually means the parts are + # spreading out. This can happen if speed is too large, so reduce it so + # the forces may start to decrease. + speed *= 0.50 + + if scr: + # Draw current part placement for debugging purposes. + draw_placement(parts, nets, scr, tx, font) + draw_text( + "alpha:{alpha:3.2f} iter:{_} force:{sum_of_forces:.1f} stable:{stable_threshold}".format( + **locals() + ), + txt_org, + scr, + tx, + font, + color=(0, 0, 0), + real=False, + ) + draw_redraw() + + +def evolve_placement(anchored_parts, mobile_parts, nets, force_func, **options): + """Evolve part placement looking for optimum using force function. + + Args: + anchored_parts (list): Set of immobile Parts whose position affects placement. + mobile_parts (list): Set of Parts that can be moved. + nets (list): List of nets that interconnect parts. + force_func (function): Computes the force affecting part positions. + options (dict): Dict of options and values that enable/disable functions. + """ + + parts = anchored_parts + mobile_parts + + # Force-directed placement. + push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options) + + # Snap parts to grid. + for part in parts: + snap_to_grid(part) + + +def place_net_terminals(net_terminals, placed_parts, nets, force_func, **options): + """Place net terminals around already-placed parts. + + Args: + net_terminals (list): List of NetTerminals + placed_parts (list): List of placed Parts. + nets (list): List of nets that interconnect parts. + force_func (function): Computes the force affecting part positions. + options (dict): Dict of options and values that enable/disable functions. + """ + + def trim_pull_pins(terminals, bbox): + """Trim pullpins of NetTerminals to the part pins closest to an edge of the bounding box of placed parts. + + Args: + terminals (list): List of NetTerminals. + bbox (BBox): Bounding box of already-placed parts. + + Note: + The rationale for this is that pin closest to an edge of the bounding box will be easier to access. + """ + + for terminal in terminals: + for net, pull_pins in terminal.pull_pins.items(): + insets = [] + for pull_pin in pull_pins: + pull_pt = pull_pin.place_pt * pull_pin.part.tx + + # Get the inset of the terminal pulling pin from each side of the placement area. + # Left side. + insets.append((abs(pull_pt.x - bbox.ll.x), pull_pin)) + # Right side. + insets.append((abs(pull_pt.x - bbox.lr.x), pull_pin)) + # Top side. + insets.append((abs(pull_pt.y - bbox.ul.y), pull_pin)) + # Bottom side. + insets.append((abs(pull_pt.y - bbox.ll.y), pull_pin)) + + # Retain only the pulling pin closest to an edge of the bounding box (i.e., minimum inset). + terminal.pull_pins[net] = [min(insets, key=lambda off: off[0])[1]] + + def orient(terminals, bbox): + """Set orientation of NetTerminals to point away from closest bounding box edge. + + Args: + terminals (list): List of NetTerminals. + bbox (BBox): Bounding box of already-placed parts. + """ + + for terminal in terminals: + # A NetTerminal should already be trimmed so it is attached to a single pin of a part on a single net. + pull_pin = list(terminal.pull_pins.values())[0][0] + pull_pt = pull_pin.place_pt * pull_pin.part.tx + + # Get the inset of the terminal pulling pin from each side of the placement area + # and the Tx() that should be applied if the terminal is placed on that side. + insets = [] + # Left side, so terminal label juts out to the left. + insets.append((abs(pull_pt.x - bbox.ll.x), Tx())) + # Right side, so terminal label flipped to jut out to the right. + insets.append((abs(pull_pt.x - bbox.lr.x), Tx().flip_x())) + # Top side, so terminal label rotated by 270 to jut out to the top. + insets.append( + (abs(pull_pt.y - bbox.ul.y), Tx().rot_90cw().rot_90cw().rot_90cw()) + ) + # Bottom side. so terminal label rotated 90 to jut out to the bottom. + insets.append((abs(pull_pt.y - bbox.ll.y), Tx().rot_90cw())) + + # Apply the Tx() for the side the terminal is closest to. + terminal.tx = min(insets, key=lambda inset: inset[0])[1] + + def move_to_pull_pin(terminals): + """Move NetTerminals immediately to their pulling pins.""" + for terminal in terminals: + anchor_pin = list(terminal.anchor_pins.values())[0][0] + anchor_pt = anchor_pin.place_pt * anchor_pin.part.tx + pull_pin = list(terminal.pull_pins.values())[0][0] + pull_pt = pull_pin.place_pt * pull_pin.part.tx + terminal.tx = terminal.tx.move(pull_pt - anchor_pt) + + def evolution(net_terminals, placed_parts, bbox): + """Evolve placement of NetTerminals starting from outermost from center to innermost.""" + + evolution_type = options.get("terminal_evolution", "all_at_once") + + if evolution_type == "all_at_once": + evolve_placement( + placed_parts, net_terminals, nets, total_part_force, **options + ) + + elif evolution_type == "outer_to_inner": + # Start off with the previously-placed parts as anchored parts. NetTerminals will be added to this as they are placed. + anchored_parts = copy(placed_parts) + + # Sort terminals from outermost to innermost w.r.t. the center. + def dist_to_bbox_edge(term): + pt = term.pins[0].place_pt * term.tx + return min( + ( + abs(pt.x - bbox.ll.x), + abs(pt.x - bbox.lr.x), + abs(pt.y - bbox.ll.y), + abs(pt.y - bbox.ul.y), + ) + ) + + terminals = sorted( + net_terminals, + key=lambda term: dist_to_bbox_edge(term), + reverse=True, + ) + + # Grab terminals starting from the outside and work towards the inside until a terminal intersects a previous one. + mobile_terminals = [] + mobile_bboxes = [] + for terminal in terminals: + terminal_bbox = terminal.place_bbox * terminal.tx + mobile_terminals.append(terminal) + mobile_bboxes.append(terminal_bbox) + for bbox in mobile_bboxes[:-1]: + if terminal_bbox.intersects(bbox): + # The current NetTerminal intersects one of the previously-selected mobile terminals, so evolve the + # placement of all the mobile terminals except the current one. + evolve_placement( + anchored_parts, + mobile_terminals[:-1], + nets, + force_func, + **options, + ) + # Anchor the mobile terminals after their placement is done. + anchored_parts.extend(mobile_terminals[:-1]) + # Remove the placed terminals, leaving only the current terminal. + mobile_terminals = mobile_terminals[-1:] + mobile_bboxes = mobile_bboxes[-1:] + + if mobile_terminals: + # Evolve placement of any remaining terminals. + evolve_placement( + anchored_parts, mobile_terminals, nets, total_part_force, **options + ) + + bbox = get_enclosing_bbox(placed_parts) + save_anchor_pull_pins(net_terminals) + trim_pull_pins(net_terminals, bbox) + orient(net_terminals, bbox) + move_to_pull_pin(net_terminals) + evolution(net_terminals, placed_parts, bbox) + restore_anchor_pull_pins(net_terminals) + + +class Placer: + """Mixin to add place function to Node class.""" + + def group_parts(node, **options): + """Group parts in the Node that are connected by internal nets + + Args: + node (Node): Node with parts. + options (dict, optional): Dictionary of options and values. Defaults to {}. + + Returns: + list: List of lists of Parts that are connected. + list: List of internal nets connecting parts. + list: List of Parts that are not connected to anything (floating). + """ + + if not node.parts: + return [], [], [] + + # Extract list of nets having at least one pin in the node. + internal_nets = node.get_internal_nets() + + # Group all the parts that have some interconnection to each other. + # Start with groups of parts on each individual net. + connected_parts = [ + set(pin.part for pin in net.pins if pin.part in node.parts) + for net in internal_nets + ] + + # Now join groups that have parts in common. + for i in range(len(connected_parts) - 1): + group1 = connected_parts[i] + for j in range(i + 1, len(connected_parts)): + group2 = connected_parts[j] + if group1 & group2: + # If part groups intersect, collect union of parts into one group + # and empty-out the other. + connected_parts[j] = connected_parts[i] | connected_parts[j] + connected_parts[i] = set() + # No need to check against group1 any more since it has been + # unioned into group2 that will be checked later in the loop. + break + + # Remove any empty groups that were unioned into other groups. + connected_parts = [group for group in connected_parts if group] + + # Find parts that aren't connected to anything. + floating_parts = set(node.parts) - set(itertools.chain(*connected_parts)) + + return connected_parts, internal_nets, floating_parts + + def place_connected_parts(node, parts, nets, **options): + """Place individual parts. + + Args: + node (Node): Node with parts. + parts (list): List of Part sets connected by nets. + nets (list): List of internal Nets connecting the parts. + options (dict): Dict of options and values that enable/disable functions. + """ + + if not parts: + # Abort if nothing to place. + return + + # Add bboxes with surrounding area so parts are not butted against each other. + add_placement_bboxes(parts, **options) + + # Set anchor and pull pins that determine attractive forces between parts. + add_anchor_pull_pins(parts, nets, **options) + + # Randomly place connected parts. + random_placement(parts) + + if options.get("draw_placement"): + # Draw the placement for debug purposes. + bbox = get_enclosing_bbox(parts) + draw_scr, draw_tx, draw_font = draw_start(bbox) + options.update( + {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} + ) + + if options.get("compress_before_place"): + central_placement(parts, **options) + + # Do force-directed placement of the parts in the parts. + + # Separate the NetTerminals from the other parts. + net_terminals = [part for part in parts if is_net_terminal(part)] + real_parts = [part for part in parts if not is_net_terminal(part)] + + # Do the first trial placement. + evolve_placement([], real_parts, nets, total_part_force, **options) + + if options.get("rotate_parts"): + # Adjust part orientations after first trial placement is done. + if adjust_orientations(real_parts, **options): + # Some part orientations were changed, so re-do placement. + evolve_placement([], real_parts, nets, total_part_force, **options) + + # Place NetTerminals after all the other parts. + place_net_terminals( + net_terminals, real_parts, nets, total_part_force, **options + ) + + if options.get("draw_placement"): + # Pause to look at placement for debugging purposes. + draw_pause() + + def place_floating_parts(node, parts, **options): + """Place individual parts. + + Args: + node (Node): Node with parts. + parts (list): List of Parts not connected by explicit nets. + options (dict): Dict of options and values that enable/disable functions. + """ + + if not parts: + # Abort if nothing to place. + return + + # Add bboxes with surrounding area so parts are not butted against each other. + add_placement_bboxes(parts) + + # Set anchor and pull pins that determine attractive forces between similar parts. + add_anchor_pull_pins(parts, [], **options) + + # Randomly place the floating parts. + random_placement(parts) + + if options.get("draw_placement"): + # Compute the drawing area for the floating parts + bbox = get_enclosing_bbox(parts) + draw_scr, draw_tx, draw_font = draw_start(bbox) + options.update( + {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} + ) + + # For non-connected parts, do placement based on their similarity to each other. + part_similarity = defaultdict(lambda: defaultdict(lambda: 0)) + for part in parts: + for other_part in parts: + # Don't compute similarity of a part to itself. + if other_part is part: + continue + + # HACK: Get similarity forces right-sized. + part_similarity[part][other_part] = part.similarity(other_part) / 100 + # part_similarity[part][other_part] = 0.1 + + # Select the top-most pin in each part as the anchor point for force-directed placement. + # tx = part.tx + # part.anchor_pin = max(part.anchor_pins, key=lambda pin: (pin.place_pt * tx).y) + + force_func = functools.partial( + total_similarity_force, similarity=part_similarity + ) + + if options.get("compress_before_place"): + # Compress all floating parts together. + central_placement(parts, **options) + + # Do force-directed placement of the parts in the group. + evolve_placement([], parts, [], force_func, **options) + + if options.get("draw_placement"): + # Pause to look at placement for debugging purposes. + draw_pause() + + def place_blocks(node, connected_parts, floating_parts, children, **options): + """Place blocks of parts and hierarchical sheets. + + Args: + node (Node): Node with parts. + connected_parts (list): List of Part sets connected by nets. + floating_parts (set): Set of Parts not connected by any of the internal nets. + children (list): Child nodes in the hierarchy. + non_sheets (list): Hierarchical set of Parts that are visible. + sheets (list): List of hierarchical blocks. + options (dict): Dict of options and values that enable/disable functions. + """ + + # Global dict of pull pins for all blocks as they each pull on each other the same way. + block_pull_pins = defaultdict(list) + + # Class for movable groups of parts/child nodes. + class PartBlock: + def __init__(self, src, bbox, anchor_pt, snap_pt, tag): + self.src = src # Source for this block. + self.place_bbox = bbox # FIXME: Is this needed if place_bbox includes room for routing? + + # Create anchor pin to which forces are applied to this block. + anchor_pin = Pin() + anchor_pin.part = self + anchor_pin.place_pt = anchor_pt + + # This block has only a single anchor pin, but it needs to be in a list + # in a dict so it can be processed by the part placement functions. + self.anchor_pins = dict() + self.anchor_pins["similarity"] = [anchor_pin] + + # Anchor pin for this block is also a pulling pin for all other blocks. + block_pull_pins["similarity"].append(anchor_pin) + + # All blocks have the same set of pulling pins because they all pull each other. + self.pull_pins = block_pull_pins + + self.snap_pt = snap_pt # For snapping to grid. + self.tx = Tx() # For placement. + self.ref = "REF" # Name for block in debug drawing. + self.tag = tag # FIXME: what is this for? + + # Create a list of blocks from the groups of interconnected parts and the group of floating parts. + part_blocks = [] + for part_list in connected_parts + [floating_parts]: + if not part_list: + # No parts in this list for some reason... + continue + + # Find snapping point and bounding box for this group of parts. + snap_pt = None + bbox = BBox() + for part in part_list: + bbox.add(part.lbl_bbox * part.tx) + if not snap_pt: + # Use the first snapping point of a part you can find. + snap_pt = get_snap_pt(part) + + # Tag indicates the type of part block. + tag = 2 if (part_list is floating_parts) else 1 + + # pad the bounding box so part blocks don't butt-up against each other. + pad = BLK_EXT_PAD + bbox = bbox.resize(Vector(pad, pad)) + + # Create the part block and place it on the list. + part_blocks.append(PartBlock(part_list, bbox, bbox.ctr, snap_pt, tag)) + + # Add part blocks for child nodes. + for child in children: + # Calculate bounding box of child node. + bbox = child.calc_bbox() + + # Set padding for separating bounding box from others. + if child.flattened: + # This is a flattened node so the parts will be shown. + # Set the padding to include a pad between the parts and the + # graphical box that contains them, plus the padding around + # the outside of the graphical box. + pad = BLK_INT_PAD + BLK_EXT_PAD + else: + # This is an unflattened child node showing no parts on the inside + # so just pad around the outside of its graphical box. + pad = BLK_EXT_PAD + bbox = bbox.resize(Vector(pad, pad)) + + # Set the grid snapping point and tag for this child node. + snap_pt = child.get_snap_pt() + tag = 3 # Standard child node. + if not snap_pt: + # No snap point found, so just use the center of the bounding box. + snap_pt = bbox.ctr + tag = 4 # A child node with no snapping point. + + # Create the child block and place it on the list. + part_blocks.append(PartBlock(child, bbox, bbox.ctr, snap_pt, tag)) + + # Get ordered list of all block tags. Use this list to tell if tags are + # adjacent since there may be missing tags if a particular type of block + # isn't present. + tags = sorted({blk.tag for blk in part_blocks}) + + # Tie the blocks together with strong links between blocks with the same tag, + # and weaker links between blocks with adjacent tags. This ties similar + # blocks together into "super blocks" and ties the super blocks into a linear + # arrangement (1 -> 2 -> 3 ->...). + blk_attr = defaultdict(lambda: defaultdict(lambda: 0)) + for blk in part_blocks: + for other_blk in part_blocks: + if blk is other_blk: + # No attraction between a block and itself. + continue + if blk.tag == other_blk.tag: + # Large attraction between blocks of same type. + blk_attr[blk][other_blk] = 1 + elif abs(tags.index(blk.tag) - tags.index(other_blk.tag)) == 1: + # Some attraction between blocks of adjacent types. + blk_attr[blk][other_blk] = 0.1 + else: + # Otherwise, no attraction between these blocks. + blk_attr[blk][other_blk] = 0 + + if not part_blocks: + # Abort if nothing to place. + return + + # Start off with a random placement of part blocks. + random_placement(part_blocks) + + if options.get("draw_placement"): + # Setup to draw the part block placement for debug purposes. + bbox = get_enclosing_bbox(part_blocks) + draw_scr, draw_tx, draw_font = draw_start(bbox) + options.update( + {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} + ) + + # Arrange the part blocks with similarity force-directed placement. + force_func = functools.partial(total_similarity_force, similarity=blk_attr) + evolve_placement([], part_blocks, [], force_func, **options) + + if options.get("draw_placement"): + # Pause to look at placement for debugging purposes. + draw_pause() + + # Apply the placement moves of the part blocks to their underlying sources. + for blk in part_blocks: + try: + # Update the Tx matrix of the source (usually a child node). + blk.src.tx = blk.tx + except AttributeError: + # The source doesn't have a Tx so it must be a collection of parts. + # Apply the block placement to the Tx of each part. + for part in blk.src: + part.tx *= blk.tx + + def get_attrs(node): + """Return dict of attribute sets for the parts, pins, and nets in a node.""" + attrs = {"parts": set(), "pins": set(), "nets": set()} + for part in node.parts: + attrs["parts"].update(set(dir(part))) + for pin in part.pins: + attrs["pins"].update(set(dir(pin))) + for net in node.get_internal_nets(): + attrs["nets"].update(set(dir(net))) + return attrs + + def show_added_attrs(node): + """Show attributes that were added to parts, pins, and nets in a node.""" + current_attrs = node.get_attrs() + for key in current_attrs.keys(): + print( + "added {} attrs: {}".format(key, current_attrs[key] - node.attrs[key]) + ) + + def rmv_placement_stuff(node): + """Remove attributes added to parts, pins, and nets of a node during the placement phase.""" + + for part in node.parts: + rmv_attr(part.pins, ("route_pt", "place_pt")) + rmv_attr( + node.parts, + ("anchor_pins", "pull_pins", "pin_ctrs", "force", "mv"), + ) + rmv_attr(node.get_internal_nets(), ("parts",)) + + def place(node, tool=None, **options): + """Place the parts and children in this node. + + Args: + node (Node): Hierarchical node containing the parts and children to be placed. + tool (str): Backend tool for schematics. + options (dict): Dictionary of options and values to control placement. + """ + + # Inject the constants for the backend tool into this module. + import skidl + from skidl.tools import tool_modules + + tool = tool or skidl.config.tool + this_module = sys.modules[__name__] + this_module.__dict__.update(tool_modules[tool].constants.__dict__) + + random.seed(options.get("seed")) + + # Store the starting attributes of the node's parts, pins, and nets. + node.attrs = node.get_attrs() + + try: + # First, recursively place children of this node. + # TODO: Child nodes are independent, so can they be processed in parallel? + for child in node.children.values(): + child.place(tool=tool, **options) + + # Group parts into those that are connected by explicit nets and + # those that float freely connected only by stub nets. + connected_parts, internal_nets, floating_parts = node.group_parts(**options) + + # Place each group of connected parts. + for group in connected_parts: + node.place_connected_parts(list(group), internal_nets, **options) + + # Place the floating parts that have no connections to anything else. + node.place_floating_parts(list(floating_parts), **options) + + # Now arrange all the blocks of placed parts and the child nodes within this node. + node.place_blocks( + connected_parts, floating_parts, node.children.values(), **options + ) + + # Remove any stuff leftover from this place & route run. + # print(f"added part attrs = {new_part_attrs}") + node.rmv_placement_stuff() + # node.show_added_attrs() + + # Calculate the bounding box for the node after placement of parts and children. + node.calc_bbox() + + except PlacementFailure: + node.rmv_placement_stuff() + raise PlacementFailure + + def get_snap_pt(node): + """Get a Point to use for snapping the node to the grid. + + Args: + node (Node): The Node to which the snapping point applies. + + Returns: + Point: The snapping point or None. + """ + + if node.flattened: + # Look for a snapping point based on one of its parts. + for part in node.parts: + snap_pt = get_snap_pt(part) + if snap_pt: + return snap_pt + + # If no part snapping point, look for one in its children. + for child in node.children.values(): + if child.flattened: + snap_pt = child.get_snap_pt() + if snap_pt: + # Apply the child transformation to its snapping point. + return snap_pt * child.tx + + # No snapping point if node is not flattened or no parts in it or its children. + return None diff --git a/src/faebryk/exporters/schematic/kicad/route.py b/src/faebryk/exporters/schematic/kicad/route.py new file mode 100644 index 00000000..b5d449f8 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/route.py @@ -0,0 +1,3413 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Autorouter for generating wiring between symbols in a schematic. +""" + +import copy +import random +import sys +from collections import Counter, defaultdict +from enum import Enum +from itertools import chain, zip_longest + +from skidl import Part +from skidl.utilities import export_to_all, rmv_attr +from .geometry import BBox, Point, Segment, Tx, Vector, tx_rot_90 +from faebryk.exporters.visualize.util import generate_pastel_palette + + +__all__ = ["RoutingFailure", "GlobalRoutingFailure", "SwitchboxRoutingFailure"] + + +################################################################### +# +# OVERVIEW OF SCHEMATIC AUTOROUTER +# +# The input is a Node containing child nodes and parts, each with a +# bounding box and an assigned (x,y) position. The following operations +# are done for each child node, and then for the parts within this node. +# +# The edges of each part bbox are extended to form tracks that divide the +# routing area into a set of four-sided, non-overlapping switchboxes. Each +# side of a switchbox is a Face, and each Face is a member of two adjoining +# switchboxes (except those Faces on the boundary of the total +# routing area.) Each face is adjacent to the six other faces of +# the two switchboxes it is part of. +# +# Each face has a capacity that indicates the number of wires that can +# cross through it. The capacity is the length of the face divided by the +# routing grid. (Faces on a part boundary have zero capacity to prevent +# routing from entering a part.) +# +# Each face on a part bbox is assigned terminals associated with the I/O +# pins of that symbol. +# +# After creating the faces and terminals, the global routing phase creates +# wires that connect the part pins on the nets. Each wire passes from +# a face of a switchbox to one of the other three faces, either directly +# across the switchbox to the opposite face or changing direction to +# either of the right-angle faces. The global router is basically a maze +# router that uses the switchboxes as high-level grid squares. +# +# After global routing, each net has a sequence of switchbox faces +# through which it will transit. The exact coordinate that each net +# enters a face is then assigned to create a Terminal. +# +# At this point there are a set of switchboxes which have fixed terminals located +# along their four faces. A greedy switchbox router +# (https://doi.org/10.1016/0167-9260(85)90029-X) +# does the detailed routing within each switchbox. +# +# The detailed wiring within all the switchboxes is combined and output +# as the total wiring for the parts in the Node. +# +################################################################### + + +try: + from .debug_draw import draw_end, draw_endpoint, draw_routing, draw_seg, draw_start +except ImportError: + + def _raise_on_call(*args, **kwargs): + raise RuntimeError("Function not available.") + + draw_end = _raise_on_call + draw_endpoint = _raise_on_call + draw_routing = _raise_on_call + draw_seg = _raise_on_call + draw_start = _raise_on_call + + +# Orientations and directions. +class Orientation(Enum): + HORZ = 1 + VERT = 2 + + +class Direction(Enum): + LEFT = 3 + RIGHT = 4 + + +# Dictionary for storing colors to visually distinguish routed nets. +# TODO: replace with generate_pastel_palette +net_colors = defaultdict( + lambda: (random.randint(0, 200), random.randint(0, 200), random.randint(0, 200)) +) + + +class NoSwitchBox(Exception): + """Exception raised when a switchbox cannot be generated.""" + + pass + + +class TerminalClashException(Exception): + """Exception raised when trying to place two terminals at the same coord on a Face.""" + + pass + + +class RoutingFailure(Exception): + """Exception raised when a net connecting pins cannot be routed.""" + + pass + + +class GlobalRoutingFailure(RoutingFailure): + """Failure during global routing phase.""" + + pass + + +class SwitchboxRoutingFailure(RoutingFailure): + """Failure during switchbox routing phase.""" + + pass + + +class Boundary: + """Class for indicating a boundary. + + When a Boundary object is placed in the part attribute of a Face, it + indicates the Face is on the outer boundary of the Node routing area + and no routes can pass through it. + """ + + pass + + +# Boundary object for placing in the bounding Faces of the Node routing area. +boundary = Boundary() + +# Absolute coords of all part pins. Used when trimming stub nets. +pin_pts = [] + + +class Terminal: + def __init__(self, net, face, coord): + """Terminal on a Face from which a net is routed within a SwitchBox. + + Args: + net (Net): Net upon which the Terminal resides. + face (Face): SwitchBox Face upon which the Terminal resides. + coord (int): Absolute position along the track the face is in. + + Notes: + A terminal exists on a Face and is assigned to a net. + The terminal's (x,y) position is determined by the terminal's + absolute coordinate along the track parallel to the face, + and by the Face's absolute coordinate in the orthogonal direction. + """ + + self.net = net + self.face = face + self.coord = coord + + @property + def route_pt(self): + """Return (x,y) Point for a Terminal on a Face.""" + track = self.face.track + if track.orientation == HORZ: + return Point(self.coord, track.coord) + else: + return Point(track.coord, self.coord) + + def get_next_terminal(self, next_face): + """Get the terminal on the next face that lies on the same net as this terminal. + + This method assumes the terminal's face and the next face are faces of the + same switchbox. Hence, they're either parallel and on opposite sides, or they're + at right angles so they meet at a corner. + + Args: + next_face (Face): Face to search for a terminal on the same net as this. + + Raises: + RoutingFailure: If no terminal exists. + + Returns: + Terminal: The terminal found on the next face. + """ + + from_face = self.face + if next_face.track in (from_face.beg, from_face.end): + # The next face bounds the interval of the terminals's face, so + # they're at right angles. With right angle faces, we want to + # select a terminal on the next face that's close to this corner + # because that will minimize the length of wire needed to make + # the connection. + if next_face.beg == from_face.track: + # next_face is oriented upward or rightward w.r.t. from_face. + # Start searching for a terminal from the lowest index + # because this is closest to the corner. + search_terminals = next_face.terminals + elif next_face.end == from_face.track: + # next_face is oriented downward or leftward w.r.t. from_face. + # Start searching for a terminal from the highest index + # because this is closest to the corner. + search_terminals = next_face.terminals[::-1] + else: + raise GlobalRoutingFailure + else: + # The next face must be the parallel face on the other side of the + # switchbox. With parallel faces, we want to selected a terminal + # having close to the same position as the given terminal. + # So if the given terminal is at position i, then search for the + # next terminal on the other face at positions i, i+1, i-1, i+2, i-2... + coord = self.coord + lower_terminals = [t for t in next_face.terminals if t.coord <= coord] + lower_terminals.sort(key=lambda t: t.coord, reverse=True) + upper_terminals = [t for t in next_face.terminals if t.coord > coord] + upper_terminals.sort(key=lambda t: t.coord, reverse=False) + search_terminals = list( + chain(*zip_longest(lower_terminals, upper_terminals)) + ) + search_terminals = [t for t in search_terminals if t is not None] + + # Search to find a terminal on the same net. + for terminal in search_terminals: + if terminal.net is self.net: + return terminal # Return found terminal. + + # No terminal on the same net, so search to find an unassigned terminal. + for terminal in search_terminals: + if terminal.net is None: + terminal.net = self.net # Assign net to terminal. + return terminal # Return newly-assigned terminal. + + # Well, something went wrong. Should have found *something*! + raise GlobalRoutingFailure + + def draw(self, scr, tx, **options): + """Draw a Terminal for debugging purposes. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + options (dict, optional): Dictionary of options and values. Defaults to {}. + """ + + # Don't draw terminal if it isn't on a net. It's just a placeholder. + if self.net or options.get("draw_all_terminals"): + draw_endpoint(self.route_pt, scr, tx, color=(255, 0, 0)) + # draw_endpoint(self.route_pt, scr, tx, color=net_colors[self.net]) + + +class Interval(object): + def __init__(self, beg, end): + """Define an interval with a beginning and an end. + + Args: + beg (GlobalTrack): Beginning orthogonal track that bounds interval. + end (GlobalTrack): Ending orthogonal track that bounds interval. + + Note: The beginning and ending Tracks are orthogonal to the Track containing + the interval. + Also, beg and end are sorted so beg <= end. + """ + + # Order beginning and end so beginning <= end. + if beg > end: + beg, end = end, beg + self.beg = beg + self.end = end + + def __bool__(self): + """An Interval object always returns True.""" + return True + + @property + def len(self): + """Return the length of the interval.""" + return self.end - self.beg + + def __len__(self): + """Return the length of the interval.""" + return self.len + + def intersects(self, other): + """Return True if the intervals overlap (even if only at one point).""" + return not ((self.beg > other.end) or (self.end < other.beg)) + + def interval_intersection(self, other): + """Return intersection of two intervals as an interval, otherwise None.""" + if self.intersects(other): + beg = max(self.beg, other.beg) + end = min(self.end, other.end) + assert beg <= end + if beg != end: + return Interval(beg, end) + return None + + def merge(self, other): + """ + Return a merged interval if the given intervals intersect, otherwise return + None. + """ + if Interval.intersects(self, other): + return Interval(min(self.beg, other.beg), max(self.end, other.end)) + return None + + +class NetInterval(Interval): + def __init__(self, net, beg, end): + """Define an Interval with an associated net (useful for wire traces in a + switchbox). + + Args: + net (Net): Net associated with interval. + beg (GlobalTrack): Beginning orthogonal track that bounds interval. + end (GlobalTrack): Ending track that bounds interval. + """ + super().__init__(beg, end) + self.net = net + + def obstructs(self, other): + """Return True if the intervals intersect and have different nets.""" + return super().intersects(other) and (self.net is not other.net) + + def merge(self, other): + """Return a merged interval if the given intervals intersect and are on the + same net, otherwise return None.""" + if self.net is other.net: + merged_intvl = super().merge(other) + if merged_intvl: + merged_intvl = NetInterval(self.net, merged_intvl.beg, merged_intvl.end) + return merged_intvl + return None + + +class Adjacency: + def __init__(self, from_face, to_face): + """Define an adjacency between two Faces. + + Args: + from_face (Face): One Face. + to_face (Face): The other Face. + + Note: The Adjacency object will be associated with the from_face object, so there's + no need to store from_face in the Adjacency object. + """ + + self.face = to_face + if from_face.track.orientation == to_face.track.orientation: + # Parallel faces, either both vertical or horizontal. + # Distance straight-across from one face to the other. + dist_a = abs(from_face.track.coord - to_face.track.coord) + # Average distance parallel to the faces. + dist_b = (from_face.length + to_face.length) / 2 + # Compute the average distance from a terminal on one face to the other. + self.dist = dist_a + dist_b / 2 + else: + # Else, orthogonal faces. + # Compute the average face-to-face distance. + dist_a = from_face.length + dist_b = to_face.length + # Average distance of dogleg route from a terminal on one face to the other. + self.dist = (dist_a + dist_b) / 2 + + +class Face(Interval): + """A side of a rectangle bounding a routing switchbox.""" + + def __init__(self, part, track, beg, end): + """One side of a routing switchbox. + + Args: + part (set,Part,Boundary): Element(s) the Face is part of. + track (GlobalTrack): Horz/vert track the Face is on. + beg (GlobalTrack): Vert/horz track the Face begins at. + end (GlobalTrack): Vert/horz track the Face ends at. + + Notes: + The beg and end tracks have to be in the same direction + (i.e., both vertical or both horizontal) and orthogonal + to the track containing the face. + """ + + # Initialize the interval beginning and ending defining the Face. + super().__init__(beg, end) + + # Store Part/Boundary the Face is part of, if any. + self.part = set() + if isinstance(part, set): + self.part.update(part) + elif part is not None: + self.part.add(part) + + # Storage for any part pins that lie along this Face. + self.pins = [] + + # Storage for routing terminals along this face. + self.terminals = [] + + # Set of Faces adjacent to this one. (Starts empty.) + self.adjacent = set() + + # Add this new face to the track it belongs to so it isn't lost. + self.track = track + track.add_face(self) + + # Storage for switchboxes this face is part of. + self.switchboxes = set() + + def combine(self, other): + """Combine information from other face into this one. + + Args: + other (Face): Other Face. + + Returns: + None. + """ + + self.pins.extend(other.pins) + self.terminals.extend(other.terminals) + self.part.update(other.part) + self.adjacent.update(other.adjacent) + self.switchboxes.update(other.switchboxes) + + @property + def length(self): + """Return the length of the face.""" + return self.end.coord - self.beg.coord + + @property + def bbox(self): + """Return the bounding box of the 1-D face segment.""" + bbox = BBox() + + if self.track.orientation == VERT: + # Face runs vertically, so bbox width is zero. + bbox.add(Point(self.track.coord, self.beg.coord)) + bbox.add(Point(self.track.coord, self.end.coord)) + else: + # Face runs horizontally, so bbox height is zero. + bbox.add(Point(self.beg.coord, self.track.coord)) + bbox.add(Point(self.end.coord, self.track.coord)) + + return bbox + + def add_terminal(self, net, coord): + """Create a Terminal on the Face. + + Args: + net (Net): The net the terminal is on. + coord (int): The absolute coordinate along the track containing the Face. + + Raises: + TerminalClashException: + """ + + if self.part and not net: + # Don't add pin terminals with no net to a Face on a part or boundary. + return + + # Search for pre-existing terminal at the same coordinate. + for terminal in self.terminals: + if terminal.coord == coord: + # There is a pre-existing terminal at this coord. + if not net: + # The new terminal has no net (i.e., non-pin terminal), + # so just quit and don't bother to add it. The pre-existing + # terminal is retained. + return + elif terminal.net and terminal.net is not net: + # The pre-existing and new terminals have differing nets, so + # raise an exception. + raise TerminalClashException + # The pre-existing and new terminals have the same net. + # Remove the pre-existing terminal. It will be replaced + # with the new terminal below. + self.terminals.remove(terminal) + + # Create a new Terminal and add it to the list of terminals for this face. + self.terminals.append(Terminal(net, self, coord)) + + def trim_repeated_terminals(self): + """Remove all but one terminal of each individual net from the face. + + Notes: + A non-part Face with multiple terminals on the same net will lead + to multi-path routing. + """ + + # Find the intersection of every non-part face in the track with this one. + intersections = [] + for face in self.track: + if not face.part: + intersection = self.interval_intersection(face) + if intersection: + intersections.append(intersection) + + # Merge any overlapping intersections to create larger ones. + for i in range(len(intersections)): + for j in range(i + 1, len(intersections)): + merge = intersections[i].merge(intersections[j]) + if merge: + intersections[j] = merge + intersections[i] = None + break + + # Remove None from the list of intersections. + intersections = list(set(intersections) - {None}) + + # The intersections are now as large as they can be and not associated + # with any parts, so there are no terminals associated with part pins. + # Look for terminals within an intersection on the same net and + # remove all but one of them. + for intersection in intersections: + # Make a dict with nets and the terminals on each one. + net_term_dict = defaultdict(list) + for terminal in self.terminals: + if intersection.beg.coord <= terminal.coord <= intersection.end.coord: + net_term_dict[terminal.net].append(terminal) + if None in net_term_dict.keys(): + del net_term_dict[None] # Get rid of terminals not assigned to nets. + + # For each multi-terminal net, remove all but one terminal. + # This terminal must be removed from all faces on the track. + for terminals in net_term_dict.values(): + for terminal in terminals[1:]: # Keep only the 1st terminal. + self.track.remove_terminal(terminal) + + def create_nonpin_terminals(self): + """Create unassigned terminals along a non-part Face with GRID spacing. + + These terminals will be used during global routing of nets from + face-to-face and during switchbox routing. + """ + + # Add terminals along a Face. A terminal can be right at the start if the Face + # starts on a grid point, but there cannot be a terminal at the end + # if the Face ends on a grid point. Otherwise, there would be two terminals + # at exactly the same point (one at the ending point of a Face and the + # other at the beginning point of the next Face). + # FIXME: This seems to cause wiring with a lot of doglegs. + if self.end.coord - self.beg.coord <= GRID: + # Allow a terminal right at the start of the Face if the Face is small. + beg = (self.beg.coord + GRID - 1) // GRID * GRID + else: + # For larger faces with lengths greater than the GRID spacing, + # don't allow terminals right at the start of the Face. + beg = (self.beg.coord + GRID) // GRID * GRID + end = self.end.coord + + # Create terminals along the Face. + for coord in range(beg, end, GRID): + self.add_terminal(None, coord) + + def set_capacity(self): + """Set the wire routing capacity of a Face.""" + + if self.part: + # Part/boundary faces have zero capacity for wires to pass thru. + self.capacity = 0 + else: + # Wire routing capacity for other faces is the number of terminals they + # have. + self.capacity = len(self.terminals) + + def has_nets(self): + """Return True if any Terminal on the Face is attached to a net.""" + return any((terminal.net for terminal in self.terminals)) + + def add_adjacencies(self): + """Add adjacent faces of the switchbox having this face as the top face.""" + + # Create a temporary switchbox. + try: + swbx = SwitchBox(self) + except NoSwitchBox: + # This face doesn't belong to a valid switchbox. + return + + def add_adjacency(from_, to): + # Faces on the boundary can never accept wires so they are never + # adjacent to any other face. + if boundary in from_.part or boundary in to.part: + return + + # If a face is an edge of a part, then it can never be adjacent to + # another face on the *same part* or else wires might get routed over + # the part bounding box. + if from_.part.intersection(to.part): + return + + # OK, no parts in common between the two faces so they can be adjacent. + from_.adjacent.add(Adjacency(from_, to)) + to.adjacent.add(Adjacency(to, from_)) + + # Add adjacent faces. + add_adjacency(swbx.top_face, swbx.bottom_face) + add_adjacency(swbx.left_face, swbx.right_face) + add_adjacency(swbx.left_face, swbx.top_face) + add_adjacency(swbx.left_face, swbx.bottom_face) + add_adjacency(swbx.right_face, swbx.top_face) + add_adjacency(swbx.right_face, swbx.bottom_face) + + # Get rid of the temporary switchbox. + del swbx + + def extend(self, orthogonal_tracks): + """Extend a Face along its track until it is blocked by an orthogonal face. + + This is used to create Faces that form the irregular grid of switchboxes. + + Args: + orthogonal_tracks (list): List of tracks at right-angle to this face. + """ + + # Only extend faces that compose part bounding boxes. + if not self.part: + return + + # Extend the face backward from its beginning and forward from its end. + for start, dir in ((self.beg, -1), (self.end, 1)): + # Get tracks to extend face towards. + search_tracks = orthogonal_tracks[start.idx :: dir] + + # The face extension starts off non-blocked by any orthogonal faces. + blocked = False + + # Search for a orthogonal face in a track that intersects this extension. + for ortho_track in search_tracks: + for ortho_face in ortho_track: + # Intersection only occurs if the extending face hits the open + # interval of the orthogonal face, not if it touches an endpoint. + if ortho_face.beg < self.track < ortho_face.end: + # OK, this face intersects the extension. It also means the + # extending face will block the face just found, so split + # each track at the intersection point. + ortho_track.add_split(self.track) + self.track.add_split(ortho_track) + + # If the intersecting face is also a face of a part bbox, + # then the extension is blocked, so create the extended face + # and stop the extension. + if ortho_face.part: + # This creates a face and adds it to the track. + Face(None, self.track, start, ortho_track) + blocked = True + + # Stop checking faces in this track after an intersection is + # found. + break + + # Stop checking any further tracks once the face extension is blocked. + if blocked: + break + + def split(self, trk): + """ + If a track intersects in the middle of a face, split the face into two faces. + """ + + if self.beg < trk < self.end: + # Add a Face from beg to trk to self.track. + Face(self.part, self.track, self.beg, trk) + # Move the beginning of the original Face to trk. + self.beg = trk + + def coincides_with(self, other_face): + """Return True if both faces have the same beginning and ending point on the + same track.""" + return (self.beg, self.end) == (other_face.beg, other_face.end) + + def has_overlap(self, other_face): + """Return True if the two faces overlap.""" + return self.beg < other_face.end and self.end > other_face.beg + + def audit(self): + """Raise exception if face is malformed.""" + assert len(self.switchboxes) <= 2 + + @property + def seg(self): + """Return a Segment that coincides with the Face.""" + + if self.track.orientation == VERT: + p1 = Point(self.track.coord, self.beg.coord) + p2 = Point(self.track.coord, self.end.coord) + else: + p1 = Point(self.beg.coord, self.track.coord) + p2 = Point(self.end.coord, self.track.coord) + + return Segment(p1, p2) + + def draw( + self, scr, tx, font, color=(128, 128, 128), thickness=2, dot_radius=0, **options + ): + """Draw a Face in the drawing area. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + options (dict, optional): Dictionary of options and values. + + Returns: + None. + """ + + # Draw a line segment for the Face. + draw_seg( + self.seg, scr, tx, color=color, thickness=thickness, dot_radius=dot_radius + ) + + # Draw the terminals on the Face. + for terminal in self.terminals: + terminal.draw(scr, tx, **options) + + if options.get("show_capacities"): + # Show the wiring capacity at the midpoint of the Face. + mid_pt = (self.seg.p1 + self.seg.p2) / 2 + draw_text(str(self.capacity), mid_pt, scr, tx, font=font, color=color) + + +class GlobalWire(list): + def __init__(self, net, *args, **kwargs): + """A list connecting switchbox faces and terminals. + + Global routes start off as a sequence of switchbox faces that the route + goes thru. Later, these faces are converted to terminals at fixed positions + on their respective faces. + + Args: + net (Net): The net associated with the wire. + *args: Positional args passed to list superclass __init__(). + **kwargs: Keyword args passed to list superclass __init__(). + """ + self.net = net + super().__init__(*args, **kwargs) + + def cvt_faces_to_terminals(self): + """Convert global face-to-face route to switchbox terminal-to-terminal route.""" + + if not self: + # Global route is empty so do nothing. + return + + # Non-empty global routes should always start from a face on a part. + assert self[0].part + + # All part faces already have terminals created from the part pins. Find all + # the route faces on part boundaries and convert them to pin terminals if + # one or more pins are attached to the same net as the route. + for i, face in enumerate(self[:]): + if face.part: + # This route face is on a part boundary, so find the terminal with the + # route's net. + for terminal in face.terminals: + if self.net is terminal.net: + # Replace the route face with the terminal on the part. + self[i] = terminal + break + else: + # Route should never touch a part face if there is no terminal with + # the route's net. + raise RuntimeError + + # Proceed through all the Faces/Terminals on the GlobalWire, converting + # all the Faces to Terminals. + for i in range(len(self) - 1): + # The current element on a GlobalWire should always be a Terminal. Use that + # terminal + # to convert the next Face on the wire to a Terminal (if it isn't one + # already). + if isinstance(self[i], Face): + # Logic error if the current element has not been converted to a + # Terminal. + raise RuntimeError + + if isinstance(self[i + 1], Face): + # Convert the next Face element into a Terminal on this net. This + # terminal will + # be the current element on the next iteration. + self[i + 1] = self[i].get_next_terminal(self[i + 1]) + + def draw(self, scr, tx, color=(0, 0, 0), thickness=1, dot_radius=10, **options): + """Draw a global wire from Face-to-Face in the drawing area. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + color (list): Three-element list of RGB integers with range [0, 255]. + thickness (int): Thickness of drawn wire in pixels. + dot_radius (int): Radius of drawn terminal in pixels. + options (dict, optional): Dictionary of options and values. Defaults to {}. + + Returns: + None. + """ + + # Draw pins on the net associated with the wire. + for pin in self.net.pins: + # Only draw pins in the current node being routed which have the route_pt + # attribute. + if hasattr(pin, "route_pt"): + pt = pin.route_pt * pin.part.tx + track = pin.face.track + pt = { + HORZ: Point(pt.x, track.coord), + VERT: Point(track.coord, pt.y), + }[track.orientation] + draw_endpoint(pt, scr, tx, color=color, dot_radius=10) + + # Draw global wire segment. + face_to_face = zip(self[:-1], self[1:]) + for terminal1, terminal2 in face_to_face: + p1 = terminal1.route_pt + p2 = terminal2.route_pt + draw_seg( + Segment(p1, p2), scr, tx, color=color, thickness=thickness, dot_radius=0 + ) + + +class GlobalRoute(list): + def __init__(self, *args, **kwargs): + """A list containing GlobalWires that form an entire routing of a net. + + Args: + net (Net): The net associated with the wire. + *args: Positional args passed to list superclass __init__(). + **kwargs: Keyword args passed to list superclass __init__(). + """ + super().__init__(*args, **kwargs) + + def cvt_faces_to_terminals(self): + """Convert GlobalWires in route to switchbox terminal-to-terminal route.""" + for wire in self: + wire.cvt_faces_to_terminals() + + def draw( + self, scr, tx, font, color=(0, 0, 0), thickness=1, dot_radius=10, **options + ): + """Draw the GlobalWires of this route in the drawing area. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (list): Three-element list of RGB integers with range [0, 255]. + thickness (int): Thickness of drawn wire in pixels. + dot_radius (int): Radius of drawn terminal in pixels. + options (dict, optional): Dictionary of options and values. Defaults to {}. + + Returns: + None. + """ + + for wire in self: + wire.draw(scr, tx, color, thickness, dot_radius, **options) + + +class GlobalTrack(list): + def __init__(self, orientation=HORZ, coord=0, idx=None, *args, **kwargs): + """A horizontal/vertical track holding zero or more faces all having the + same Y/X coordinate. + + These global tracks are made by extending the edges of part bounding boxes to + form a non-regular grid of rectangular switchboxes. These tracks are *NOT* + the same + as the tracks used within a switchbox for the detailed routing phase. + + Args: + orientation (Orientation): Orientation of track (horizontal or vertical). + coord (int): Coordinate of track on axis orthogonal to track direction. + idx (int): Index of track into a list of X or Y coords. + *args: Positional args passed to list superclass __init__(). + **kwargs: Keyword args passed to list superclass __init__(). + """ + + self.orientation = orientation + self.coord = coord + self.idx = idx + super().__init__(*args, **kwargs) + + # This stores the orthogonal tracks that intersect this one. + self.splits = set() + + def __eq__(self, track): + """Used for ordering tracks.""" + return self.coord == track.coord + + def __ne__(self, track): + """Used for ordering tracks.""" + return self.coord != track.coord + + def __lt__(self, track): + """Used for ordering tracks.""" + return self.coord < track.coord + + def __le__(self, track): + """Used for ordering tracks.""" + return self.coord <= track.coord + + def __gt__(self, track): + """Used for ordering tracks.""" + return self.coord > track.coord + + def __ge__(self, track): + """Used for ordering tracks.""" + return self.coord >= track.coord + + def __sub__(self, other): + """Subtract coords of two tracks.""" + return self.coord - other.coord + + def extend_faces(self, orthogonal_tracks): + """Extend the faces in a track. + + This is part of forming the irregular grid of switchboxes. + + Args: + orthogonal_tracks (list): List of tracks orthogonal to this one + (L/R vs. H/V). + """ + + for face in self[:]: + face.extend(orthogonal_tracks) + + def __hash__(self): + """This method lets a track be inserted into a set of splits.""" + return self.idx + + def add_split(self, orthogonal_track): + """Store the orthogonal track that intersects this one.""" + self.splits.add(orthogonal_track) + + def add_face(self, face): + """Add a face to a track. + + Args: + face (Face): Face to be added to track. + """ + + self.append(face) + + # The orthogonal tracks that bound the added face will split this track. + self.add_split(face.beg) + self.add_split(face.end) + + def split_faces(self): + """Split track faces by any intersecting orthogonal tracks.""" + + for split in self.splits: + for face in self[:]: + # Apply the split track to the face. The face will only be split + # if the split track intersects it. Any split faces will be added + # to the track this face is on. + face.split(split) + + def remove_duplicate_faces(self): + """Remove faces from the track having the same endpoints.""" + + # Create lists of faces having the same endpoints. + dup_faces_dict = defaultdict(list) + for face in self: + key = (face.beg, face.end) + dup_faces_dict[key].append(face) + + # Remove all but the first face from each list. + for dup_faces in dup_faces_dict.values(): + retained_face = dup_faces[0] + for dup_face in dup_faces[1:]: + # Add info from duplicate face to the retained face. + retained_face.combine(dup_face) + self.remove(dup_face) + + def remove_terminal(self, terminal): + """Remove a terminal from any non-part Faces in the track.""" + + coord = terminal.coord + # Look for the terminal in all non-part faces on the track. + for face in self: + if not face.part: + for term in face.terminals[:]: + if term.coord == coord: + face.terminals.remove(term) + + def add_adjacencies(self): + """Add adjacent switchbox faces to each face in a track.""" + + for top_face in self: + top_face.add_adjacencies() + + def audit(self): + """Raise exception if track is malformed.""" + + for i, first_face in enumerate(self): + first_face.audit() + for second_face in self[i + 1 :]: + if first_face.has_overlap(second_face): + raise AssertionError + + def draw(self, scr, tx, font, **options): + """Draw the Faces in a track. + + Args: + scr (_type_): _description + scr (PyGame screen): Screen object forPyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + options (dict, optional): Dictionary of options and values. Defaults to {}. + """ + for face in self: + face.draw(scr, tx, font, **options) + + +class Target: + def __init__(self, net, row, col): + """A point on a switchbox face that switchbox router has not yet reached. + + Targets are used to direct the switchbox router towards terminals that + need to be connected to nets. So wiring will be nudged up/down to + get closer to terminals along the upper/lower faces. Wiring will also + be nudged toward the track rows where terminals on the right face reside + as the router works from the left to the right. + + Args: + net (Net): Target net. + row (int): Track row for the target, including top or bottom faces. + col (int): Switchbox column for the target. + """ + self.row = row + self.col = col + self.net = net + + def __lt__(self, other): + """Used for ordering Targets in terms of priority.""" + + # Targets in the left-most columns are given priority since they will be reached + # first as the switchbox router proceeds from left-to-right. + return (self.col, self.row, id(self.net)) < ( + other.col, + other.row, + id(other.net), + ) + + +class SwitchBox: + # Indices for faces of the switchbox. + TOP, LEFT, BOTTOM, RIGHT = 0, 1, 2, 3 + + def __init__(self, top_face, left_face=None, bottom_face=None, right_face=None): + """Routing switchbox. + + A switchbox is a rectangular region through which wires are routed. + It has top, bottom, left and right faces. + + Args: + top_face (Face): The top face of the switchbox (needed to find the other faces). + bottom_face (Face): The bottom face. Will be calculated if set to None. + left_face (Face): The left face. Will be calculated if set to None. + right_face (Face): The right face. Will be calculated if set to None. + + Raises: + NoSwitchBox: Exception raised if the switchbox is an + unroutable region inside a part bounding box. + """ + + # Find the left face in the left track that bounds the top face. + if left_face == None: + left_track = top_face.beg + for face in left_track: + # The left face will end at the track for the top face. + if face.end.coord == top_face.track.coord: + left_face = face + break + else: + raise NoSwitchBox("Unroutable switchbox (left)!") + + # Find the right face in the right track that bounds the top face. + if right_face == None: + right_track = top_face.end + for face in right_track: + # The right face will end at the track for the top face. + if face.end.coord == top_face.track.coord: + right_face = face + break + else: + raise NoSwitchBox("Unroutable switchbox (right)!") + + # For a routable switchbox, the left and right faces should each + # begin at the same point. + if left_face.beg != right_face.beg: + # Inequality only happens when two parts are butted up against each other + # to form a non-routable switchbox inside a part bounding box. + raise NoSwitchBox("Unroutable switchbox (left-right)!") + + # Find the bottom face in the track where the left/right faces begin. + if bottom_face == None: + bottom_track = left_face.beg + for face in bottom_track: + # The bottom face should begin/end in the same places as the top face. + if (face.beg.coord, face.end.coord) == ( + top_face.beg.coord, + top_face.end.coord, + ): + bottom_face = face + break + else: + raise NoSwitchBox("Unroutable switchbox (bottom)!") + + # If all four sides have a part in common, then the switchbox is inside + # a part bbox that wires cannot be routed through. + if top_face.part & bottom_face.part & left_face.part & right_face.part: + raise NoSwitchBox("Unroutable switchbox (part)!") + + # Store the faces. + self.top_face = top_face + self.bottom_face = bottom_face + self.left_face = left_face + self.right_face = right_face + + # Each face records which switchboxes it belongs to. + self.top_face.switchboxes.add(self) + self.bottom_face.switchboxes.add(self) + self.left_face.switchboxes.add(self) + self.right_face.switchboxes.add(self) + + def find_terminal_net(terminals, terminal_coords, coord): + """Return the net attached to a terminal at the given coordinate. + + Args: + terminals (list): List of Terminals to search. + terminal_coords (list): List of integer coordinates for Terminals. + coord (int): Terminal coordinate to search for. + + Returns: + Net/None: Net at given coordinate or None if no net exists. + """ + try: + return terminals[terminal_coords.index(coord)].net + except ValueError: + return None + + # Find the coordinates of all the horizontal routing tracks + left_coords = [terminal.coord for terminal in self.left_face.terminals] + right_coords = [terminal.coord for terminal in self.right_face.terminals] + tb_coords = [self.top_face.track.coord, self.bottom_face.track.coord] + # Remove duplicate coords. + self.track_coords = list(set(left_coords + right_coords + tb_coords)) + + if len(self.track_coords) == 2: + # This is a weird case. If the switchbox channel is too narrow to hold + # a routing track in the middle, then place two pseudo-tracks along the + # top and bottom faces to allow routing to proceed. The routed wires will + # end up in the top or bottom faces, but maybe that's OK. + # FIXME: Should this be extending with tb_coords? + # FIXME: Should we always extend with tb_coords? + self.track_coords.extend(self.track_coords) + + # Sort horiz. track coords from bottom to top. + self.track_coords = sorted(self.track_coords) + + # Create a list of nets for each of the left/right faces. + self.left_nets = [ + find_terminal_net(self.left_face.terminals, left_coords, coord) + for coord in self.track_coords + ] + self.right_nets = [ + find_terminal_net(self.right_face.terminals, right_coords, coord) + for coord in self.track_coords + ] + + # Find the coordinates of all the vertical columns and then create + # a list of nets for each of the top/bottom faces. + top_coords = [terminal.coord for terminal in self.top_face.terminals] + bottom_coords = [terminal.coord for terminal in self.bottom_face.terminals] + lr_coords = [self.left_face.track.coord, self.right_face.track.coord] + self.column_coords = sorted(set(top_coords + bottom_coords + lr_coords)) + self.top_nets = [ + find_terminal_net(self.top_face.terminals, top_coords, coord) + for coord in self.column_coords + ] + self.bottom_nets = [ + find_terminal_net(self.bottom_face.terminals, bottom_coords, coord) + for coord in self.column_coords + ] + + # Remove any nets that only have a single terminal in the switchbox. + all_nets = self.left_nets + self.right_nets + self.top_nets + self.bottom_nets + net_counts = Counter(all_nets) + single_terminal_nets = [net for net, count in net_counts.items() if count <= 1] + if single_terminal_nets: + for side_nets in ( + self.left_nets, + self.right_nets, + self.top_nets, + self.bottom_nets, + ): + for i, net in enumerate(side_nets): + if net in single_terminal_nets: + side_nets[i] = None + + # Handle special case when a terminal is right on the corner of the switchbox. + self.move_corner_nets() + + # Storage for detailed routing. + self.segments = defaultdict(list) + + def audit(self): + """Raise exception if switchbox is malformed.""" + + for face in self.face_list: + face.audit() + assert self.top_face.track.orientation == HORZ + assert self.bottom_face.track.orientation == HORZ + assert self.left_face.track.orientation == VERT + assert self.right_face.track.orientation == VERT + assert len(self.top_nets) == len(self.bottom_nets) + assert len(self.left_nets) == len(self.right_nets) + + @property + def face_list(self): + """Return list of switchbox faces in CCW order, starting from top face.""" + flst = [None] * 4 + flst[self.TOP] = self.top_face + flst[self.LEFT] = self.left_face + flst[self.BOTTOM] = self.bottom_face + flst[self.RIGHT] = self.right_face + return flst + + def move_corner_nets(self): + """ + Move any nets at the edges of the left/right faces + (i.e., the corners) to the edges of the top/bottom faces. + This will allow these nets to be routed within the switchbox columns + as the routing proceeds from left to right. + """ + + if self.left_nets[0]: + # Move bottommost net on left face to leftmost net on bottom face. + self.bottom_nets[0] = self.left_nets[0] + self.left_nets[0] = None + + if self.left_nets[-1]: + # Move topmost net on left face to leftmost net on top face. + self.top_nets[0] = self.left_nets[-1] + self.left_nets[-1] = None + + if self.right_nets[0]: + # Move bottommost net on right face to rightmost net on bottom face. + self.bottom_nets[-1] = self.right_nets[0] + self.right_nets[0] = None + + if self.right_nets[-1]: + # Move topmost net on right face to rightmost net on top face. + self.top_nets[-1] = self.right_nets[-1] + self.right_nets[-1] = None + + def flip_xy(self): + """ + Flip X-Y of switchbox to route from top-to-bottom instead of left-to-right. + """ + + # Flip coords of tracks and columns. + self.column_coords, self.track_coords = self.track_coords, self.column_coords + + # Flip top/right and bottom/left nets. + self.top_nets, self.right_nets = self.right_nets, self.top_nets + self.bottom_nets, self.left_nets = self.left_nets, self.bottom_nets + + # Flip top/right and bottom/left faces. + self.top_face, self.right_face = self.right_face, self.top_face + self.bottom_face, self.left_face = self.left_face, self.bottom_face + + # Move any corner nets from the new left/right faces to the new top/bottom + # faces. + self.move_corner_nets() + + # Flip X/Y coords of any routed segments. + for segments in self.segments.values(): + for seg in segments: + seg.flip_xy() + + def coalesce(self, switchboxes): + """Group switchboxes around a seed switchbox into a larger switchbox. + + Args: + switchboxes (list): List of seed switchboxes that have not yet been + coalesced into a larger switchbox. + + Returns: + A coalesced switchbox or None if the seed was no longer available for + coalescing. + """ + + # Abort if the switchbox is no longer a potential seed (it was already merged + # into a previous switchbox). + if self not in switchboxes: + return None + + # Remove the switchbox from the list of seeds. + switchboxes.remove(self) + + # List the switchboxes along the top, left, bottom and right borders of the + # coalesced switchbox. + box_lists = [[self], [self], [self], [self]] + + # Iteratively search to the top, left, bottom, and right for switchboxes to add. + active_directions = {self.TOP, self.LEFT, self.BOTTOM, self.RIGHT} + while active_directions: + # Grow in the shortest dimension so the coalesced switchbox + # stays "squarish". + bbox = BBox() + for box_list in box_lists: + bbox.add(box_list[0].bbox) + if bbox.w == bbox.h: + # Already square, so grow in any direction. + growth_directions = {self.TOP, self.LEFT, self.BOTTOM, self.RIGHT} + elif bbox.w < bbox.h: + # Taller than wide, so grow left or right. + growth_directions = {self.LEFT, self.RIGHT} + else: + # Wider than tall, so grow up or down. + growth_directions = {self.TOP, self.BOTTOM} + + # Only keep growth directions that are still active. + growth_directions = growth_directions & active_directions + + # If there is no active growth direction, then stop the growth iterations. + if not growth_directions: + break + + # Take a random choice of the active growth directions. + direction = random.choice(list(growth_directions)) + + # Check the switchboxes along the growth side to see if further expansion + # is possible. + box_list = box_lists[direction] + for box in box_list: + # Get the face of the box from which growth will occur. + box_face = box.face_list[direction] + if box_face.part: + # This box butts up against a part, so expansion in this direction + # is blocked. + active_directions.remove(direction) + break + # Get the box which will be added if expansion occurs. + # Every face borders two switchboxes, so the adjacent box is the other + # one. + adj_box = (box_face.switchboxes - {box}).pop() + if adj_box not in switchboxes: + # This box cannot be added, so expansion in this direction + # is blocked. + active_directions.remove(direction) + break + else: + # All the switchboxes along the growth side are available for expansion, + # so replace the current boxes in the growth side with these new ones. + for i, box in enumerate(box_list[:]): + # Get the adjacent box for the current box on the growth side. + box_face = box.face_list[direction] + adj_box = (box_face.switchboxes - {box}).pop() + # Replace the current box with the new box from the expansion. + box_list[i] = adj_box + # Remove the newly added box from the list of available boxes + # for growth. + switchboxes.remove(adj_box) + + # Add the first box on the growth side to the end of the list of + # boxes on the + # preceding direction: (top,left,bottom,right) if current + # direction is (left,bottom,right,top). + box_lists[direction - 1].append(box_list[0]) + + # Add the last box on the growth side to the start of the list of boxes on the + # next direction: (bottom,right,top,left) if current direction + # is (left,bottom,right,top). + box_lists[(direction + 1) % 4].insert(0, box_list[-1]) + + # Create new faces that bound the coalesced group of switchboxes. + total_faces = [None, None, None, None] + directions = (self.TOP, self.LEFT, self.BOTTOM, self.RIGHT) + for direction, box_list in zip(directions, box_lists): + # Create a face that spans all the faces of the boxes along one side. + face_list = [box.face_list[direction] for box in box_list] + beg = min([face.beg for face in face_list]) + end = max([face.end for face in face_list]) + total_face = Face(None, face_list[0].track, beg, end) + + # Add terminals from the box faces along one side. + total_face.create_nonpin_terminals() + for face in face_list: + for terminal in face.terminals: + if terminal.net: + total_face.add_terminal(terminal.net, terminal.coord) + + # Set the routing capacity of the new face. + total_face.set_capacity() + + # Store the new face for this side. + total_faces[direction] = total_face + + # Return the coalesced switchbox created from the new faces. + return SwitchBox(*total_faces) + + def trim_repeated_terminals(self): + """Trim terminals on each face.""" + for face in self.face_list: + face.trim_repeated_terminals() + + @property + def bbox(self): + """Return bounding box for a switchbox.""" + return BBox().add(self.top_face.bbox).add(self.left_face.bbox) + + def has_nets(self): + """Return True if switchbox has any terminals on any face with nets attached.""" + return ( + self.top_face.has_nets() + or self.bottom_face.has_nets() + or self.left_face.has_nets() + or self.right_face.has_nets() + ) + + def route(self, **options): + """Route wires between terminals on the switchbox faces. + + Args: + options (dict, optional): Dictionary of options and values. Defaults to {}. + + Raises: + RoutingFailure: Raised if routing could not be completed. + + Returns: + List of Segments: List of wiring segments for switchbox routes. + """ + + if not self.has_nets(): + # Return what should be an empty dict. + assert not self.segments.keys() + return self.segments + + def collect_targets(top_nets, bottom_nets, right_nets): + """Collect target nets along top, bottom, right faces of switchbox.""" + + min_row = 1 + max_row = len(right_nets) - 2 + max_col = len(top_nets) + targets = [] + + # Collect target nets along top and bottom faces of the switchbox. + for col, (t_net, b_net) in enumerate(zip(top_nets, bottom_nets)): + if t_net is not None: + targets.append(Target(t_net, max_row, col)) + if b_net is not None: + targets.append(Target(b_net, min_row, col)) + + # Collect target nets on the right face of the switchbox. + for row, r_net in enumerate(right_nets): + if r_net is not None: + targets.append(Target(r_net, row, max_col)) + + # Sort the targets by increasing column order so targets closer to + # the left-to-right routing have priority. + targets.sort() + + return targets + + def connect_top_btm(track_nets): + """Connect nets from top/bottom terminals in a column to nets in + horizontal tracks of the switchbox.""" + + def find_connection(net, tracks, direction): + """ + Searches for the closest track with the same net followed by the + closest empty track. The indices of these tracks are returned. + If the net cannot be connected to any track, return []. + If the net given to connect is None, then return a list of [None]. + + Args: + net (Net): Net to be connected. + tracks (list): Nets on tracks + direction (int): Search direction for connection (-1: down, +1:up). + + Returns: + list: Indices of tracks where the net can connect. + """ + + if net: + if direction < 0: + # Searching down so reverse tracks. + tracks = tracks[::-1] + + connections = [] + + try: + # Find closest track with the given net. + connections.append(tracks[1:-1].index(net) + 1) + except ValueError: + pass + + try: + # Find closest empty track. + connections.append(tracks[1:-1].index(None) + 1) + except ValueError: + pass + + if direction < 0: + # Reverse track indices if searching down. + l = len(tracks) + connections = [l - 1 - cnct for cnct in connections] + else: + # No net so return no connections. + connections = [None] + + return connections + + # Stores net intervals connecting top/bottom nets to horizontal tracks. + column_intvls = [] + + # Top/bottom nets for this switchbox column. Horizontal track nets are + # at indexes 1..-2. + b_net = track_nets[0] + t_net = track_nets[-1] + + if t_net and (t_net is b_net): + # If top & bottom nets are the same, just create a single net interval + # connecting them and that's it. + column_intvls.append(NetInterval(t_net, 0, len(track_nets) - 1)) + return column_intvls + + # Find which tracks the top/bottom nets can connect to. + t_cncts = find_connection(t_net, track_nets, -1) + b_cncts = find_connection(b_net, track_nets, 1) + + # Create all possible pairs of top/bottom connections. + tb_cncts = [(t, b) for t in t_cncts for b in b_cncts] + + if not tb_cncts: + # No possible connections for top and/or bottom. + if options.get("allow_routing_failure"): + return column_intvls # Return empty column. + else: + raise SwitchboxRoutingFailure + + # Test each possible pair of connections to find one that is free of + # interference. + for t_cnct, b_cnct in tb_cncts: + if t_cnct is None or b_cnct is None: + # No possible interference if at least one connection is None. + break + if t_cnct > b_cnct: + # Top & bottom connections don't interfere. + break + if t_cnct == b_cnct and t_net is b_net: + # Top & bottom connect to the same track but they're the same net + # so that's OK. + break + else: + if options.get("allow_routing_failure"): + return column_intvls + else: + raise SwitchboxRoutingFailure + + if t_cnct is not None: + # Connection from track to terminal on top of switchbox. + column_intvls.append(NetInterval(t_net, t_cnct, len(track_nets) - 1)) + if b_cnct is not None: + # Connection from terminal on bottom of switchbox to track. + column_intvls.append(NetInterval(b_net, 0, b_cnct)) + + # Return connection segments. + return column_intvls + + def prune_targets(targets, current_col): + """Remove targets in columns to the left of the current left-to-right routing column""" + return [target for target in targets if target.col > current_col] + + def insert_column_nets(track_nets, column_intvls): + """Return the active nets with the added nets of the column's vertical intervals.""" + + nets = track_nets[:] + for intvl in column_intvls: + nets[intvl.beg] = intvl.net + nets[intvl.end] = intvl.net + return nets + + def net_search(net, start, track_nets): + """Search for the closest points for the net before and after the start point.""" + + # illegal offset past the end of the list of track nets. + large_offset = 2 * len(track_nets) + + try: + # Find closest occurrence of net going up. + up = track_nets[start:].index(net) + except ValueError: + # Net not found, so use out-of-bounds index. + up = large_offset + + try: + # Find closest occurrence of net going down. + down = track_nets[start::-1].index(net) + except ValueError: + # Net not found, so use out-of-bounds index. + down = large_offset + + if up <= down: + return up + else: + return -down + + def insert_target_nets(track_nets, targets, right_nets): + """Return copy of active track nets with additional prioritized targets + from the top, bottom, right faces.""" + + # Allocate storage for potential target nets to be added to the list of + # active track nets. + placed_target_nets = [None] * len(track_nets) + + # Get a list of nets on the right face that are being actively routed + # right now + # so we can steer the routing as it proceeds rightward. + active_right_nets = [ + net if net in track_nets else None for net in right_nets + ] + + # Strip-off the top/bottom rows where terminals are and routing doesn't go. + search_nets = track_nets[1:-1] + + for target in targets: + target_net, target_row = target.net, target.row + + # Skip target nets that aren't currently active or have already been + # placed (prevents multiple insertions of the same target net). + # Also ignore targets on the far right face until the last step. + if ( + target_net not in track_nets # TODO: Use search_nets??? + or target_net in placed_target_nets + or target_net in active_right_nets + ): + continue + + # Assign the target net to the closest row to the target row that is + # either + # empty or has the same net. + net_row_offset = net_search(target_net, target_row, search_nets) + empty_row_offset = net_search(None, target_row, search_nets) + if abs(net_row_offset) <= abs(empty_row_offset): + row_offset = net_row_offset + else: + row_offset = empty_row_offset + try: + placed_target_nets[target_row + row_offset + 1] = target_net + search_nets[target_row + row_offset] = target_net + except IndexError: + # There was no place for this target net + pass + + return [ + active_net or target_net or right_net + # active_net or right_net or target_net + for (active_net, right_net, target_net) in zip( + track_nets, active_right_nets, placed_target_nets + ) + ] + + def connect_splits(track_nets, column): + """Vertically connect nets on multiple tracks.""" + + # Make a copy so the original isn't disturbed. + track_nets = track_nets[:] + + # Find nets that are running on multiple tracks. + multi_nets = set( + net for net in set(track_nets) if track_nets.count(net) > 1 + ) + multi_nets.discard(None) # Ignore empty tracks. + + # Find possible intervals for multi-track nets. + net_intervals = [] + for net in multi_nets: + net_trk_idxs = [idx for idx, nt in enumerate(track_nets) if nt is net] + for index, trk1 in enumerate(net_trk_idxs[:-1], 1): + for trk2 in net_trk_idxs[index:]: + net_intervals.append(NetInterval(net, trk1, trk2)) + + # Sort interval lengths from smallest to largest. + net_intervals.sort(key=lambda ni: len(ni)) + # Sort interval lengths from largest to smallest. + # net_intervals.sort(key=lambda ni: -len(ni)) + + # Connect tracks for each interval if it doesn't intersect an + # already existing connection. + for net_interval in net_intervals: + for col_interval in column: + if net_interval.obstructs(col_interval): + break + else: + # No conflicts found with existing connections. + column.append(net_interval) + + # Get the nets that have vertical wires in the column. + column_nets = set(intvl.net for intvl in column) + + # Merge segments of each net in the column. + for net in column_nets: + # Extract intervals if the current net has more than one interval. + intervals = [intvl for intvl in column if intvl.net is net] + if len(intervals) < 2: + # Skip if there's only a single interval for this net. + continue + + # Remove the intervals so they can be replaced with joined intervals. + for intvl in intervals: + column.remove(intvl) + + # Merge the extracted intervals as much as possible. + + # Sort intervals by their beginning coordinates. + intervals.sort(key=lambda intvl: intvl.beg) + + # Try merging consecutive pairs of intervals. + for i in range(len(intervals) - 1): + # Try to merge consecutive intervals. + merged_intvl = intervals[i].merge(intervals[i + 1]) + if merged_intvl: + # Keep only the merged interval and place it so it's compared + # to the next one. + intervals[i : i + 2] = None, merged_intvl + + # Remove the None entries that are inserted when segments get merged. + intervals = [intvl for intvl in intervals if intvl] + + # Place merged intervals back into column. + column.extend(intervals) + + return column + + def extend_tracks(track_nets, column, targets): + """Extend track nets into the next column.""" + + # These are nets to the right of the current column. + rightward_nets = set(target.net for target in targets) + + # Keep extending nets to next column if they do not intersect + # intervals in the + # current column with the same net. + flow_thru_nets = track_nets[:] + for intvl in column: + for trk_idx in range(intvl.beg, intvl.end + 1): + if flow_thru_nets[trk_idx] is intvl.net: + # Remove net from track since it intersects an interval with the + # same net. The net may be extended from the interval in the + # next phase, + # or it may terminate here. + flow_thru_nets[trk_idx] = None + + next_track_nets = flow_thru_nets[:] + + # Extend track net if net has multiple column intervals that need further + # interconnection + # or if there are terminals in rightward columns that need connections to + # this net. + first_track = 0 + last_track = len(track_nets) - 1 + column_nets = set([intvl.net for intvl in column]) + for net in column_nets: + # Get all the vertical intervals for this net in the current column. + net_intervals = [i for i in column if i.net is net] + + # No need to extend tracks for this net into next column if there + # aren't multiple + # intervals or further terminals to connect. + if net not in rightward_nets and len(net_intervals) < 2: + continue + + # Sort the net's intervals from bottom of the column to top. + net_intervals.sort(key=lambda e: e.beg) + + # Find the nearest target to the right matching the current net. + target_row = None + for target in targets: + if target.net is net: + target_row = target.row + break + + for i, intvl in enumerate(net_intervals): + # Sanity check: should never get here if interval goes from + # top-to-bottom of + # column (hence, only one interval) and there is no further + # terminal for this + # net to the right. + assert not ( + intvl.beg == first_track + and intvl.end == last_track + and not target_row + ) + + if intvl.beg == first_track and intvl.end < last_track: + # Interval starts on bottom of column, so extend net in the + # track where it ends. + assert i == 0 + assert track_nets[intvl.end] in (net, None) + exit_row = intvl.end + next_track_nets[exit_row] = net + continue + + if intvl.end == last_track and intvl.beg > first_track: + # Interval ends on top of column, so extend net in the track + # where it begins. + assert i == len(net_intervals) - 1 + assert track_nets[intvl.beg] in (net, None) + exit_row = intvl.beg + next_track_nets[exit_row] = net + continue + + if target_row is None: + # No target to the right, so we must be trying to connect + # multiple column intervals for this net. + if i == 0: + # First interval in column so extend from its top-most + # point. + exit_row = intvl.end + next_track_nets[exit_row] = net + elif i == len(net_intervals) - 1: + # Last interval in column so extend from its bottom-most + # point. + exit_row = intvl.beg + next_track_nets[exit_row] = net + else: + # This interval is between the top and bottom intervals. + beg_end = ( + bool(flow_thru_nets[intvl.beg]), + bool(flow_thru_nets[intvl.end]), + ) + if beg_end == (True, False): + # The net enters this interval at its bottom, so extend + # from the top (dogleg). + exit_row = intvl.end + next_track_nets[exit_row] = net + elif beg_end == (False, True): + # The net enters this interval at its top, so extend + # from the bottom (dogleg). + exit_row = intvl.beg + next_track_nets[exit_row] = net + else: + raise RuntimeError + continue + + else: + # Target to the right, so aim for it. + + if target_row > intvl.end: + # target track is above the interval's end, so bound it to + # the end. + target_row = intvl.end + elif target_row < intvl.beg: + # target track is below the interval's start, so bound it + # to the start. + target_row = intvl.beg + + # Search for the closest track to the target row that is either + # open + # or occupied by the same target net. + intvl_nets = track_nets[intvl.beg : intvl.end + 1] + net_row = ( + net_search(net, target_row - intvl.beg, intvl_nets) + + target_row + ) + open_row = ( + net_search(None, target_row - intvl.beg, intvl_nets) + + target_row + ) + net_dist = abs(net_row - target_row) + open_dist = abs(open_row - target_row) + if net_dist <= open_dist: + exit_row = net_row + else: + exit_row = open_row + assert intvl.beg <= exit_row <= intvl.end + next_track_nets[exit_row] = net + continue + + return next_track_nets + + def trim_column_intervals(column, track_nets, next_track_nets): + """Trim stubs from column intervals.""" + + # All nets entering and exiting the column. + trk_nets = list(enumerate(zip(track_nets, next_track_nets))) + + for intvl in column: + # Get all the entry/exit track positions having the same net as the + # interval + # and that are within the bounds of the interval. + net = intvl.net + beg = intvl.beg + end = intvl.end + trks = [i for (i, nets) in trk_nets if net in nets and beg <= i <= end] + + # Chop off any stubs of the interval that extend past where it could + # connect to an entry/exit point of its net. + intvl.beg = min(trks) + intvl.end = max(trks) + + ######################################## + # Main switchbox routing loop. + ######################################## + + # Get target nets as routing proceeds from left-to-right. + targets = collect_targets(self.top_nets, self.bottom_nets, self.right_nets) + + # Store the nets in each column that are in the process of being routed, + # starting with the nets in the left-hand face of the switchbox. + nets_in_column = [self.left_nets[:]] + + # Store routing intervals in each column. + all_column_intvls = [] + + # Route left-to-right across the columns connecting the top & bottom nets + # on each column to tracks within the switchbox. + for col, (t_net, b_net) in enumerate(zip(self.top_nets, self.bottom_nets)): + # Nets in the previous column become the currently active nets being routed + active_nets = nets_in_column[-1][:] + + if col == 0 and not t_net and not b_net: + # Nothing happens in the first column if there are no top & bottom nets. + # Just continue the active nets from the left-hand face to the next + # column. + column_intvls = [] + next_active_nets = active_nets[:] + + else: + # Bring any nets on the top & bottom of this column into the list of + # active nets. + active_nets[0] = b_net + active_nets[-1] = t_net + + # Generate the intervals that will vertically connect the top & bottom + # nets to + # horizontal tracks in the switchbox. + column_intvls = connect_top_btm(active_nets) + + # Add the nets from the new vertical connections to the active nets. + augmented_active_nets = insert_column_nets(active_nets, column_intvls) + + # Remove the nets processed in this column from the list of target nets. + targets = prune_targets(targets, col) + + # Insert target nets from rightward columns into this column to direct + # the placement of additional vertical intervals towards them. + augmented_active_nets = insert_target_nets( + augmented_active_nets, targets, self.right_nets + ) + + # Make vertical connections between tracks in the column having the + # same net. + column_intvls = connect_splits(augmented_active_nets, column_intvls) + + # Get the nets that will be active in the next column. + next_active_nets = extend_tracks(active_nets, column_intvls, targets) + + # Trim any hanging stubs from vertical routing intervals in the current + # column. + trim_column_intervals(column_intvls, active_nets, next_active_nets) + + # Store the active nets for the next column. + nets_in_column.append(next_active_nets) + + # Store the vertical routing intervals for this column. + all_column_intvls.append(column_intvls) + + ######################################## + # End of switchbox routing loop. + ######################################## + + # After routing from left-to-right, verify the active track nets coincide + # with the positions of the nets on the right-hand face of the switchbox. + for track_net, right_net in zip(nets_in_column[-1], self.right_nets): + if track_net is not right_net: + if not options.get("allow_routing_failure"): + raise SwitchboxRoutingFailure + + # Create wiring segments along each horizontal track. + # Add left and right faces to coordinates of the vertical columns. + column_coords = ( + [self.left_face.track.coord] + + self.column_coords + + [self.right_face.track.coord] + ) + # Proceed column-by-column from left-to-right creating horizontal wires. + for col_idx, nets in enumerate(nets_in_column): + beg_col_coord = column_coords[col_idx] + end_col_coord = column_coords[col_idx + 1] + # Create segments for each track (skipping bottom & top faces). + for trk_idx, net in enumerate(nets[1:-1], start=1): + if net: + # Create a wire segment for the net in this horizontal track of the + # column. + trk_coord = self.track_coords[trk_idx] + p1 = Point(beg_col_coord, trk_coord) + p2 = Point(end_col_coord, trk_coord) + seg = Segment(p1, p2) + self.segments[net].append(seg) + + # Create vertical wiring segments for each switchbox column. + for idx, column in enumerate(all_column_intvls): + # Get X coord of this column. + col_coord = self.column_coords[idx] + # Create vertical wire segments for wire interval in the column. + for intvl in column: + p1 = Point(col_coord, self.track_coords[intvl.beg]) + p2 = Point(col_coord, self.track_coords[intvl.end]) + self.segments[intvl.net].append(Segment(p1, p2)) + + return self.segments + + def draw( + self, scr=None, tx=None, font=None, color=(128, 0, 128), thickness=2, **options + ): + """Draw a switchbox and its routing for debugging purposes. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. + Initialize PyGame ifNone. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (tuple, optional): Switchbox boundary color. Defaults to (128, 0, 128). + thickness (int, optional): Switchbox boundary thickness. Defaults to 2. + options (dict, optional): Dictionary of options and values. Defaults to {}. + """ + + # If the screen object is not None, then PyGame drawing is enabled so set flag + # to initialize PyGame. + do_start_end = not bool(scr) + + if do_start_end: + # Initialize PyGame. + scr, tx, font = draw_start( + self.bbox.resize(Vector(DRAWING_BOX_RESIZE, DRAWING_BOX_RESIZE)) + ) + + if options.get("draw_switchbox_boundary"): + # Draw switchbox boundary. + self.top_face.draw(scr, tx, font, color, thickness, **options) + self.bottom_face.draw(scr, tx, font, color, thickness, **options) + self.left_face.draw(scr, tx, font, color, thickness, **options) + self.right_face.draw(scr, tx, font, color, thickness, **options) + + if options.get("draw_switchbox_routing"): + # Draw routed wire segments. + try: + for segments in self.segments.values(): + for segment in segments: + draw_seg(segment, scr, tx, dot_radius=0) + except AttributeError: + pass + + if options.get("draw_routing_channels"): + # Draw routing channels from midpoint of one switchbox face to midpoint of another. + + def draw_channel(face1, face2): + seg1 = face1.seg + seg2 = face2.seg + p1 = (seg1.p1 + seg1.p2) / 2 + p2 = (seg2.p1 + seg2.p2) / 2 + draw_seg(Segment(p1, p2), scr, tx, (128, 0, 128), 1, dot_radius=0) + + draw_channel(self.top_face, self.bottom_face) + draw_channel(self.top_face, self.left_face) + draw_channel(self.top_face, self.right_face) + draw_channel(self.bottom_face, self.left_face) + draw_channel(self.bottom_face, self.right_face) + draw_channel(self.left_face, self.right_face) + + if do_start_end: + # Terminate PyGame. + draw_end() + + +@export_to_all +class Router: + """Mixin to add routing function to Node class.""" + + def add_routing_points(node, nets): + """Add routing points by extending wires from pins out to the edge of the part + bounding box. + + Args: + nets (list): List of nets to be routed. + """ + + def add_routing_pt(pin): + """Add the point for a pin on the boundary of a part.""" + + bbox = pin.part.lbl_bbox + pin.route_pt = copy.copy(pin.pt) + if pin.orientation == "U": + # Pin points up, so extend downward to the bottom of the bounding box. + pin.route_pt.y = bbox.min.y + elif pin.orientation == "D": + # Pin points down, so extend upward to the top of the bounding box. + pin.route_pt.y = bbox.max.y + elif pin.orientation == "L": + # Pin points left, so extend rightward to the right-edge of the + # bounding box. + pin.route_pt.x = bbox.max.x + elif pin.orientation == "R": + # Pin points right, so extend leftward to the left-edge of the + # bounding box. + pin.route_pt.x = bbox.min.x + else: + raise RuntimeError("Unknown pin orientation.") + + # Global set of part pin (x,y) points may have stuff from processing previous + # nodes, so clear it. + del pin_pts[:] # Clear the list. Works for Python 2 and 3. + + for net in nets: + # Add routing points for all pins on the net that are inside this node. + for pin in node.get_internal_pins(net): + # Store the point where the pin is. (This is used after routing to + # trim wire stubs.) + pin_pts.append((pin.pt * pin.part.tx).round()) + + # Add the point to which the wiring should be extended. + add_routing_pt(pin) + + # Add a wire to connect the part pin to the routing point on the + # bounding box periphery. + if pin.route_pt != pin.pt: + seg = Segment(pin.pt, pin.route_pt) * pin.part.tx + node.wires[pin.net].append(seg) + + def create_routing_tracks(node, routing_bbox): + """Create horizontal & vertical global routing tracks.""" + + # Find the coords of the horiz/vert tracks that will hold the H/V faces of the + # routing switchboxes. + v_track_coord = [] + h_track_coord = [] + + # The top/bottom/left/right of each part's labeled bounding box define the H/V + # tracks. + for part in node.parts: + bbox = (part.lbl_bbox * part.tx).round() + v_track_coord.append(bbox.min.x) + v_track_coord.append(bbox.max.x) + h_track_coord.append(bbox.min.y) + h_track_coord.append(bbox.max.y) + + # Create delimiting tracks around the routing area. Just take the number of + # nets to be routed + # and create a channel that size around the periphery. That's guaranteed to be + # big enough. + # This is overkill but probably not worth optimizing since any excess boundary + # area is ignored. + v_track_coord.append(routing_bbox.min.x) + v_track_coord.append(routing_bbox.max.x) + h_track_coord.append(routing_bbox.min.y) + h_track_coord.append(routing_bbox.max.y) + + # Remove any duplicate track coords and then sort them. + v_track_coord = list(set(v_track_coord)) + h_track_coord = list(set(h_track_coord)) + v_track_coord.sort() + h_track_coord.sort() + + # Create an H/V track for each H/V coord containing a list for holding the + # faces in that track. + v_tracks = [ + GlobalTrack(orientation=VERT, idx=idx, coord=coord) + for idx, coord in enumerate(v_track_coord) + ] + h_tracks = [ + GlobalTrack(orientation=HORZ, idx=idx, coord=coord) + for idx, coord in enumerate(h_track_coord) + ] + + def bbox_to_faces(part, bbox): + left_track = v_tracks[v_track_coord.index(bbox.min.x)] + right_track = v_tracks[v_track_coord.index(bbox.max.x)] + bottom_track = h_tracks[h_track_coord.index(bbox.min.y)] + top_track = h_tracks[h_track_coord.index(bbox.max.y)] + Face(part, left_track, bottom_track, top_track) + Face(part, right_track, bottom_track, top_track) + Face(part, bottom_track, left_track, right_track) + Face(part, top_track, left_track, right_track) + if isinstance(part, Part): + part.left_track = left_track + part.right_track = right_track + part.top_track = top_track + part.bottom_track = bottom_track + + # Add routing box faces for each side of a part's labeled bounding box. + for part in node.parts: + part_bbox = (part.lbl_bbox * part.tx).round() + bbox_to_faces(part, part_bbox) + + # Add routing box faces for each side of the expanded bounding box surrounding + # all parts. + bbox_to_faces(boundary, routing_bbox) + + # Extend the part faces in each horizontal track and then each vertical track. + for track in h_tracks: + track.extend_faces(v_tracks) + for track in v_tracks: + track.extend_faces(h_tracks) + + # Apply splits to all faces and combine coincident faces. + for track in h_tracks + v_tracks: + track.split_faces() + track.remove_duplicate_faces() + + # Add adjacencies between faces that define global routing paths within + # switchboxes. + for h_track in h_tracks[1:]: + h_track.add_adjacencies() + + return h_tracks, v_tracks + + def create_terminals(node, internal_nets, h_tracks, v_tracks): + """Create terminals on the faces in the routing tracks.""" + + # Add terminals to all non-part/non-boundary faces. + for track in h_tracks + v_tracks: + for face in track: + face.create_nonpin_terminals() + + # Add terminals to switchbox faces for all part pins on internal nets. + for net in internal_nets: + for pin in node.get_internal_pins(net): + # Find the track (top/bottom/left/right) that the pin is on. + part = pin.part + pt = pin.route_pt * part.tx + closest_dist = abs(pt.y - part.top_track.coord) + pin_track = part.top_track + coord = pt.x # Pin coord within top track. + dist = abs(pt.y - part.bottom_track.coord) + if dist < closest_dist: + closest_dist = dist + pin_track = part.bottom_track + coord = pt.x # Pin coord within bottom track. + dist = abs(pt.x - part.left_track.coord) + if dist < closest_dist: + closest_dist = dist + pin_track = part.left_track + coord = pt.y # Pin coord within left track. + dist = abs(pt.x - part.right_track.coord) + if dist < closest_dist: + closest_dist = dist + pin_track = part.right_track + coord = pt.y # Pin coord within right track. + + # Now search for the face in the track that the pin is on. + for face in pin_track: + if part in face.part and face.beg.coord <= coord <= face.end.coord: + if not getattr(pin, "face", None): + # Only assign pin to face if it hasn't already been assigned + # to + # another face. This handles the case where a pin is exactly + # at the end coordinate and beginning coordinate of two + # successive faces in the same track. + pin.face = face + face.pins.append(pin) + terminal = Terminal(pin.net, face, coord) + face.terminals.append(terminal) + break + + # Set routing capacity of faces based on # of terminals on each face. + for track in h_tracks + v_tracks: + for face in track: + face.set_capacity() + + def global_router(node, nets): + """Globally route a list of nets from face to face. + + Args: + nets (list): List of Nets to be routed. + + Returns: + List: List of GlobalRoutes. + """ + + # This maze router assembles routes from each pin sequentially. + # + # 1. Find faces with net pins on them and place them on the + # start_faces list. + # 2. Randomly select one of the start faces. Add all the other + # faces to the stop_faces list. + # 3. Find a route from the start face to closest stop face. + # This concludes the initial phase of the routing. + # 4. Iterate through the remaining faces on the start_faces list. + # a. Randomly select a start face. + # b. Set stop faces to be all the faces currently on + # global routes. + # c. Find a route from the start face to any face on + # the global routes, thus enlarging the set of + # contiguous routes while reducing the number of + # unrouted start faces. + # d. Add the faces on the new route to the stop_faces list. + + # Core routing function. + def rt_srch(start_face, stop_faces): + """ + Return a minimal-distance path from the start face to one of the stop + faces. + + Args: + start_face (Face): Face from which path search begins + stop_faces (List): List of Faces at which search will end. + + Raises: + RoutingFailure: No path was found. + + Returns: + GlobalWire: List of Faces from start face to one of the stop faces. + """ + + # Return empty path if no stop faces or already starting from a stop face. + if start_face in stop_faces or not stop_faces: + return GlobalWire(net) + + # Record faces that have been visited and their distance from the start face + visited_faces = [start_face] + start_face.dist_from_start = 0 + + # Path searches are allowed to touch a Face on a Part if it + # has a Pin on the net being routed or if it is one of the stop faces. + # This is necessary to allow a search to terminate on a stop face or to + # pass through a face with a net pin on the way to finding a connection + # to one of the stop faces. + unconstrained_faces = stop_faces | net_pin_faces + + # Search through faces until a path is found & returned or a routing + # exception occurs. + while True: + # Set up for finding the closest unvisited face. + closest_dist = float("inf") + closest_face = None + + # Search for the closest face adjacent to the visited faces. + visited_faces.sort(key=lambda f: f.dist_from_start) + for visited_face in visited_faces: + if visited_face.dist_from_start > closest_dist: + # Visited face is already further than the current + # closest face, so no use continuing search since + # any remaining visited faces are even more distant. + break + + # Get the distances to the faces adjacent to this previously-visited + # face + # and update the closest face if appropriate. + for adj in visited_face.adjacent: + if adj.face in visited_faces: + # Don't re-visit faces that have already been visited. + continue + + if ( + adj.face not in unconstrained_faces + and adj.face.capacity <= 0 + ): + # Skip faces with insufficient routing capacity. + continue + + # Compute distance of this adjacent face to the start face. + dist = visited_face.dist_from_start + adj.dist + + if dist < closest_dist: + # Record the closest face seen so far. + closest_dist = dist + closest_face = adj.face + closest_face.prev_face = visited_face + + if not closest_face: + # Exception raised if couldn't find a path from start to stop faces. + raise GlobalRoutingFailure( + "Global routing failure: " f"{net.name} {net} {start_face.pins}" + ) + + # Add the closest adjacent face to the list of visited faces. + closest_face.dist_from_start = closest_dist + visited_faces.append(closest_face) + + if closest_face in stop_faces: + # The newest, closest face is actually on the list of stop faces, + # so the search is done. + # Now search back from this face to find the path back to the start + # face. + face_path = [closest_face] + while face_path[-1] is not start_face: + face_path.append(face_path[-1].prev_face) + + # Decrement the routing capacities of the path faces to account for + # this new routing. + # Don't decrement the stop face because any routing through it was + # accounted for + # during a previous routing. + for face in face_path[:-1]: + if face.capacity > 0: + face.capacity -= 1 + + # Reverse face path to go from start-to-stop face and return it. + return GlobalWire(net, reversed(face_path)) + + # Key function for setting the order in which nets will be globally routed. + def rank_net(net): + """Rank net based on W/H of bounding box of pins and the # of pins.""" + + # Nets with a small bounding box probably have fewer routing resources + # so they should be routed first. + + bbox = BBox() + for pin in node.get_internal_pins(net): + bbox.add(pin.route_pt) + return (bbox.w + bbox.h, len(net.pins)) + + # Set order in which nets will be routed. + nets.sort(key=rank_net) + + # Globally route each net. + global_routes = [] + + for net in nets: + # List for storing GlobalWires connecting pins on net. + global_route = GlobalRoute() + + # Faces with pins from which paths/routing originate. + net_pin_faces = {pin.face for pin in node.get_internal_pins(net)} + start_faces = set(net_pin_faces) + + # Select a random start face and look for a route to *any* of the other + # start faces. + start_face = random.choice(list(start_faces)) + start_faces.discard(start_face) + stop_faces = set(start_faces) + initial_route = rt_srch(start_face, stop_faces) + global_route.append(initial_route) + + # The faces on the route that was found now become the stopping faces for + # any further routing. + stop_faces = set(initial_route) + + # Go thru the other start faces looking for a connection to any existing + # route. + for start_face in start_faces: + next_route = rt_srch(start_face, stop_faces) + global_route.append(next_route) + + # Update the set of stopping faces with the faces on the newest route. + stop_faces |= set(next_route) + + # Add the complete global route for this net to the list of global routes. + global_routes.append(global_route) + + return global_routes + + def create_switchboxes(node, h_tracks, v_tracks, **options): + """Create routing switchboxes from the faces in the horz/vert tracks. + + Args: + h_tracks (list): List of horizontal Tracks. + v_tracks (list): List of vertical Tracks. + options (dict, optional): Dictionary of options and values. + + Returns: + list: List of Switchboxes. + """ + + # Clear any switchboxes associated with faces because we'll be making new ones. + for track in h_tracks + v_tracks: + for face in track: + face.switchboxes.clear() + + # For each horizontal Face, create a switchbox where it is the top face of the + # box. + switchboxes = [] + for h_track in h_tracks[1:]: + for face in h_track: + try: + # Create a Switchbox with the given Face on top and add it to the + # list. + switchboxes.append(SwitchBox(face)) + except NoSwitchBox: + continue + + # Check the switchboxes for problems. + for swbx in switchboxes: + swbx.audit() + + # Small switchboxes are more likely to fail routing so try to combine them into + # larger switchboxes. + # Use switchboxes containing nets for routing as seeds for coalescing into larger + # switchboxes. + seeds = [swbx for swbx in switchboxes if swbx.has_nets()] + + # Sort seeds by perimeter so smaller ones are coalesced before larger ones. + seeds.sort(key=lambda swbx: swbx.bbox.w + swbx.bbox.h) + + # Coalesce smaller switchboxes into larger ones having more routing area. + # The smaller switchboxes are removed from the list of switchboxes. + switchboxes = [seed.coalesce(switchboxes) for seed in seeds] + switchboxes = [swbx for swbx in switchboxes if swbx] # Remove None boxes. + + # A coalesced switchbox may have non-part faces containing multiple terminals + # on the same net. Remove all but one to prevent multi-path routes. + for switchbox in switchboxes: + switchbox.trim_repeated_terminals() + + return switchboxes + + def switchbox_router(node, switchboxes, **options): + """Create detailed wiring between the terminals along the sides of each switchbox. + + Args: + switchboxes (list): List of SwitchBox objects to be individually routed. + options (dict, optional): Dictionary of options and values. + + Returns: + None + """ + + # Do detailed routing inside each switchbox. + # TODO: Switchboxes are independent so could they be routed in parallel? + for swbx in switchboxes: + try: + # Try routing switchbox from left-to-right. + swbx.route(**options) + + except RoutingFailure: + # Routing failed, so try routing top-to-bottom instead. + swbx.flip_xy() + # If this fails, then a routing exception will terminate the whole + # routing process. + swbx.route(**options) + swbx.flip_xy() + + # Add switchbox routes to existing node wiring. + for net, segments in swbx.segments.items(): + node.wires[net].extend(segments) + + def cleanup_wires(node): + """Try to make wire segments look prettier.""" + + def order_seg_points(segments): + """Order endpoints in a horizontal or vertical segment.""" + for seg in segments: + if seg.p2 < seg.p1: + seg.p1, seg.p2 = seg.p2, seg.p1 + + def segments_bbox(segments): + """Return bounding box containing the given list of segments.""" + seg_pts = list(chain(*((s.p1, s.p2) for s in segments))) + return BBox(*seg_pts) + + def extract_horz_vert_segs(segments): + """Separate segments and return lists of horizontal & vertical segments.""" + horz_segs = [seg for seg in segments if seg.p1.y == seg.p2.y] + vert_segs = [seg for seg in segments if seg.p1.x == seg.p2.x] + assert len(horz_segs) + len(vert_segs) == len(segments) + return horz_segs, vert_segs + + def split_segments(segments, net_pin_pts): + """ + Return list of net segments split into the smallest intervals without + intersections with other segments. + """ + + # Check each horizontal segment against each vertical segment and split + # each one if they intersect. + # (This clunky iteration is used so the horz/vert lists can be updated + # within the loop.) + horz_segs, vert_segs = extract_horz_vert_segs(segments) + i = 0 + while i < len(horz_segs): + hseg = horz_segs[i] + hseg_y = hseg.p1.y + j = 0 + while j < len(vert_segs): + vseg = vert_segs[j] + vseg_x = vseg.p1.x + if ( + hseg.p1.x <= vseg_x <= hseg.p2.x + and vseg.p1.y <= hseg_y <= vseg.p2.y + ): + int_pt = Point(vseg_x, hseg_y) + if int_pt != hseg.p1 and int_pt != hseg.p2: + horz_segs.append( + Segment(copy.copy(int_pt), copy.copy(hseg.p2)) + ) + hseg.p2 = copy.copy(int_pt) + if int_pt != vseg.p1 and int_pt != vseg.p2: + vert_segs.append( + Segment(copy.copy(int_pt), copy.copy(vseg.p2)) + ) + vseg.p2 = copy.copy(int_pt) + j += 1 + i += 1 + + i = 0 + while i < len(horz_segs): + hseg = horz_segs[i] + hseg_y = hseg.p1.y + for pt in net_pin_pts: + if pt.y == hseg_y and hseg.p1.x < pt.x < hseg.p2.x: + horz_segs.append(Segment(copy.copy(pt), copy.copy(hseg.p2))) + hseg.p2 = copy.copy(pt) + i += 1 + + j = 0 + while j < len(vert_segs): + vseg = vert_segs[j] + vseg_x = vseg.p1.x + for pt in net_pin_pts: + if pt.x == vseg_x and vseg.p1.y < pt.y < vseg.p2.y: + vert_segs.append(Segment(copy.copy(pt), copy.copy(vseg.p2))) + vseg.p2 = copy.copy(pt) + j += 1 + + return horz_segs + vert_segs + + def merge_segments(segments): + """ + Return segments after merging those that run the same direction and overlap + """ + + # Preprocess the segments. + horz_segs, vert_segs = extract_horz_vert_segs(segments) + + merged_segs = [] + + # Separate horizontal segments having the same Y coord. + horz_segs_v = defaultdict(list) + for seg in horz_segs: + horz_segs_v[seg.p1.y].append(seg) + + # Merge overlapping segments having the same Y coord. + for segs in horz_segs_v.values(): + # Order segments by their starting X coord. + segs.sort(key=lambda s: s.p1.x) + # Append first segment to list of merged segments. + merged_segs.append(segs[0]) + # Go thru the remaining segments looking for overlaps with the last + # entry on the merge list. + for seg in segs[1:]: + if seg.p1.x <= merged_segs[-1].p2.x: + # Segments overlap, so update the extent of the last entry. + merged_segs[-1].p2.x = max(seg.p2.x, merged_segs[-1].p2.x) + else: + # No overlap, so append the current segment to the merge list + # and use it for + # further checks of intersection with remaining segments. + merged_segs.append(seg) + + # Separate vertical segments having the same X coord. + vert_segs_h = defaultdict(list) + for seg in vert_segs: + vert_segs_h[seg.p1.x].append(seg) + + # Merge overlapping segments having the same X coord. + for segs in vert_segs_h.values(): + # Order segments by their starting Y coord. + segs.sort(key=lambda s: s.p1.y) + # Append first segment to list of merged segments. + merged_segs.append(segs[0]) + # Go thru the remaining segments looking for overlaps with the last + # entry on the merge list. + for seg in segs[1:]: + if seg.p1.y <= merged_segs[-1].p2.y: + # Segments overlap, so update the extent of the last entry. + merged_segs[-1].p2.y = max(seg.p2.y, merged_segs[-1].p2.y) + else: + # No overlap, so append the current segment to the merge list + # and use it for + # further checks of intersection with remaining segments. + merged_segs.append(seg) + + return merged_segs + + def break_cycles(segments): + """Remove segments to break any cycles of a net's segments.""" + + # Create a dict storing set of segments adjacent to each endpoint. + adj_segs = defaultdict(set) + for seg in segments: + # Add segment to set for each endpoint. + adj_segs[seg.p1].add(seg) + adj_segs[seg.p2].add(seg) + + # Create a dict storing the list of endpoints adjacent to each endpoint. + adj_pts = dict() + for pt, segs in adj_segs.items(): + # Store endpoints of all segments adjacent to endpoint, then remove the + # endpoint. + adj_pts[pt] = list({p for seg in segs for p in (seg.p1, seg.p2)}) + adj_pts[pt].remove(pt) + + # Start at any endpoint and visit adjacent endpoints until all have been + # visited. + # If an endpoint is seen more than once, then a cycle exists. Remove the + # segment forming the cycle. + visited_pts = [] # List of visited endpoints. + frontier_pts = list(adj_pts.keys())[:1] # Arbitrary starting point. + while frontier_pts: + # Visit a point on the frontier. + frontier_pt = frontier_pts.pop() + visited_pts.append(frontier_pt) + + # Check each adjacent endpoint for cycles. + for adj_pt in adj_pts[frontier_pt][:]: + if adj_pt in visited_pts + frontier_pts: + # This point was already reached by another path so there is a + # cycle. + # Break it by removing segment between frontier_pt and adj_pt. + loop_seg = (adj_segs[frontier_pt] & adj_segs[adj_pt]).pop() + segments.remove(loop_seg) + adj_segs[frontier_pt].remove(loop_seg) + adj_segs[adj_pt].remove(loop_seg) + adj_pts[frontier_pt].remove(adj_pt) + adj_pts[adj_pt].remove(frontier_pt) + else: + # First time adjacent point has been reached, so add it to + # frontier. + frontier_pts.append(adj_pt) + # Keep this new frontier point from backtracking to the current + # frontier point later. + adj_pts[adj_pt].remove(frontier_pt) + + return segments + + def is_pin_pt(pt): + """Return True if the point is on one of the part pins.""" + return pt in pin_pts + + def contains_pt(seg, pt): + """Return True if the point is contained within the horz/vert segment.""" + return seg.p1.x <= pt.x <= seg.p2.x and seg.p1.y <= pt.y <= seg.p2.y + + def trim_stubs(segments): + """ + Return segments after removing stubs that have an unconnected endpoint. + """ + + def get_stubs(segments): + """Return set of stub segments.""" + + # For end point, the dict entry contains a list of the segments that + # meet there. + stubs = defaultdict(list) + + # Process the segments looking for points that are on only a single + # segment. + for seg in segments: + # Add the segment to the segment list of each end point. + stubs[seg.p1].append(seg) + stubs[seg.p2].append(seg) + + # Keep only the segments with an unconnected endpoint that is not on a + # part pin. + stubs = { + segs[0] + for endpt, segs in stubs.items() + if len(segs) == 1 and not is_pin_pt(endpt) + } + return stubs + + trimmed_segments = set(segments[:]) + stubs = get_stubs(trimmed_segments) + while stubs: + trimmed_segments -= stubs + stubs = get_stubs(trimmed_segments) + return list(trimmed_segments) + + def remove_jogs_old(net, segments, wires, net_bboxes, part_bboxes): + """Remove jogs in wiring segments. + + Args: + net (Net): Net whose wire segments will be modified. + segments (list): List of wire segments for the given net. + wires (dict): Dict of lists of wire segments indexed by nets. + net_bboxes (dict): Dict of BBoxes for wire segments indexed by nets. + part_bboxes (list): List of BBoxes for the placed parts. + """ + + def get_touching_segs(seg, ortho_segs): + """Return list of orthogonal segments that touch the given segment.""" + touch_segs = set() + for oseg in ortho_segs: + # oseg horz, seg vert. Do they intersect? + if contains_pt(oseg, Point(seg.p2.x, oseg.p1.y)): + touch_segs.add(oseg) + # oseg vert, seg horz. Do they intersect? + elif contains_pt(oseg, Point(oseg.p2.x, seg.p1.y)): + touch_segs.add(oseg) + return list(touch_segs) # Convert to list with no dups. + + def get_overlap(*segs): + """ + Find extent of overlap of parallel horz/vert + segments and return as (min, max) tuple. + """ + ov1 = float("-inf") + ov2 = float("inf") + for seg in segs: + if seg.p1.y == seg.p2.y: + # Horizontal segment. + p1, p2 = seg.p1.x, seg.p2.x + else: + # Vertical segment. + p1, p2 = seg.p1.y, seg.p2.y + ov1 = max(ov1, p1) # Max of extent minimums. + ov2 = min(ov2, p2) # Min of extent maximums. + # assert ov1 <= ov2 + return ov1, ov2 + + def obstructed(segment): + """Return true if segment obstructed by parts or segments of other nets.""" + + # Obstructed if segment bbox intersects one of the part bboxes. + segment_bbox = BBox(segment.p1, segment.p2) + for part_bbox in part_bboxes: + if part_bbox.intersects(segment_bbox): + return True + + # BBoxes don't intersect if they line up exactly edge-to-edge. + # So expand the segment bbox slightly so intersections with bboxes of + # other segments will be detected. + segment_bbox = segment_bbox.resize(Vector(1, 1)) + + # Look for an overlay intersection with a segment of another net. + for nt, nt_bbox in net_bboxes.items(): + if nt is net: + # Don't check this segment with other segments of its own net. + continue + + if not segment_bbox.intersects(nt_bbox): + # Don't check this segment against segments of another net whose + # bbox doesn't even intersect this segment. + continue + + # Check for overlay intersectionss between this segment and the + # parallel segments of the other net. + for seg in wires[nt]: + if segment.p1.x == segment.p2.x == seg.p1.x == seg.p2.x: + # Segments are both aligned vertically on the same track + # X coord. + if segment.p1.y <= seg.p2.y and segment.p2.y >= seg.p1.y: + # Segments overlap so segment is obstructed. + return True + elif segment.p1.y == segment.p2.y == seg.p1.y == seg.p2.y: + # Segments are both aligned horizontally on the same track + # Y coord. + if segment.p1.x <= seg.p2.x and segment.p2.x >= seg.p1.x: + # Segments overlap so segment is obstructed. + return True + + # No obstructions found, so return False. + return False + + # Make sure p1 <= p2 for segment endpoints. + order_seg_points(segments) + + # Split segments into horizontal/vertical groups. + horz_segs, vert_segs = extract_horz_vert_segs(segments) + + # Look for a segment touched by ends of orthogonal segments all pointing in + # the same direction. + # Then slide this segment to the other end of the interval by which the + # touching segments + # overlap. This will reduce or eliminate the jog. + stop = True + for segs, ortho_segs in ((horz_segs, vert_segs), (vert_segs, horz_segs)): + for seg in segs: + # Don't move a segment if one of its endpoints connects to a part + # pin. + if is_pin_pt(seg.p1) or is_pin_pt(seg.p2): + continue + + # Find all orthogonal segments that touch this one. + touching_segs = get_touching_segs(seg, ortho_segs) + + # Find extent of overlap of all orthogonal segments. + ov1, ov2 = get_overlap(*touching_segs) + + if ov1 >= ov2: + # No overlap, so this segment can't be moved one way or + # the other. + continue + + if seg in horz_segs: + # Move horz segment vertically to other end of overlap to remove + # jog. + test_seg = Segment(seg.p1, seg.p2) + seg_y = test_seg.p1.y + if seg_y == ov1: + # Segment is at one end of the overlay, so move it to the + # other end. + test_seg.p1.y = ov2 + test_seg.p2.y = ov2 + if not obstructed(test_seg): + # Segment move is not obstructed, so accept it. + seg.p1 = test_seg.p1 + seg.p2 = test_seg.p2 + # If one segment is moved, maybe more can be moved so + # don't stop. + stop = False + elif seg_y == ov2: + # Segment is at one end of the overlay, so move it to the + # other end. + test_seg.p1.y = ov1 + test_seg.p2.y = ov1 + if not obstructed(test_seg): + # Segment move is not obstructed, so accept it. + seg.p1 = test_seg.p1 + seg.p2 = test_seg.p2 + # If one segment is moved, maybe more can be moved so + # don't stop. + stop = False + else: + # Segment in interior of overlay, so it's not a jog. Don't + # move it. + pass + else: + # Move vert segment horizontally to other end of overlap to + # remove jog. + test_seg = Segment(seg.p1, seg.p2) + seg_x = seg.p1.x + if seg_x == ov1: + # Segment is at one end of the overlay, so move it to the + # other end. + test_seg.p1.x = ov2 + test_seg.p2.x = ov2 + if not obstructed(test_seg): + # Segment move is not obstructed, so accept it. + seg.p1 = test_seg.p1 + seg.p2 = test_seg.p2 + # If one segment is moved, maybe more can be moved so + # don't stop. + stop = False + elif seg_x == ov2: + # Segment is at one end of the overlay, so move it to the + # other end. + test_seg.p1.x = ov1 + test_seg.p2.x = ov1 + if not obstructed(test_seg): + # Segment move is not obstructed, so accept it. + seg.p1 = test_seg.p1 + seg.p2 = test_seg.p2 + # If one segment is moved, maybe more can be moved so + # don't stop. + stop = False + else: + # Segment in interior of overlay, so it's not a jog. Don't + # move it. + pass + + # Return updated segments. If no segments for this net were updated, then + # stop is True. + return segments, stop + + def remove_jogs(net, segments, wires, net_bboxes, part_bboxes): + """Remove jogs and staircases in wiring segments. + + Args: + net (Net): Net whose wire segments will be modified. + segments (list): List of wire segments for the given net. + wires (dict): Dict of lists of wire segments indexed by nets. + net_bboxes (dict): Dict of BBoxes for wire segments indexed by nets. + part_bboxes (list): List of BBoxes for the placed parts. + """ + + def obstructed(segment): + """ + Return true if segment obstructed by parts or segments of other nets. + """ + + # Obstructed if segment bbox intersects one of the part bboxes. + segment_bbox = BBox(segment.p1, segment.p2) + for part_bbox in part_bboxes: + if part_bbox.intersects(segment_bbox): + return True + + # BBoxes don't intersect if they line up exactly edge-to-edge. + # So expand the segment bbox slightly so intersections with bboxes of + # other segments will be detected. + segment_bbox = segment_bbox.resize(Vector(2, 2)) + + # Look for an overlay intersection with a segment of another net. + for nt, nt_bbox in net_bboxes.items(): + if nt is net: + # Don't check this segment with other segments of its own net. + continue + + if not segment_bbox.intersects(nt_bbox): + # Don't check this segment against segments of another net whose + # bbox doesn't even intersect this segment. + continue + + # Check for overlay intersectionss between this segment and the + # parallel segments of the other net. + for seg in wires[nt]: + if segment.p1.x == segment.p2.x == seg.p1.x == seg.p2.x: + # Segments are both aligned vertically on the same track + # X coord. + if segment.p1.y <= seg.p2.y and segment.p2.y >= seg.p1.y: + # Segments overlap so segment is obstructed. + return True + elif segment.p1.y == segment.p2.y == seg.p1.y == seg.p2.y: + # Segments are both aligned horizontally on the same track + # Y coord. + if segment.p1.x <= seg.p2.x and segment.p2.x >= seg.p1.x: + # Segments overlap so segment is obstructed. + return True + + # No obstructions found, so return False. + return False + + def get_corners(segments): + """ + Return dictionary of right-angle corner + points and lists of associated segments. + """ + + # For each corner point, the dict entry contains a list of the segments + # that meet there. + corners = defaultdict(list) + + # Process the segments so that any potential right-angle corner has the + # horizontal + # segment followed by the vertical segment. + horz_segs, vert_segs = extract_horz_vert_segs(segments) + for seg in horz_segs + vert_segs: + # Add the segment to the segment list of each end point. + corners[seg.p1].append(seg) + corners[seg.p2].append(seg) + + # Keep only the corner points where two segments meet at right angles at + # a point not on a part pin. + corners = { + corner: segs + for corner, segs in corners.items() + if len(segs) == 2 + and not is_pin_pt(corner) + and segs[0] in horz_segs + and segs[1] in vert_segs + } + return corners + + def get_jogs(segments): + """ + Yield the three segments and starting and end points of a staircase or + tophat jog. + """ + + # Get dict of right-angle corners formed by segments. + corners = get_corners(segments) + + # Look for segments with both endpoints on right-angle corners, + # indicating this segment + # is in the middle of a three-segment staircase or tophat jog. + for segment in segments: + if segment.p1 in corners and segment.p2 in corners: + # Get the three segments in the jog. + jog_segs = set() + jog_segs.add(corners[segment.p1][0]) + jog_segs.add(corners[segment.p1][1]) + jog_segs.add(corners[segment.p2][0]) + jog_segs.add(corners[segment.p2][1]) + + # Get the points where the three-segment jog starts and stops. + start_stop_pts = set() + for seg in jog_segs: + start_stop_pts.add(seg.p1) + start_stop_pts.add(seg.p2) + start_stop_pts.discard(segment.p1) + start_stop_pts.discard(segment.p2) + + # Send the jog that was found. + yield list(jog_segs), list(start_stop_pts) + + # Shuffle segments to vary the order of detected jogs. + random.shuffle(segments) + + # Get iterator for jogs. + jogs = get_jogs(segments) + + # Search for jogs and break from the loop if a correctable jog is found or + # we run out of jogs. + while True: + # Get the segments and start-stop points for the next jog. + try: + jog_segs, start_stop_pts = next(jogs) + except StopIteration: + # No more jogs and no corrections made, so return segments and stop + # flag is true. + return segments, True + + # Get the start-stop points and order them so p1 < p3. + p1, p3 = sorted(start_stop_pts) + + # These are the potential routing points for correcting the jog. + # Either start at p1 and move vertically and then horizontally to p3, or + # move horizontally from p1 and then vertically to p3. + p2s = [Point(p1.x, p3.y), Point(p3.x, p1.y)] + + # Shuffle the routing points so the applied correction isn't always the + # same orientation. + random.shuffle(p2s) + + # Check each routing point to see if it leads to a valid routing. + for p2 in p2s: + # Replace the three-segment jog with these two right-angle segments. + new_segs = [ + Segment(copy.copy(pa), copy.copy(pb)) + for pa, pb in ((p1, p2), (p2, p3)) + if pa != pb + ] + order_seg_points(new_segs) + + # Check the new segments to see if they run into parts or segmentsof + # other nets. + if not any((obstructed(new_seg) for new_seg in new_segs)): + # OK, segments are good so replace the old segments in the jog + # with them. + for seg in jog_segs: + segments.remove(seg) + segments.extend(new_segs) + + # Return updated segments and set stop flag to false because + # segments were modified. + return segments, False + + # Get part bounding boxes so parts can be avoided when modifying net segments. + part_bboxes = [p.bbox * p.tx for p in node.parts] + + # Get dict of bounding boxes for the nets in this node. + net_bboxes = {net: segments_bbox(segs) for net, segs in node.wires.items()} + + # Get locations for part pins of each net. + # (For use when splitting net segments.) + net_pin_pts = dict() + for net in node.wires.keys(): + net_pin_pts[net] = [ + (pin.pt * pin.part.tx).round() for pin in node.get_internal_pins(net) + ] + + # Do a generalized cleanup of the wire segments of each net. + for net, segments in node.wires.items(): + # Round the wire segment endpoints to integers. + segments = [seg.round() for seg in segments] + + # Keep only non zero-length segments. + segments = [seg for seg in segments if seg.p1 != seg.p2] + + # Make sure the segment endpoints are in the right order. + order_seg_points(segments) + + # Merge colinear, overlapping segments. + # Also removes any duplicated segments. + segments = merge_segments(segments) + + # Split intersecting segments. + segments = split_segments(segments, net_pin_pts[net]) + + # Break loops of segments. + segments = break_cycles(segments) + + # Keep only non zero-length segments. + segments = [seg for seg in segments if seg.p1 != seg.p2] + + # Trim wire stubs. + segments = trim_stubs(segments) + + node.wires[net] = segments + + # Remove jogs in the wire segments of each net. + keep_cleaning = True + while keep_cleaning: + keep_cleaning = False + + for net, segments in node.wires.items(): + while True: + # Split intersecting segments. + segments = split_segments(segments, net_pin_pts[net]) + + # Remove unnecessary wire jogs. + segments, stop = remove_jogs( + net, segments, node.wires, net_bboxes, part_bboxes + ) + + # Keep only non zero-length segments. + segments = [seg for seg in segments if seg.p1 != seg.p2] + + # Merge segments made colinear by removing jogs. + segments = merge_segments(segments) + + # Split intersecting segments. + segments = split_segments(segments, net_pin_pts[net]) + + # Keep only non zero-length segments. + segments = [seg for seg in segments if seg.p1 != seg.p2] + + # Trim wire stubs caused by removing jogs. + segments = trim_stubs(segments) + + if stop: + # Break from loop once net segments can no longer be improved. + break + + # Recalculate the net bounding box after modifying its segments. + net_bboxes[net] = segments_bbox(segments) + + keep_cleaning = True + + # Merge segments made colinear by removing jogs. + segments = merge_segments(segments) + + # Update the node net's wire with the cleaned version. + node.wires[net] = segments + + def add_junctions(node): + """Add X & T-junctions where wire segments in the same net meet.""" + + def find_junctions(route): + """Find junctions where segments of a net intersect. + + Args: + route (List): List of Segment objects. + + Returns: + List: List of Points, one for each junction. + + Notes: + You must run merge_segments() before finding junctions + or else the segment endpoints might not be ordered + correctly with p1 < p2. + """ + + # Separate route into vertical and horizontal segments. + horz_segs = [seg for seg in route if seg.p1.y == seg.p2.y] + vert_segs = [seg for seg in route if seg.p1.x == seg.p2.x] + + junctions = [] + + # Check each pair of horz/vert segments for an intersection, except + # where they form a right-angle turn. + for hseg in horz_segs: + hseg_y = hseg.p1.y # Horz seg Y coord. + for vseg in vert_segs: + vseg_x = vseg.p1.x # Vert seg X coord. + if (hseg.p1.x < vseg_x < hseg.p2.x) and ( + vseg.p1.y <= hseg_y <= vseg.p2.y + ): + # The vert segment intersects the interior of the horz seg. + junctions.append(Point(vseg_x, hseg_y)) + elif (vseg.p1.y < hseg_y < vseg.p2.y) and ( + hseg.p1.x <= vseg_x <= hseg.p2.x + ): + # The horz segment intersects the interior of the vert seg. + junctions.append(Point(vseg_x, hseg_y)) + + return junctions + + for net, segments in node.wires.items(): + # Add X & T-junctions between segments in the same net. + junctions = find_junctions(segments) + node.junctions[net].extend(junctions) + + def rmv_routing_stuff(node): + """Remove attributes added to parts/pins during routing.""" + + rmv_attr(node.parts, ("left_track", "right_track", "top_track", "bottom_track")) + for part in node.parts: + rmv_attr(part.pins, ("route_pt", "face")) + + def route(node, tool=None, **options): + """Route the wires between part pins in this node and its children. + + Steps: + 1. Divide the bounding box surrounding the parts into switchboxes. + 2. Do global routing of nets through sequences of switchboxes. + 3. Do detailed routing within each switchbox. + + Args: + node (Node): Hierarchical node containing the parts to be connected. + tool (str): Backend tool for schematics. + options (dict, optional): Dictionary of options and values: + "allow_routing_failure", "draw", "draw_all_terminals", + "show_capacities", "draw_switchbox", "draw_routing", + "draw_channels" + """ + + # Inject the constants for the backend tool into this module. + import skidl + from skidl.tools import tool_modules + + tool = tool or skidl.config.tool + this_module = sys.modules[__name__] + this_module.__dict__.update(tool_modules[tool].constants.__dict__) + + random.seed(options.get("seed")) + + # Remove any stuff leftover from a previous place & route run. + node.rmv_routing_stuff() + + # First, recursively route any children of this node. + # TODO: Child nodes are independent so could they be processed in parallel? + for child in node.children.values(): + child.route(tool=tool, **options) + + # Exit if no parts to route in this node. + if not node.parts: + return + + # Get all the nets that have one or more pins within this node. + internal_nets = node.get_internal_nets() + + # Exit if no nets to route. + if not internal_nets: + return + + try: + # Extend routing points of part pins to the edges of their bounding boxes. + node.add_routing_points(internal_nets) + + # Create the surrounding box that contains the entire routing area. + channel_sz = (len(internal_nets) + 1) * GRID + routing_bbox = ( + node.internal_bbox().resize(Vector(channel_sz, channel_sz)) + ).round() + + # Create horizontal & vertical global routing tracks and faces. + h_tracks, v_tracks = node.create_routing_tracks(routing_bbox) + + # Create terminals on the faces in the routing tracks. + node.create_terminals(internal_nets, h_tracks, v_tracks) + + # Draw part outlines, routing tracks and terminals. + if options.get("draw_routing_channels"): + draw_routing( + node, routing_bbox, node.parts, h_tracks, v_tracks, **options + ) + + # Do global routing of nets internal to the node. + global_routes = node.global_router(internal_nets) + + # Convert the global face-to-face routes into terminals on the switchboxes. + for route in global_routes: + route.cvt_faces_to_terminals() + + # If enabled, draw the global routing for debug purposes. + if options.get("draw_global_routing"): + draw_routing( + node, + routing_bbox, + node.parts, + h_tracks, + v_tracks, + global_routes, + **options, + ) + + # Create detailed wiring using switchbox routing for the global routes. + switchboxes = node.create_switchboxes(h_tracks, v_tracks) + + # Draw switchboxes and routing channels. + if options.get("draw_assigned_terminals"): + draw_routing( + node, + routing_bbox, + node.parts, + switchboxes, + global_routes, + **options, + ) + + node.switchbox_router(switchboxes, **options) + + # If enabled, draw the global and detailed routing for debug purposes. + if options.get("draw_switchbox_routing"): + draw_routing( + node, + routing_bbox, + node.parts, + global_routes, + switchboxes, + **options, + ) + + # Now clean-up the wires and add junctions. + node.cleanup_wires() + node.add_junctions() + + # If enabled, draw the global and detailed routing for debug purposes. + if options.get("draw_switchbox_routing"): + draw_routing(node, routing_bbox, node.parts, **options) + + # Remove any stuff leftover from this place & route run. + node.rmv_routing_stuff() + + except RoutingFailure: + # Remove any stuff leftover from this place & route run. + node.rmv_routing_stuff() + raise RoutingFailure diff --git a/src/faebryk/exporters/schematic/kicad/schematic.py b/src/faebryk/exporters/schematic/kicad/schematic.py new file mode 100644 index 00000000..c3c7b519 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/schematic.py @@ -0,0 +1,797 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + + +import datetime +import os.path +import re +import time +from collections import Counter, OrderedDict + +import faebryk.library._F as F +from faebryk.core.graphinterface import Graph +from faebryk.core.module import Module +from faebryk.core.node import Node +from faebryk.core.trait import Trait +from faebryk.exporters.schematic.kicad.transformer import SchTransformer +from faebryk.libs.util import cast_assert + +from .bboxes import calc_hier_label_bbox, calc_symbol_bbox +from .constants import BLK_INT_PAD, BOX_LABEL_FONT_SIZE, GRID, PIN_LABEL_FONT_SIZE + +# from skidl.scriptinfo import get_script_name +from .geometry import BBox, Point, Tx, Vector +from .net_terminal import NetTerminal + +# from skidl.utilities import rmv_attr + + +__all__ = [] + +""" +Functions for generating a KiCad EESCHEMA schematic. +""" + + +def bbox_to_eeschema(bbox, tx, name=None): + """Create a bounding box using EESCHEMA graphic lines.""" + + # Make sure the box corners are integers. + bbox = (bbox * tx).round() + + graphic_box = [] + + if name: + # Place name at the lower-left corner of the box. + name_pt = bbox.ul + graphic_box.append( + "Text Notes {} {} 0 {} ~ 20\n{}".format( + name_pt.x, name_pt.y, BOX_LABEL_FONT_SIZE, name + ) + ) + + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.lr.x, bbox.lr.y) + ) + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.lr.x, bbox.lr.y, bbox.ur.x, bbox.ur.y) + ) + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.ur.x, bbox.ur.y, bbox.ul.x, bbox.ul.y) + ) + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.ul.x, bbox.ul.y, bbox.ll.x, bbox.ll.y) + ) + graphic_box.append("") # For blank line at end. + + return "\n".join(graphic_box) + + +def net_to_eeschema(self, tx): + """Generate the EESCHEMA code for the net terminal. + + Args: + tx (Tx): Transformation matrix for the node containing this net terminal. + + Returns: + str: EESCHEMA code string. + """ + self.pins[0].stub = True + self.pins[0].orientation = "R" + return pin_label_to_eeschema(self.pins[0], tx) + # return pin_label_to_eeschema(self.pins[0], tx) + bbox_to_eeschema(self.bbox, self.tx * tx) + + +def part_to_eeschema(part, tx): + """Create EESCHEMA code for a part. + + Args: + part (Part): SKiDL part. + tx (Tx): Transformation matrix. + + Returns: + string: EESCHEMA code for the part. + + Notes: + https://en.wikibooks.org/wiki/Kicad/file_formats#Schematic_Files_Format + """ + + tx = part.tx * tx + origin = tx.origin.round() + time_hex = hex(int(time.time()))[2:] + unit_num = getattr(part, "num", 1) + + eeschema = [] + eeschema.append("$Comp") + lib = os.path.splitext(part.lib.filename)[0] + eeschema.append("L {}:{} {}".format(lib, part.name, part.ref)) + eeschema.append("U {} 1 {}".format(unit_num, time_hex)) + eeschema.append("P {} {}".format(str(origin.x), str(origin.y))) + + # Add part symbols. For now we are only adding the designator + n_F0 = 1 + for i in range(len(part.draw)): + if re.search("^DrawF0", str(part.draw[i])): + n_F0 = i + break + eeschema.append( + 'F 0 "{}" {} {} {} {} {} {} {}'.format( + part.ref, + part.draw[n_F0].orientation, + str(origin.x + part.draw[n_F0].x), + str(origin.y + part.draw[n_F0].y), + part.draw[n_F0].size, + "000", # TODO: Refine this to match part def. + part.draw[n_F0].halign, + part.draw[n_F0].valign, + ) + ) + + # Part value. + n_F1 = 1 + for i in range(len(part.draw)): + if re.search("^DrawF1", str(part.draw[i])): + n_F1 = i + break + eeschema.append( + 'F 1 "{}" {} {} {} {} {} {} {}'.format( + str(part.value), + part.draw[n_F1].orientation, + str(origin.x + part.draw[n_F1].x), + str(origin.y + part.draw[n_F1].y), + part.draw[n_F1].size, + "000", # TODO: Refine this to match part def. + part.draw[n_F1].halign, + part.draw[n_F1].valign, + ) + ) + + # Part footprint. + n_F2 = 2 + for i in range(len(part.draw)): + if re.search("^DrawF2", str(part.draw[i])): + n_F2 = i + break + eeschema.append( + 'F 2 "{}" {} {} {} {} {} {} {}'.format( + part.footprint, + part.draw[n_F2].orientation, + str(origin.x + part.draw[n_F2].x), + str(origin.y + part.draw[n_F2].y), + part.draw[n_F2].size, + "001", # TODO: Refine this to match part def. + part.draw[n_F2].halign, + part.draw[n_F2].valign, + ) + ) + eeschema.append(" 1 {} {}".format(str(origin.x), str(origin.y))) + eeschema.append(" {} {} {} {}".format(tx.a, tx.b, tx.c, tx.d)) + eeschema.append("$EndComp") + eeschema.append("") # For blank line at end. + + # For debugging: draws a bounding box around a part. + # eeschema.append(bbox_to_eeschema(part.bbox, tx)) + # eeschema.append(bbox_to_eeschema(part.place_bbox, tx)) + + return "\n".join(eeschema) + + +def wire_to_eeschema(net, wire, tx): + """Create EESCHEMA code for a multi-segment wire. + + Args: + net (Net): Net associated with the wire. + wire (list): List of Segments for a wire. + tx (Tx): transformation matrix for each point in the wire. + + Returns: + string: Text to be placed into EESCHEMA file. + """ + + eeschema = [] + for segment in wire: + eeschema.append("Wire Wire Line") + w = (segment * tx).round() + eeschema.append(" {} {} {} {}".format(w.p1.x, w.p1.y, w.p2.x, w.p2.y)) + eeschema.append("") # For blank line at end. + return "\n".join(eeschema) + + +def junction_to_eeschema(net, junctions, tx): + eeschema = [] + for junction in junctions: + pt = (junction * tx).round() + eeschema.append("Connection ~ {} {}".format(pt.x, pt.y)) + eeschema.append("") # For blank line at end. + return "\n".join(eeschema) + + +def power_part_to_eeschema(part, tx=Tx()): + return "" # REMOVE: Remove this. + out = [] + for pin in part.pins: + try: + if not (pin.net is None): + if pin.net.netclass == "Power": + # strip out the '_...' section from power nets + t = pin.net.name + u = t.split("_") + symbol_name = u[0] + # find the stub in the part + time_hex = hex(int(time.time()))[2:] + pin_pt = (part.origin + offset + Point(pin.x, pin.y)).round() + x, y = pin_pt.x, pin_pt.y + out.append("$Comp\n") + out.append("L power:{} #PWR?\n".format(symbol_name)) + out.append("U 1 1 {}\n".format(time_hex)) + out.append("P {} {}\n".format(str(x), str(y))) + # Add part symbols. For now we are only adding the designator + n_F0 = 1 + for i in range(len(part.draw)): + if re.search("^DrawF0", str(part.draw[i])): + n_F0 = i + break + part_orientation = part.draw[n_F0].orientation + part_horizontal_align = part.draw[n_F0].halign + part_vertical_align = part.draw[n_F0].valign + + # check if the pin orientation will clash with the power part + if "+" in symbol_name: + # voltage sources face up, so check if the pin is facing down (opposite logic y-axis) + if pin.orientation == "U": + orientation = [-1, 0, 0, 1] + elif "gnd" in symbol_name.lower(): + # gnd points down so check if the pin is facing up (opposite logic y-axis) + if pin.orientation == "D": + orientation = [-1, 0, 0, 1] + out.append( + 'F 0 "{}" {} {} {} {} {} {} {}\n'.format( + "#PWR?", + part_orientation, + str(x + 25), + str(y + 25), + str(40), + "001", + part_horizontal_align, + part_vertical_align, + ) + ) + out.append( + 'F 1 "{}" {} {} {} {} {} {} {}\n'.format( + symbol_name, + part_orientation, + str(x + 25), + str(y + 25), + str(40), + "000", + part_horizontal_align, + part_vertical_align, + ) + ) + out.append(" 1 {} {}\n".format(str(x), str(y))) + out.append( + " {} {} {} {}\n".format( + orientation[0], + orientation[1], + orientation[2], + orientation[3], + ) + ) + out.append("$EndComp\n") + except Exception as inst: + print(type(inst)) + print(inst.args) + print(inst) + return "\n" + "".join(out) + + +# Sizes of EESCHEMA schematic pages from smallest to largest. Dimensions in mils. +A_sizes_list = [ + ("A4", BBox(Point(0, 0), Point(11693, 8268))), + ("A3", BBox(Point(0, 0), Point(16535, 11693))), + ("A2", BBox(Point(0, 0), Point(23386, 16535))), + ("A1", BBox(Point(0, 0), Point(33110, 23386))), + ("A0", BBox(Point(0, 0), Point(46811, 33110))), +] + +# Create bounding box for each A size sheet. +A_sizes = OrderedDict(A_sizes_list) + + +def get_A_size(bbox): + """Return the A-size page needed to fit the given bounding box.""" + + width = bbox.w + height = bbox.h * 1.25 # HACK: why 1.25? + for A_size, page in A_sizes.items(): + if width < page.w and height < page.h: + return A_size + return "A0" # Nothing fits, so use the largest available. + + +def calc_sheet_tx(bbox): + """Compute the page size and positioning for this sheet.""" + A_size = get_A_size(bbox) + page_bbox = bbox * Tx(d=-1) + move_to_ctr = A_sizes[A_size].ctr.snap(GRID) - page_bbox.ctr.snap(GRID) + move_tx = Tx(d=-1).move(move_to_ctr) + return move_tx + + +def calc_pin_dir(pin): + """Calculate pin direction accounting for part transformation matrix.""" + + # Copy the part trans. matrix, but remove the translation vector, leaving only scaling/rotation stuff. + tx = pin.part.tx + tx = Tx(a=tx.a, b=tx.b, c=tx.c, d=tx.d) + + # Use the pin orientation to compute the pin direction vector. + pin_vector = { + "U": Point(0, 1), + "D": Point(0, -1), + "L": Point(-1, 0), + "R": Point(1, 0), + }[pin.orientation] + + # Rotate the direction vector using the part rotation matrix. + pin_vector = pin_vector * tx + + # Create an integer tuple from the rotated direction vector. + pin_vector = (int(round(pin_vector.x)), int(round(pin_vector.y))) + + # Return the pin orientation based on its rotated direction vector. + return { + (0, 1): "U", + (0, -1): "D", + (-1, 0): "L", + (1, 0): "R", + }[pin_vector] + + +def pin_label_to_eeschema(pin, tx): + """Create EESCHEMA text of net label attached to a pin.""" + + if pin.stub is False or not pin.is_connected(): + # No label if pin is not connected or is connected to an explicit wire. + return "" + + label_type = "HLabel" + for pn in pin.net.pins: + if pin.part.hierarchy.startswith(pn.part.hierarchy): + continue + if pn.part.hierarchy.startswith(pin.part.hierarchy): + continue + label_type = "GLabel" + break + + part_tx = pin.part.tx * tx + pt = pin.pt * part_tx + + pin_dir = calc_pin_dir(pin) + orientation = { + "R": 0, + "D": 1, + "L": 2, + "U": 3, + }[pin_dir] + + return "Text {} {} {} {} {} UnSpc ~ 0\n{}\n".format( + label_type, + int(round(pt.x)), + int(round(pt.y)), + orientation, + PIN_LABEL_FONT_SIZE, + pin.net.name, + ) + + +def create_eeschema_file( + filename, + contents, + cur_sheet_num=1, + total_sheet_num=1, + title="Default", + rev_major=0, + rev_minor=1, + year=datetime.date.today().year, + month=datetime.date.today().month, + day=datetime.date.today().day, + A_size="A2", +): + """Write EESCHEMA header, contents, and footer to a file.""" + + with open(filename, "w") as f: + f.write( + "\n".join( + ( + "EESchema Schematic File Version 4", + "EELAYER 30 0", + "EELAYER END", + "$Descr {} {} {}".format( + A_size, A_sizes[A_size].max.x, A_sizes[A_size].max.y + ), + "encoding utf-8", + "Sheet {} {}".format(cur_sheet_num, total_sheet_num), + 'Title "{}"'.format(title), + 'Date "{}-{}-{}"'.format(year, month, day), + 'Rev "v{}.{}"'.format(rev_major, rev_minor), + 'Comp ""', + 'Comment1 ""', + 'Comment2 ""', + 'Comment3 ""', + 'Comment4 ""', + "$EndDescr", + "", + contents, + "$EndSCHEMATC", + ) + ) + ) + + +def node_to_eeschema(node, sheet_tx=Tx()): + """Convert node circuitry to an EESCHEMA sheet. + + Args: + sheet_tx (Tx, optional): Scaling/translation matrix for sheet. Defaults to Tx(). + + Returns: + str: EESCHEMA text for the node circuitry. + """ + + from skidl import HIER_SEP + + # List to hold all the EESCHEMA code for this node. + eeschema_code = [] + + if node.flattened: + # Create the transformation matrix for the placement of the parts in the node. + tx = node.tx * sheet_tx + else: + # Unflattened nodes are placed in their own sheet, so compute + # their bounding box as if they *were* flattened and use that to + # find the transformation matrix for an appropriately-sized sheet. + flattened_bbox = node.internal_bbox() + tx = calc_sheet_tx(flattened_bbox) + + # Generate EESCHEMA code for each child of this node. + for child in node.children.values(): + eeschema_code.append(node_to_eeschema(child, tx)) + + # Generate EESCHEMA code for each part in the node. + for part in node.parts: + if isinstance(part, NetTerminal): + eeschema_code.append(net_to_eeschema(part, tx=tx)) + else: + eeschema_code.append(part_to_eeschema(part, tx=tx)) + + # Generate EESCHEMA wiring code between the parts in the node. + for net, wire in node.wires.items(): + wire_code = wire_to_eeschema(net, wire, tx=tx) + eeschema_code.append(wire_code) + for net, junctions in node.junctions.items(): + junction_code = junction_to_eeschema(net, junctions, tx=tx) + eeschema_code.append(junction_code) + + # Generate power connections for the each part in the node. + for part in node.parts: + stub_code = power_part_to_eeschema(part, tx=tx) + if len(stub_code) != 0: + eeschema_code.append(stub_code) + + # Generate pin labels for stubbed nets on each part in the node. + for part in node.parts: + for pin in part: + pin_label_code = pin_label_to_eeschema(pin, tx=tx) + eeschema_code.append(pin_label_code) + + # Join EESCHEMA code into one big string. + eeschema_code = "\n".join(eeschema_code) + + # If this node was flattened, then return the EESCHEMA code and surrounding box + # for inclusion in the parent node. + if node.flattened: + # Generate the graphic box that surrounds the flattened hierarchical block of this node. + block_name = node.name.split(HIER_SEP)[-1] + pad = Vector(BLK_INT_PAD, BLK_INT_PAD) + bbox_code = bbox_to_eeschema(node.bbox.resize(pad), tx, block_name) + + return "\n".join((eeschema_code, bbox_code)) + + # Create a hierarchical sheet file for storing this unflattened node. + A_size = get_A_size(flattened_bbox) + filepath = os.path.join(node.filepath, node.sheet_filename) + create_eeschema_file(filepath, eeschema_code, title=node.title, A_size=A_size) + + # Create the hierarchical sheet for insertion into the calling node sheet. + bbox = (node.bbox * node.tx * sheet_tx).round() + time_hex = hex(int(time.time()))[2:] + return "\n".join( + ( + "$Sheet", + "S {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.w, bbox.h), + "U {}".format(time_hex), + 'F0 "{}" {}'.format(node.name, node.name_sz), + 'F1 "{}" {}'.format(node.sheet_filename, node.filename_sz), + "$EndSheet", + "", + ) + ) + + +""" +Generate a KiCad EESCHEMA schematic from a Circuit object. +""" + +class has_symbol_layout_data(Trait): + tx: Tx + orientation_locked: bool + + +class has_pin_layout_data(Trait): + pt: Point + routed: bool + + +def _add_data_trait[T: Trait](node: Node, trait: type[T]) -> T: + """Helper to shave down boilerplate for adding data-traits to nodes""" + class Impl(trait.impl()): + pass + + return node.add(Impl()) + + +def preprocess_circuit(circuit: Graph, **options): + """Add stuff to parts & nets for doing placement and routing of schematics.""" + + def units(part: Module): + return [part] + # TODO: handle units within parts + # if len(part.unit) == 0: + # return [part] + # else: + # return part.unit.values() + + def initialize(part: Module): + """Initialize part or its part units.""" + + # Initialize the units of the part, or the part itself if it has no units. + pin_limit = options.get("orientation_pin_limit", 44) + for part_unit in units(part): + cmp_data_trait = _add_data_trait(part, has_symbol_layout_data) + + # Initialize transform matrix. + layout_data = part_unit.get_trait(F.has_symbol_layout) or part_unit.add(F.has_symbol_layout_defined()) + cmp_data_trait.tx = Tx.from_symtx(layout_data.translations) + + # Lock part orientation if symtx was specified. Also lock parts with a lot of pins + # since they're typically drawn the way they're supposed to be oriented. + # And also lock single-pin parts because these are usually power/ground and + # they shouldn't be flipped around. + num_pins = len(part_unit.get_children(direct_only=True, types=F.Symbol.Pin)) + cmp_data_trait.orientation_locked = bool(layout_data.translations) or not ( + 1 < num_pins <= pin_limit + ) + + # Initialize pin attributes used for generating schematics. + for pin in part_unit.get_children(direct_only=True, types=F.Symbol.Pin): + pin_data_trait = _add_data_trait(pin, has_pin_layout_data) + lib_pin = SchTransformer.get_lib_pin(pin) + # TODO: what to do with pin rotation? + pin_data_trait.pt = Point(lib_pin.at.x, lib_pin.at.y) + pin_data_trait.routed = False + + def rotate_power_pins(part: Module): + """Rotate a part based on the direction of its power pins. + + This function is to make sure that voltage sources face up and gnd pins + face down. + """ + + # Don't rotate parts that are already explicitly rotated/flipped. + if part.get_trait(F.has_symbol_layout).translations: + return + + def is_pwr(net: F.Electrical): + F.ElectricPower + + def is_gnd(net): + return "gnd" in net_name.lower() + + dont_rotate_pin_cnt = options.get("dont_rotate_pin_count", 10000) + + for part_unit in units(part): + # Don't rotate parts with too many pins. + if len(part_unit) > dont_rotate_pin_cnt: + return + + # Tally what rotation would make each pwr/gnd pin point up or down. + rotation_tally = Counter() + for pin in part_unit: + net_name = getattr(pin.net, "name", "").lower() + if is_gnd(net_name): + if pin.orientation == "U": + rotation_tally[0] += 1 + if pin.orientation == "D": + rotation_tally[180] += 1 + if pin.orientation == "L": + rotation_tally[90] += 1 + if pin.orientation == "R": + rotation_tally[270] += 1 + elif is_pwr(net_name): + if pin.orientation == "D": + rotation_tally[0] += 1 + if pin.orientation == "U": + rotation_tally[180] += 1 + if pin.orientation == "L": + rotation_tally[270] += 1 + if pin.orientation == "R": + rotation_tally[90] += 1 + + # Rotate the part unit in the direction with the most tallies. + try: + rotation = rotation_tally.most_common()[0][0] + except IndexError: + pass + else: + # Rotate part unit 90-degrees clockwise until the desired rotation is reached. + tx_cw_90 = Tx(a=0, b=-1, c=1, d=0) # 90-degree trans. matrix. + for _ in range(int(round(rotation / 90))): + part_unit.tx = part_unit.tx * tx_cw_90 + + def calc_part_bbox(part: Module): + """Calculate the labeled bounding boxes and store it in the part.""" + + # Find part/unit bounding boxes excluding any net labels on pins. + # TODO: part.lbl_bbox could be substituted for part.bbox. + # TODO: Part ref and value should be updated before calculating bounding box. + bare_bboxes = calc_symbol_bbox(part)[1:] + + for part_unit, bare_bbox in zip(units(part), bare_bboxes): + # Expand the bounding box if it's too small in either dimension. + resize_wh = Vector(0, 0) + if bare_bbox.w < 100: + resize_wh.x = (100 - bare_bbox.w) / 2 + if bare_bbox.h < 100: + resize_wh.y = (100 - bare_bbox.h) / 2 + bare_bbox = bare_bbox.resize(resize_wh) + + # Find expanded bounding box that includes any hier labels attached to pins. + part_unit.lbl_bbox = BBox() + part_unit.lbl_bbox.add(bare_bbox) + for pin in part_unit: + if pin.stub: + # Find bounding box for net stub label attached to pin. + hlbl_bbox = calc_hier_label_bbox(pin.net.name, pin.orientation) + # Move the label bbox to the pin location. + hlbl_bbox *= Tx().move(pin.pt) + # Update the bbox for the labelled part with this pin label. + part_unit.lbl_bbox.add(hlbl_bbox) + + # Set the active bounding box to the labeled version. + part_unit.bbox = part_unit.lbl_bbox + + # Pre-process parts + # TODO: complete criteria on what schematic symbols we can handle + for part, has_symbol_trait in circuit.nodes_with_trait(F.has_symbol): + symbol = has_symbol_trait.get_symbol() + # Initialize part attributes used for generating schematics. + initialize(symbol) + + # Rotate parts. Power pins should face up. GND pins should face down. + rotate_power_pins(symbol) + + # Compute bounding boxes around parts + calc_part_bbox(symbol) + + +def finalize_parts_and_nets(circuit, **options): + """Restore parts and nets after place & route is done.""" + + # Remove any NetTerminals that were added. + net_terminals = (p for p in circuit.parts if isinstance(p, NetTerminal)) + circuit.rmv_parts(*net_terminals) + + # Return pins from the part units to their parent part. + for part in circuit.parts: + part.grab_pins() + + # Remove some stuff added to parts during schematic generation process. + rmv_attr(circuit.parts, ("force", "bbox", "lbl_bbox", "tx")) + + +def gen_schematic( + circuit, + filepath=".", + top_name=get_script_name(), + title="SKiDL-Generated Schematic", + flatness=0.0, + retries=2, + **options, +): + """Create a schematic file from a Circuit object. + + Args: + circuit (Circuit): The Circuit object that will have a schematic generated for it. + filepath (str, optional): The directory where the schematic files are placed. Defaults to ".". + top_name (str, optional): The name for the top of the circuit hierarchy. Defaults to get_script_name(). + title (str, optional): The title of the schematic. Defaults to "SKiDL-Generated Schematic". + flatness (float, optional): Determines how much the hierarchy is flattened in the schematic. Defaults to 0.0 (completely hierarchical). + retries (int, optional): Number of times to re-try if routing fails. Defaults to 2. + options (dict, optional): Dict of options and values, usually for drawing/debugging. + """ + + from skidl import KICAD + from skidl.schematics.node import Node + from skidl.tools import tool_modules + + from .place import PlacementFailure + from .route import RoutingFailure + + # Part placement options that should always be turned on. + options["use_push_pull"] = True + options["rotate_parts"] = True + options["pt_to_pt_mult"] = 5 # HACK: Ad-hoc value. + options["pin_normalize"] = True + + # Start with default routing area. + expansion_factor = 1.0 + + # Try to place & route one or more times. + for _ in range(retries): + preprocess_circuit(circuit, **options) + + node = Node(circuit, tool_modules[KICAD], filepath, top_name, title, flatness) + + try: + # Place parts. + node.place(expansion_factor=expansion_factor, **options) + + # Route parts. + node.route(**options) + + except PlacementFailure: + # Placement failed, so clean up ... + finalize_parts_and_nets(circuit, **options) + # ... and try again. + continue + + except RoutingFailure: + # Routing failed, so clean up ... + finalize_parts_and_nets(circuit, **options) + # ... and expand routing area ... + expansion_factor *= 1.5 # HACK: Ad-hoc increase of expansion factor. + # ... and try again. + continue + + # Generate EESCHEMA code for the schematic. + node_to_eeschema(node) + + # Append place & route statistics for the schematic to a file. + if options.get("collect_stats"): + stats = node.collect_stats(**options) + with open(options["stats_file"], "a") as f: + f.write(stats) + + # Clean up. + finalize_parts_and_nets(circuit, **options) + + # Place & route was successful if we got here, so exit. + return + + # Append failed place & route statistics for the schematic to a file. + if options.get("collect_stats"): + stats = "-1\n" + with open(options["stats_file"], "a") as f: + f.write(stats) + + # Clean-up after failure. + finalize_parts_and_nets(circuit, **options) + + # Exited the loop without successful routing. + raise (RoutingFailure) From 65f81a0f4ad3084603e990577415f3c01a2b452d Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 12:32:36 +0200 Subject: [PATCH 02/85] Add geometry bbox methods to sch transformer --- .../exporters/schematic/kicad/transformer.py | 81 ++++++++++++++++++- src/faebryk/libs/sexp/dataclass_sexp.py | 8 ++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 0aa9ea6b..88383c8c 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -4,7 +4,7 @@ import logging import pprint from copy import deepcopy -from functools import singledispatch +from functools import singledispatchmethod from itertools import chain, groupby from os import PathLike from pathlib import Path @@ -270,7 +270,7 @@ def get_related_lib_sym_units( groups = groupby(lib_sym.symbols.items(), key=lambda item: int(item[0][-1])) return {k: [v[1] for v in vs] for k, vs in groups} - @singledispatch + @singledispatchmethod def get_lib_symbol(self, sym) -> SCH.C_lib_symbols.C_symbol: raise NotImplementedError(f"Don't know how to get lib symbol for {type(sym)}") @@ -283,7 +283,7 @@ def _(self, sym: F.Symbol) -> SCH.C_lib_symbols.C_symbol: def _(self, sym: SCH.C_symbol_instance) -> SCH.C_lib_symbols.C_symbol: return self.sch.lib_symbols.symbols[sym.lib_id] - @singledispatch + @singledispatchmethod def get_lib_pin(self, pin) -> SCH.C_lib_symbols.C_symbol.C_symbol.C_pin: raise NotImplementedError(f"Don't know how to get lib pin for {type(pin)}") @@ -437,3 +437,78 @@ def insert_symbol( self.attach_symbol(module, unit_instance) self.sch.symbols.append(unit_instance) + + # Bounding boxes ---------------------------------------------------------------- + type BoundingBox = tuple[Geometry.Point2D, Geometry.Point2D] + + @singledispatchmethod + @staticmethod + def get_bbox(obj) -> BoundingBox: + """ + Get the bounding box of the object in it's reference frame + This means that for things like pins, which know their own position, + the bbox returned will include the offset of the pin. + """ + raise NotImplementedError(f"Don't know how to get bbox for {type(obj)}") + + @get_bbox.register + @staticmethod + def _(obj: C_arc) -> BoundingBox: + return Geometry.bbox( + Geometry.approximate_arc(obj.start, obj.mid, obj.end), + tolerance=obj.stroke.width, + ) + + @get_bbox.register + @staticmethod + def _(obj: C_polyline | C_rect) -> BoundingBox: + return Geometry.bbox( + ((pt.x, pt.y) for pt in obj.pts.xys), + tolerance=obj.stroke.width, + ) + + @get_bbox.register + @staticmethod + def _(obj: C_circle) -> BoundingBox: + radius = Geometry.distance_euclid(obj.center, obj.end) + return Geometry.bbox( + (obj.center.x - radius, obj.center.y - radius), + (obj.center.x + radius, obj.center.y + radius), + tolerance=obj.stroke.width, + ) + + @get_bbox.register + def _(self, pin: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin) -> BoundingBox: + # TODO: include the name and number in the bbox + start = (pin.at.x, pin.at.y) + end = Geometry.rotate(start, [(pin.at.x + pin.length, pin.at.y)], pin.at.r)[0] + return Geometry.bbox([start, end]) + + @get_bbox.register + @classmethod + def _(cls, symbol: SCH.C_lib_symbols.C_symbol.C_symbol) -> BoundingBox: + return Geometry.bbox( + map( + cls.get_bbox, + chain( + symbol.arcs, + symbol.polylines, + symbol.circles, + symbol.rectangles, + symbol.pins, + ), + ) + ) + + @get_bbox.register + @classmethod + def _(cls, symbol: SCH.C_lib_symbols.C_symbol) -> BoundingBox: + return Geometry.bbox( + chain.from_iterable(cls.get_bbox(unit) for unit in symbol.symbols.values()) + ) + + @get_bbox.register + def _(self, symbol: SCH.C_symbol_instance) -> BoundingBox: + # FIXME: this requires context to get the lib symbol, + # which means it must be called with self + return Geometry.abs_pos(self.get_bbox(self.get_lib_symbol(symbol))) diff --git a/src/faebryk/libs/sexp/dataclass_sexp.py b/src/faebryk/libs/sexp/dataclass_sexp.py index 4632df19..1c213f9a 100644 --- a/src/faebryk/libs/sexp/dataclass_sexp.py +++ b/src/faebryk/libs/sexp/dataclass_sexp.py @@ -480,6 +480,14 @@ def dumps(self, path: PathLike | None = None): def dataclass_dfs(obj) -> Iterator[tuple[Any, list, list[str]]]: + """ + Iterates over all dataclass fields and their values. + + Yields tuples of: + - value of the field + - list of the objects leading to the value + - list of the names of the fields leading to the value + """ return _iterate_tree(obj, [], []) From 5059b162704c9a249fcd9606c8208b959bc45ddb Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 12:42:26 +0200 Subject: [PATCH 03/85] Make dataclass for options --- .../exporters/schematic/kicad/schematic.py | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/schematic.py b/src/faebryk/exporters/schematic/kicad/schematic.py index c3c7b519..a231f4aa 100644 --- a/src/faebryk/exporters/schematic/kicad/schematic.py +++ b/src/faebryk/exporters/schematic/kicad/schematic.py @@ -3,6 +3,7 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. +from dataclasses import dataclass import datetime import os.path import re @@ -26,14 +27,38 @@ # from skidl.utilities import rmv_attr - -__all__ = [] - """ Functions for generating a KiCad EESCHEMA schematic. """ +@dataclass +class Options: + allow_routing_failure: bool = False + compress_before_place: bool = False + dont_rotate_pin_count: int = 10000 + draw_assigned_terminals: bool = False + draw_font: str = "Arial" + draw_global_routing: bool = False + draw_placement: bool = False + draw_routing_channels: bool = False + draw_routing: bool = False + draw_scr: bool = True + draw_switchbox_boundary: bool = False + draw_switchbox_routing: bool = False + draw_tx: Tx = Tx() + expansion_factor: float = 1.0 + graphics_only: bool = False + net_normalize: bool = False + pin_normalize: bool = False + pt_to_pt_mult: float = 1.0 + rotate_parts: bool = False + seed: int = 0 + show_capacities: bool = False + terminal_evolution: str = "all_at_once" + use_push_pull: bool = True + + def bbox_to_eeschema(bbox, tx, name=None): """Create a bounding box using EESCHEMA graphic lines.""" From ee7b4a452da0e4bcebbf78ced8b6d32f5023883d Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 13:07:54 +0200 Subject: [PATCH 04/85] Tidy symbol layout traits --- src/faebryk/library/has_symbol_layout.py | 2 +- src/faebryk/library/has_symbol_layout_defined.py | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 src/faebryk/library/has_symbol_layout_defined.py diff --git a/src/faebryk/library/has_symbol_layout.py b/src/faebryk/library/has_symbol_layout.py index 9cbc2c1e..26ac4d67 100644 --- a/src/faebryk/library/has_symbol_layout.py +++ b/src/faebryk/library/has_symbol_layout.py @@ -4,5 +4,5 @@ import faebryk.library._F as F -class has_symbol_layout(F.Symbol.TraitT): +class has_symbol_layout(F.Symbol.TraitT.decless()): translations: str diff --git a/src/faebryk/library/has_symbol_layout_defined.py b/src/faebryk/library/has_symbol_layout_defined.py deleted file mode 100644 index e5d9bfd9..00000000 --- a/src/faebryk/library/has_symbol_layout_defined.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -import faebryk.library._F as F - - -class has_symbol_layout_defined(F.has_symbol_layout.impl()): - def __init__(self, translations: str = ""): - super().__init__() - self.translations = translations From 38d9b12f2c07dc3cf3dbf1da4f26db894d50deb5 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 15:32:24 +0200 Subject: [PATCH 05/85] Minor transformer cleanup --- .../exporters/schematic/kicad/transformer.py | 47 +++++++++---------- src/faebryk/library/Symbol.py | 3 +- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 88383c8c..8b0fe9d0 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -77,18 +77,13 @@ class _HasUUID(Protocol): # TODO: consider common transformer base class SchTransformer: - class has_linked_sch_symbol(Module.TraitT): - symbol: SCH.C_symbol_instance - class has_linked_sch_symbol_defined(has_linked_sch_symbol.impl()): + class has_linked_sch_symbol(Module.TraitT.decless()): def __init__(self, symbol: SCH.C_symbol_instance) -> None: super().__init__() self.symbol = symbol - class has_linked_pins(F.Symbol.Pin.TraitT): - pins: list[SCH.C_symbol_instance.C_pin] - - class has_linked_pins_defined(has_linked_pins.impl()): + class has_linked_sch_pins(F.Symbol.Pin.TraitT.decless()): def __init__( self, pins: list[SCH.C_symbol_instance.C_pin], @@ -104,7 +99,7 @@ def __init__( self.app = app self._symbol_files_index: dict[str, Path] = {} - self.missing_lib_symbols: list[SCH.C_lib_symbols.C_symbol] = [] + self.missing_symbols: list[SCH.C_lib_symbols.C_symbol] = [] self.dimensions = None @@ -126,8 +121,6 @@ def attach(self): (f.propertys["Reference"].value, f.lib_id): f for f in self.sch.symbols } for node, sym_trait in self.graph.nodes_with_trait(F.Symbol.has_symbol): - # FIXME: I believe this trait is used as a proxy for being a component - # since, names are replaced with designators during typical pipelines if not node.has_trait(F.has_overriden_name): continue @@ -139,14 +132,11 @@ def attach(self): sym_ref = node.get_trait(F.has_overriden_name).get_name() sym_name = symbol.get_trait(F.Symbol.has_kicad_symbol).symbol_name - try: - sym = symbols[(sym_ref, sym_name)] - except KeyError: - # TODO: add diag - self.missing_lib_symbols.append(symbol) + if (sym_ref, sym_name) not in symbols: + self.missing_symbols.append(symbol) continue - self.attach_symbol(node, sym) + self.attach_symbol(node, symbols[(sym_ref, sym_name)]) # Log what we were able to attach attached = { @@ -157,25 +147,23 @@ def attach(self): } logger.debug(f"Attached: {pprint.pformat(attached)}") - if self.missing_lib_symbols: + if self.missing_symbols: # TODO: just go look for the symbols instead raise ExceptionGroup( "Missing lib symbols", [ f"Symbol {sym.name} not found in symbols dictionary" - for sym in self.missing_lib_symbols + for sym in self.missing_symbols ], ) - def attach_symbol(self, node: Node, symbol: SCH.C_symbol_instance): + def attach_symbol(self, f_symbol: F.Symbol, sch_symbol: SCH.C_symbol_instance): """Bind the module and symbol together on the graph""" - graph_sym = node.get_trait(F.Symbol.has_symbol).reference - - graph_sym.add(self.has_linked_sch_symbol_defined(symbol)) + f_symbol.add(self.has_linked_sch_symbol(sch_symbol)) # Attach the pins on the symbol to the module interface - for pin_name, pins in groupby(symbol.pins, key=lambda p: p.name): - graph_sym.pins[pin_name].add(SchTransformer.has_linked_pins_defined(pins)) + for pin_name, pins in groupby(sch_symbol.pins, key=lambda p: p.name): + f_symbol.pins[pin_name].add(SchTransformer.has_linked_sch_pins(pins)) def cleanup(self): """Delete faebryk-created objects in schematic.""" @@ -297,7 +285,7 @@ def _(self, pin: F.Symbol.Pin) -> SCH.C_lib_symbols.C_symbol.C_symbol.C_pin: def _name_filter(sch_pin: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin): return sch_pin.name in { - p.name for p in pin.get_trait(self.has_linked_pins).pins + p.name for p in pin.get_trait(self.has_linked_sch_pins).pins } lib_pin = find( @@ -402,6 +390,13 @@ def insert_symbol( lib_sym = self._ensure_lib_symbol(lib_id) # insert all units + if len(self.get_related_lib_sym_units(lib_sym) > 1): + # problems today: + # - F.Symbol -> Module mapping + # - has_linked_sch_symbol mapping is currently 1:1 + # - has_kicad_symbol mapping is currently 1:1 + raise NotImplementedError("Multiple units not implemented") + for unit_key, unit_objs in self.get_related_lib_sym_units(lib_sym).items(): pins = [] @@ -434,7 +429,7 @@ def insert_symbol( # TODO: handle not having an overriden name better raise Exception(f"Module {module} has no overriden name") - self.attach_symbol(module, unit_instance) + self.attach_symbol(symbol, unit_instance) self.sch.symbols.append(unit_instance) diff --git a/src/faebryk/library/Symbol.py b/src/faebryk/library/Symbol.py index f6ec5d4d..9ff7d473 100644 --- a/src/faebryk/library/Symbol.py +++ b/src/faebryk/library/Symbol.py @@ -7,8 +7,7 @@ class Symbol(Module): """ - Symbols represent a symbol instance and are bi-directionally - linked with the module they represent via the `has_linked` trait. + A symbolic representation of a component. """ class Pin(ModuleInterface): From 4a9edc6bbee8d574892fcc5d8942b76a6e653ff2 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 15:33:28 +0200 Subject: [PATCH 06/85] Too many modifications to the original code. Trying another method --- .../exporters/schematic/kicad/bboxes.py | 19 +- .../exporters/schematic/kicad/draw_objs.py | 41 ++ .../exporters/schematic/kicad/place.py | 4 - .../schematic/kicad/{node.py => sch_node.py} | 71 +- .../exporters/schematic/kicad/schematic.py | 680 ++++++++---------- 5 files changed, 392 insertions(+), 423 deletions(-) create mode 100644 src/faebryk/exporters/schematic/kicad/draw_objs.py rename src/faebryk/exporters/schematic/kicad/{node.py => sch_node.py} (89%) diff --git a/src/faebryk/exporters/schematic/kicad/bboxes.py b/src/faebryk/exporters/schematic/kicad/bboxes.py index 88368986..5f883aab 100644 --- a/src/faebryk/exporters/schematic/kicad/bboxes.py +++ b/src/faebryk/exporters/schematic/kicad/bboxes.py @@ -6,26 +6,26 @@ Calculate bounding boxes for part symbols and hierarchical sheets. """ +import logging from collections import namedtuple -from skidl.logger import active_logger -from skidl.schematics.geometry import ( - Tx, +from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE +from .draw_objs import * +from .geometry import ( BBox, Point, + Tx, Vector, tx_rot_0, tx_rot_90, tx_rot_180, tx_rot_270, ) -from skidl.utilities import export_to_all -from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE -from skidl.schematics.geometry import BBox, Point, Tx, Vector -from .draw_objs import * + +logger = logging.getLogger(__name__) + -@export_to_all def calc_symbol_bbox(part, **options): """ Return the bounding box of the part symbol. @@ -270,7 +270,7 @@ def make_pin_dir_tbl(abs_xoff=20): obj_bbox.add(end) else: - active_logger.error( + logger.error( "Unknown graphical object {} in part symbol {}.".format( type(obj), part.name ) @@ -293,7 +293,6 @@ def make_pin_dir_tbl(abs_xoff=20): return unit_bboxes -@export_to_all def calc_hier_label_bbox(label, dir): """Calculate the bounding box for a hierarchical label. diff --git a/src/faebryk/exporters/schematic/kicad/draw_objs.py b/src/faebryk/exporters/schematic/kicad/draw_objs.py new file mode 100644 index 00000000..f4703981 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/draw_objs.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +KiCad 5 drawing objects. +""" + +from collections import namedtuple + + +DrawDef = namedtuple( + "DrawDef", + "name ref zero name_offset show_nums show_names num_units lock_units power_symbol", +) + +DrawF0 = namedtuple("DrawF0", "ref x y size orientation visibility halign valign") + +DrawF1 = namedtuple( + "DrawF1", "name x y size orientation visibility halign valign fieldname" +) + +DrawArc = namedtuple( + "DrawArc", + "cx cy radius start_angle end_angle unit dmg thickness fill startx starty endx endy", +) + +DrawCircle = namedtuple("DrawCircle", "cx cy radius unit dmg thickness fill") + +DrawPoly = namedtuple("DrawPoly", "point_count unit dmg thickness points fill") + +DrawRect = namedtuple("DrawRect", "x1 y1 x2 y2 unit dmg thickness fill") + +DrawText = namedtuple( + "DrawText", "angle x y size hidden unit dmg text italic bold halign valign" +) + +DrawPin = namedtuple( + "DrawPin", + "name num x y length orientation num_size name_size unit dmg electrical_type shape", +) diff --git a/src/faebryk/exporters/schematic/kicad/place.py b/src/faebryk/exporters/schematic/kicad/place.py index 38e7b1b0..3be76204 100644 --- a/src/faebryk/exporters/schematic/kicad/place.py +++ b/src/faebryk/exporters/schematic/kicad/place.py @@ -14,8 +14,6 @@ from collections import defaultdict from copy import copy -from skidl import Pin, rmv_attr - import faebryk.library._F as F from .debug_draw import ( @@ -66,8 +64,6 @@ class PlacementFailure(Exception): """Exception raised when parts or blocks could not be placed.""" - pass - # Small functions for summing Points and Vectors. pt_sum = lambda pts: sum(pts, Point(0, 0)) diff --git a/src/faebryk/exporters/schematic/kicad/node.py b/src/faebryk/exporters/schematic/kicad/sch_node.py similarity index 89% rename from src/faebryk/exporters/schematic/kicad/node.py rename to src/faebryk/exporters/schematic/kicad/sch_node.py index 9c658051..da8b690a 100644 --- a/src/faebryk/exporters/schematic/kicad/node.py +++ b/src/faebryk/exporters/schematic/kicad/sch_node.py @@ -1,3 +1,6 @@ +""" +Node class for storing circuit hierarchy. +""" # -*- coding: utf-8 -*- # The MIT License (MIT) - Copyright (c) Dave Vandenbout. @@ -6,19 +9,17 @@ from collections import defaultdict from itertools import chain -from skidl.utilities import export_to_all, rmv_attr +from faebryk.core.module import Module +import faebryk.library._F as F +from faebryk.core.graphinterface import Graph +from faebryk.core.node import Node + from .geometry import BBox, Point, Tx, Vector from .place import Placer from .route import Router -""" -Node class for storing circuit hierarchy. -""" - - -@export_to_all -class Node(Placer, Router): +class SchNode(Placer, Router, Node): """Data structure for holding information about a node in the circuit hierarchy.""" filename_sz = 20 @@ -26,8 +27,7 @@ class Node(Placer, Router): def __init__( self, - circuit=None, - tool_module=None, + circuit: Graph | None = None, filepath=".", top_name="", title="", @@ -35,16 +35,14 @@ def __init__( ): self.parent = None self.children = defaultdict( - lambda: Node(None, tool_module, filepath, top_name, title, flatness) + lambda: SchNode(None, top_name, title, flatness) ) - self.filepath = filepath self.top_name = top_name self.sheet_name = None self.sheet_filename = None self.title = title self.flatness = flatness self.flattened = False - self.tool_module = tool_module # Backend tool. self.parts = [] self.wires = defaultdict(list) self.junctions = defaultdict(list) @@ -54,41 +52,26 @@ def __init__( if circuit: self.add_circuit(circuit) - def find_node_with_part(self, part): - """Find the node that contains the part based on its hierarchy. + class _HasSchNode(Module.TraitT.decless(())): + def __init__(self, node: "SchNode"): + self.node = node - Args: - part (Part): The part being searched for in the node hierarchy. + def find_node_with_part(self, part: F.Symbol) -> "SchNode": + return part.get_trait(self._HasSchNode).node - Returns: - Node: The Node object containing the part. - """ - - from skidl.circuit import HIER_SEP - - level_names = part.hierarchy.split(HIER_SEP) - node = self - for lvl_nm in level_names[1:]: - node = node.children[lvl_nm] - assert part in node.parts - return node - - def add_circuit(self, circuit): - """Add parts in circuit to node and its children. - - Args: - circuit (Circuit): Circuit object. - """ + def add_circuit(self, circuit: Graph): + """Add parts in circuit to node and its children.""" # Build the circuit node hierarchy by adding the parts. - for part in circuit.parts: + for part, trait in circuit.nodes_with_trait(F.Symbol.has_symbol): self.add_part(part) # Add terminals to nodes in the hierarchy for nets that span across nodes. for net in circuit.nets: + # TODO: # Skip nets that are stubbed since there will be no wire to attach to the NetTerminal. - if getattr(net, "stub", False): - continue + # if getattr(net, "stub", False): + # continue # Search for pins in different nodes. for pin1, pin2 in zip(net.pins[:-1], net.pins[1:]): @@ -130,14 +113,8 @@ def add_circuit(self, circuit): self.flatten(self.flatness) def add_part(self, part, level=0): - """Add a part to the node at the appropriate level of the hierarchy. + """Add a part to the node at the appropriate level of the hierarchy.""" - Args: - part (Part): Part to be added to this node or one of its children. - level (int, optional): The current level (depth) of the node in the hierarchy. Defaults to 0. - """ - - from skidl.circuit import HIER_SEP # Get list of names of hierarchical levels (in order) leading to this part. level_names = part.hierarchy.split(HIER_SEP) @@ -180,8 +157,6 @@ def add_terminal(self, net): Args: net (Net): The net to be added to this node. """ - - from skidl.circuit import HIER_SEP from .net_terminal import NetTerminal nt = NetTerminal(net, self.tool_module) diff --git a/src/faebryk/exporters/schematic/kicad/schematic.py b/src/faebryk/exporters/schematic/kicad/schematic.py index a231f4aa..ce49c2e4 100644 --- a/src/faebryk/exporters/schematic/kicad/schematic.py +++ b/src/faebryk/exporters/schematic/kicad/schematic.py @@ -3,20 +3,21 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. -from dataclasses import dataclass import datetime import os.path import re import time from collections import Counter, OrderedDict +from dataclasses import dataclass +from typing import TypedDict, Unpack +from faebryk.exporters.schematic.kicad.sch_node import SchNode import faebryk.library._F as F from faebryk.core.graphinterface import Graph from faebryk.core.module import Module from faebryk.core.node import Node from faebryk.core.trait import Trait -from faebryk.exporters.schematic.kicad.transformer import SchTransformer -from faebryk.libs.util import cast_assert +from faebryk.exporters.schematic.kicad.transformer import SCH, SchTransformer from .bboxes import calc_hier_label_bbox, calc_symbol_bbox from .constants import BLK_INT_PAD, BOX_LABEL_FONT_SIZE, GRID, PIN_LABEL_FONT_SIZE @@ -33,7 +34,7 @@ @dataclass -class Options: +class Options(TypedDict): allow_routing_failure: bool = False compress_before_place: bool = False dont_rotate_pin_count: int = 10000 @@ -59,260 +60,260 @@ class Options: use_push_pull: bool = True -def bbox_to_eeschema(bbox, tx, name=None): - """Create a bounding box using EESCHEMA graphic lines.""" - - # Make sure the box corners are integers. - bbox = (bbox * tx).round() - - graphic_box = [] - - if name: - # Place name at the lower-left corner of the box. - name_pt = bbox.ul - graphic_box.append( - "Text Notes {} {} 0 {} ~ 20\n{}".format( - name_pt.x, name_pt.y, BOX_LABEL_FONT_SIZE, name - ) - ) - - graphic_box.append("Wire Notes Line") - graphic_box.append( - " {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.lr.x, bbox.lr.y) - ) - graphic_box.append("Wire Notes Line") - graphic_box.append( - " {} {} {} {}".format(bbox.lr.x, bbox.lr.y, bbox.ur.x, bbox.ur.y) - ) - graphic_box.append("Wire Notes Line") - graphic_box.append( - " {} {} {} {}".format(bbox.ur.x, bbox.ur.y, bbox.ul.x, bbox.ul.y) - ) - graphic_box.append("Wire Notes Line") - graphic_box.append( - " {} {} {} {}".format(bbox.ul.x, bbox.ul.y, bbox.ll.x, bbox.ll.y) - ) - graphic_box.append("") # For blank line at end. - - return "\n".join(graphic_box) - - -def net_to_eeschema(self, tx): - """Generate the EESCHEMA code for the net terminal. - - Args: - tx (Tx): Transformation matrix for the node containing this net terminal. - - Returns: - str: EESCHEMA code string. - """ - self.pins[0].stub = True - self.pins[0].orientation = "R" - return pin_label_to_eeschema(self.pins[0], tx) - # return pin_label_to_eeschema(self.pins[0], tx) + bbox_to_eeschema(self.bbox, self.tx * tx) - - -def part_to_eeschema(part, tx): - """Create EESCHEMA code for a part. - - Args: - part (Part): SKiDL part. - tx (Tx): Transformation matrix. - - Returns: - string: EESCHEMA code for the part. - - Notes: - https://en.wikibooks.org/wiki/Kicad/file_formats#Schematic_Files_Format - """ - - tx = part.tx * tx - origin = tx.origin.round() - time_hex = hex(int(time.time()))[2:] - unit_num = getattr(part, "num", 1) - - eeschema = [] - eeschema.append("$Comp") - lib = os.path.splitext(part.lib.filename)[0] - eeschema.append("L {}:{} {}".format(lib, part.name, part.ref)) - eeschema.append("U {} 1 {}".format(unit_num, time_hex)) - eeschema.append("P {} {}".format(str(origin.x), str(origin.y))) - - # Add part symbols. For now we are only adding the designator - n_F0 = 1 - for i in range(len(part.draw)): - if re.search("^DrawF0", str(part.draw[i])): - n_F0 = i - break - eeschema.append( - 'F 0 "{}" {} {} {} {} {} {} {}'.format( - part.ref, - part.draw[n_F0].orientation, - str(origin.x + part.draw[n_F0].x), - str(origin.y + part.draw[n_F0].y), - part.draw[n_F0].size, - "000", # TODO: Refine this to match part def. - part.draw[n_F0].halign, - part.draw[n_F0].valign, - ) - ) - - # Part value. - n_F1 = 1 - for i in range(len(part.draw)): - if re.search("^DrawF1", str(part.draw[i])): - n_F1 = i - break - eeschema.append( - 'F 1 "{}" {} {} {} {} {} {} {}'.format( - str(part.value), - part.draw[n_F1].orientation, - str(origin.x + part.draw[n_F1].x), - str(origin.y + part.draw[n_F1].y), - part.draw[n_F1].size, - "000", # TODO: Refine this to match part def. - part.draw[n_F1].halign, - part.draw[n_F1].valign, - ) - ) - - # Part footprint. - n_F2 = 2 - for i in range(len(part.draw)): - if re.search("^DrawF2", str(part.draw[i])): - n_F2 = i - break - eeschema.append( - 'F 2 "{}" {} {} {} {} {} {} {}'.format( - part.footprint, - part.draw[n_F2].orientation, - str(origin.x + part.draw[n_F2].x), - str(origin.y + part.draw[n_F2].y), - part.draw[n_F2].size, - "001", # TODO: Refine this to match part def. - part.draw[n_F2].halign, - part.draw[n_F2].valign, - ) - ) - eeschema.append(" 1 {} {}".format(str(origin.x), str(origin.y))) - eeschema.append(" {} {} {} {}".format(tx.a, tx.b, tx.c, tx.d)) - eeschema.append("$EndComp") - eeschema.append("") # For blank line at end. - - # For debugging: draws a bounding box around a part. - # eeschema.append(bbox_to_eeschema(part.bbox, tx)) - # eeschema.append(bbox_to_eeschema(part.place_bbox, tx)) - - return "\n".join(eeschema) - - -def wire_to_eeschema(net, wire, tx): - """Create EESCHEMA code for a multi-segment wire. - - Args: - net (Net): Net associated with the wire. - wire (list): List of Segments for a wire. - tx (Tx): transformation matrix for each point in the wire. - - Returns: - string: Text to be placed into EESCHEMA file. - """ - - eeschema = [] - for segment in wire: - eeschema.append("Wire Wire Line") - w = (segment * tx).round() - eeschema.append(" {} {} {} {}".format(w.p1.x, w.p1.y, w.p2.x, w.p2.y)) - eeschema.append("") # For blank line at end. - return "\n".join(eeschema) - - -def junction_to_eeschema(net, junctions, tx): - eeschema = [] - for junction in junctions: - pt = (junction * tx).round() - eeschema.append("Connection ~ {} {}".format(pt.x, pt.y)) - eeschema.append("") # For blank line at end. - return "\n".join(eeschema) - - -def power_part_to_eeschema(part, tx=Tx()): - return "" # REMOVE: Remove this. - out = [] - for pin in part.pins: - try: - if not (pin.net is None): - if pin.net.netclass == "Power": - # strip out the '_...' section from power nets - t = pin.net.name - u = t.split("_") - symbol_name = u[0] - # find the stub in the part - time_hex = hex(int(time.time()))[2:] - pin_pt = (part.origin + offset + Point(pin.x, pin.y)).round() - x, y = pin_pt.x, pin_pt.y - out.append("$Comp\n") - out.append("L power:{} #PWR?\n".format(symbol_name)) - out.append("U 1 1 {}\n".format(time_hex)) - out.append("P {} {}\n".format(str(x), str(y))) - # Add part symbols. For now we are only adding the designator - n_F0 = 1 - for i in range(len(part.draw)): - if re.search("^DrawF0", str(part.draw[i])): - n_F0 = i - break - part_orientation = part.draw[n_F0].orientation - part_horizontal_align = part.draw[n_F0].halign - part_vertical_align = part.draw[n_F0].valign - - # check if the pin orientation will clash with the power part - if "+" in symbol_name: - # voltage sources face up, so check if the pin is facing down (opposite logic y-axis) - if pin.orientation == "U": - orientation = [-1, 0, 0, 1] - elif "gnd" in symbol_name.lower(): - # gnd points down so check if the pin is facing up (opposite logic y-axis) - if pin.orientation == "D": - orientation = [-1, 0, 0, 1] - out.append( - 'F 0 "{}" {} {} {} {} {} {} {}\n'.format( - "#PWR?", - part_orientation, - str(x + 25), - str(y + 25), - str(40), - "001", - part_horizontal_align, - part_vertical_align, - ) - ) - out.append( - 'F 1 "{}" {} {} {} {} {} {} {}\n'.format( - symbol_name, - part_orientation, - str(x + 25), - str(y + 25), - str(40), - "000", - part_horizontal_align, - part_vertical_align, - ) - ) - out.append(" 1 {} {}\n".format(str(x), str(y))) - out.append( - " {} {} {} {}\n".format( - orientation[0], - orientation[1], - orientation[2], - orientation[3], - ) - ) - out.append("$EndComp\n") - except Exception as inst: - print(type(inst)) - print(inst.args) - print(inst) - return "\n" + "".join(out) +# def bbox_to_eeschema(bbox, tx, name=None): +# """Create a bounding box using EESCHEMA graphic lines.""" + +# # Make sure the box corners are integers. +# bbox = (bbox * tx).round() + +# graphic_box = [] + +# if name: +# # Place name at the lower-left corner of the box. +# name_pt = bbox.ul +# graphic_box.append( +# "Text Notes {} {} 0 {} ~ 20\n{}".format( +# name_pt.x, name_pt.y, BOX_LABEL_FONT_SIZE, name +# ) +# ) + +# graphic_box.append("Wire Notes Line") +# graphic_box.append( +# " {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.lr.x, bbox.lr.y) +# ) +# graphic_box.append("Wire Notes Line") +# graphic_box.append( +# " {} {} {} {}".format(bbox.lr.x, bbox.lr.y, bbox.ur.x, bbox.ur.y) +# ) +# graphic_box.append("Wire Notes Line") +# graphic_box.append( +# " {} {} {} {}".format(bbox.ur.x, bbox.ur.y, bbox.ul.x, bbox.ul.y) +# ) +# graphic_box.append("Wire Notes Line") +# graphic_box.append( +# " {} {} {} {}".format(bbox.ul.x, bbox.ul.y, bbox.ll.x, bbox.ll.y) +# ) +# graphic_box.append("") # For blank line at end. + +# return "\n".join(graphic_box) + + +# def net_to_eeschema(self, tx): +# """Generate the EESCHEMA code for the net terminal. + +# Args: +# tx (Tx): Transformation matrix for the node containing this net terminal. + +# Returns: +# str: EESCHEMA code string. +# """ +# self.pins[0].stub = True +# self.pins[0].orientation = "R" +# return pin_label_to_eeschema(self.pins[0], tx) +# # return pin_label_to_eeschema(self.pins[0], tx) + bbox_to_eeschema(self.bbox, self.tx * tx) + + +# def part_to_eeschema(part, tx): +# """Create EESCHEMA code for a part. + +# Args: +# part (Part): SKiDL part. +# tx (Tx): Transformation matrix. + +# Returns: +# string: EESCHEMA code for the part. + +# Notes: +# https://en.wikibooks.org/wiki/Kicad/file_formats#Schematic_Files_Format +# """ + +# tx = part.tx * tx +# origin = tx.origin.round() +# time_hex = hex(int(time.time()))[2:] +# unit_num = getattr(part, "num", 1) + +# eeschema = [] +# eeschema.append("$Comp") +# lib = os.path.splitext(part.lib.filename)[0] +# eeschema.append("L {}:{} {}".format(lib, part.name, part.ref)) +# eeschema.append("U {} 1 {}".format(unit_num, time_hex)) +# eeschema.append("P {} {}".format(str(origin.x), str(origin.y))) + +# # Add part symbols. For now we are only adding the designator +# n_F0 = 1 +# for i in range(len(part.draw)): +# if re.search("^DrawF0", str(part.draw[i])): +# n_F0 = i +# break +# eeschema.append( +# 'F 0 "{}" {} {} {} {} {} {} {}'.format( +# part.ref, +# part.draw[n_F0].orientation, +# str(origin.x + part.draw[n_F0].x), +# str(origin.y + part.draw[n_F0].y), +# part.draw[n_F0].size, +# "000", # TODO: Refine this to match part def. +# part.draw[n_F0].halign, +# part.draw[n_F0].valign, +# ) +# ) + +# # Part value. +# n_F1 = 1 +# for i in range(len(part.draw)): +# if re.search("^DrawF1", str(part.draw[i])): +# n_F1 = i +# break +# eeschema.append( +# 'F 1 "{}" {} {} {} {} {} {} {}'.format( +# str(part.value), +# part.draw[n_F1].orientation, +# str(origin.x + part.draw[n_F1].x), +# str(origin.y + part.draw[n_F1].y), +# part.draw[n_F1].size, +# "000", # TODO: Refine this to match part def. +# part.draw[n_F1].halign, +# part.draw[n_F1].valign, +# ) +# ) + +# # Part footprint. +# n_F2 = 2 +# for i in range(len(part.draw)): +# if re.search("^DrawF2", str(part.draw[i])): +# n_F2 = i +# break +# eeschema.append( +# 'F 2 "{}" {} {} {} {} {} {} {}'.format( +# part.footprint, +# part.draw[n_F2].orientation, +# str(origin.x + part.draw[n_F2].x), +# str(origin.y + part.draw[n_F2].y), +# part.draw[n_F2].size, +# "001", # TODO: Refine this to match part def. +# part.draw[n_F2].halign, +# part.draw[n_F2].valign, +# ) +# ) +# eeschema.append(" 1 {} {}".format(str(origin.x), str(origin.y))) +# eeschema.append(" {} {} {} {}".format(tx.a, tx.b, tx.c, tx.d)) +# eeschema.append("$EndComp") +# eeschema.append("") # For blank line at end. + +# # For debugging: draws a bounding box around a part. +# # eeschema.append(bbox_to_eeschema(part.bbox, tx)) +# # eeschema.append(bbox_to_eeschema(part.place_bbox, tx)) + +# return "\n".join(eeschema) + + +# def wire_to_eeschema(net, wire, tx): +# """Create EESCHEMA code for a multi-segment wire. + +# Args: +# net (Net): Net associated with the wire. +# wire (list): List of Segments for a wire. +# tx (Tx): transformation matrix for each point in the wire. + +# Returns: +# string: Text to be placed into EESCHEMA file. +# """ + +# eeschema = [] +# for segment in wire: +# eeschema.append("Wire Wire Line") +# w = (segment * tx).round() +# eeschema.append(" {} {} {} {}".format(w.p1.x, w.p1.y, w.p2.x, w.p2.y)) +# eeschema.append("") # For blank line at end. +# return "\n".join(eeschema) + + +# def junction_to_eeschema(net, junctions, tx): +# eeschema = [] +# for junction in junctions: +# pt = (junction * tx).round() +# eeschema.append("Connection ~ {} {}".format(pt.x, pt.y)) +# eeschema.append("") # For blank line at end. +# return "\n".join(eeschema) + + +# def power_part_to_eeschema(part, tx=Tx()): +# return "" # REMOVE: Remove this. +# out = [] +# for pin in part.pins: +# try: +# if not (pin.net is None): +# if pin.net.netclass == "Power": +# # strip out the '_...' section from power nets +# t = pin.net.name +# u = t.split("_") +# symbol_name = u[0] +# # find the stub in the part +# time_hex = hex(int(time.time()))[2:] +# pin_pt = (part.origin + offset + Point(pin.x, pin.y)).round() +# x, y = pin_pt.x, pin_pt.y +# out.append("$Comp\n") +# out.append("L power:{} #PWR?\n".format(symbol_name)) +# out.append("U 1 1 {}\n".format(time_hex)) +# out.append("P {} {}\n".format(str(x), str(y))) +# # Add part symbols. For now we are only adding the designator +# n_F0 = 1 +# for i in range(len(part.draw)): +# if re.search("^DrawF0", str(part.draw[i])): +# n_F0 = i +# break +# part_orientation = part.draw[n_F0].orientation +# part_horizontal_align = part.draw[n_F0].halign +# part_vertical_align = part.draw[n_F0].valign + +# # check if the pin orientation will clash with the power part +# if "+" in symbol_name: +# # voltage sources face up, so check if the pin is facing down (opposite logic y-axis) +# if pin.orientation == "U": +# orientation = [-1, 0, 0, 1] +# elif "gnd" in symbol_name.lower(): +# # gnd points down so check if the pin is facing up (opposite logic y-axis) +# if pin.orientation == "D": +# orientation = [-1, 0, 0, 1] +# out.append( +# 'F 0 "{}" {} {} {} {} {} {} {}\n'.format( +# "#PWR?", +# part_orientation, +# str(x + 25), +# str(y + 25), +# str(40), +# "001", +# part_horizontal_align, +# part_vertical_align, +# ) +# ) +# out.append( +# 'F 1 "{}" {} {} {} {} {} {} {}\n'.format( +# symbol_name, +# part_orientation, +# str(x + 25), +# str(y + 25), +# str(40), +# "000", +# part_horizontal_align, +# part_vertical_align, +# ) +# ) +# out.append(" 1 {} {}\n".format(str(x), str(y))) +# out.append( +# " {} {} {} {}\n".format( +# orientation[0], +# orientation[1], +# orientation[2], +# orientation[3], +# ) +# ) +# out.append("$EndComp\n") +# except Exception as inst: +# print(type(inst)) +# print(inst.args) +# print(inst) +# return "\n" + "".join(out) # Sizes of EESCHEMA schematic pages from smallest to largest. Dimensions in mils. @@ -553,12 +554,12 @@ def node_to_eeschema(node, sheet_tx=Tx()): Generate a KiCad EESCHEMA schematic from a Circuit object. """ -class has_symbol_layout_data(Trait): +class has_symbol_layout_data(Trait.decless()): tx: Tx orientation_locked: bool -class has_pin_layout_data(Trait): +class has_pin_layout_data(Trait.decless()): pt: Point routed: bool @@ -571,47 +572,44 @@ class Impl(trait.impl()): return node.add(Impl()) -def preprocess_circuit(circuit: Graph, **options): +def preprocess_circuit(circuit: Graph, transformer: SchTransformer, **options: Unpack[Options]): """Add stuff to parts & nets for doing placement and routing of schematics.""" - def units(part: Module): + def units(part: F.Symbol) -> list[F.Symbol]: return [part] - # TODO: handle units within parts - # if len(part.unit) == 0: - # return [part] - # else: - # return part.unit.values() - def initialize(part: Module): + def initialize(part: F.Symbol): """Initialize part or its part units.""" # Initialize the units of the part, or the part itself if it has no units. pin_limit = options.get("orientation_pin_limit", 44) for part_unit in units(part): - cmp_data_trait = _add_data_trait(part, has_symbol_layout_data) + layout_data = part.add(has_symbol_layout_data()) # Initialize transform matrix. - layout_data = part_unit.get_trait(F.has_symbol_layout) or part_unit.add(F.has_symbol_layout_defined()) - cmp_data_trait.tx = Tx.from_symtx(layout_data.translations) + user_layout_data = part_unit.try_get_trait( + F.has_symbol_layout + ) or part_unit.add(F.has_symbol_layout()) + layout_data.tx = Tx.from_symtx(user_layout_data.translations) # Lock part orientation if symtx was specified. Also lock parts with a lot of pins # since they're typically drawn the way they're supposed to be oriented. # And also lock single-pin parts because these are usually power/ground and # they shouldn't be flipped around. num_pins = len(part_unit.get_children(direct_only=True, types=F.Symbol.Pin)) - cmp_data_trait.orientation_locked = bool(layout_data.translations) or not ( - 1 < num_pins <= pin_limit + layout_data.orientation_locked = bool(user_layout_data.translations) or ( + num_pins >= pin_limit ) # Initialize pin attributes used for generating schematics. for pin in part_unit.get_children(direct_only=True, types=F.Symbol.Pin): - pin_data_trait = _add_data_trait(pin, has_pin_layout_data) + pin_data = pin.add(has_pin_layout_data()) lib_pin = SchTransformer.get_lib_pin(pin) # TODO: what to do with pin rotation? - pin_data_trait.pt = Point(lib_pin.at.x, lib_pin.at.y) - pin_data_trait.routed = False + pin_data.pt = Point(lib_pin.at.x, lib_pin.at.y) + pin_data.routed = False - def rotate_power_pins(part: Module): + def rotate_power_pins(part: F.Symbol): """Rotate a part based on the direction of its power pins. This function is to make sure that voltage sources face up and gnd pins @@ -623,10 +621,14 @@ def rotate_power_pins(part: Module): return def is_pwr(net: F.Electrical): - F.ElectricPower + if power := net.get_parent_of_type(F.ElectricPower): + return net is power.hv + return False - def is_gnd(net): - return "gnd" in net_name.lower() + def is_gnd(net: F.Electrical): + if power := net.get_parent_of_type(F.ElectricPower): + return net is power.lv + return False dont_rotate_pin_cnt = options.get("dont_rotate_pin_count", 10000) @@ -637,9 +639,10 @@ def is_gnd(net): # Tally what rotation would make each pwr/gnd pin point up or down. rotation_tally = Counter() - for pin in part_unit: - net_name = getattr(pin.net, "name", "").lower() - if is_gnd(net_name): + for pin in part_unit.get_children(direct_only=True, types=F.Symbol.Pin): + lib_pin = transformer.get_lib_pin(pin) + lib_pin.at.r + if is_gnd(pin.represents): if pin.orientation == "U": rotation_tally[0] += 1 if pin.orientation == "D": @@ -648,7 +651,7 @@ def is_gnd(net): rotation_tally[90] += 1 if pin.orientation == "R": rotation_tally[270] += 1 - elif is_pwr(net_name): + elif is_pwr(pin.represents): if pin.orientation == "D": rotation_tally[0] += 1 if pin.orientation == "U": @@ -703,7 +706,7 @@ def calc_part_bbox(part: Module): # Pre-process parts # TODO: complete criteria on what schematic symbols we can handle - for part, has_symbol_trait in circuit.nodes_with_trait(F.has_symbol): + for part, has_symbol_trait in circuit.nodes_with_trait(F.Symbol.has_symbol): symbol = has_symbol_trait.get_symbol() # Initialize part attributes used for generating schematics. initialize(symbol) @@ -715,45 +718,20 @@ def calc_part_bbox(part: Module): calc_part_bbox(symbol) -def finalize_parts_and_nets(circuit, **options): - """Restore parts and nets after place & route is done.""" - - # Remove any NetTerminals that were added. - net_terminals = (p for p in circuit.parts if isinstance(p, NetTerminal)) - circuit.rmv_parts(*net_terminals) - - # Return pins from the part units to their parent part. - for part in circuit.parts: - part.grab_pins() - - # Remove some stuff added to parts during schematic generation process. - rmv_attr(circuit.parts, ("force", "bbox", "lbl_bbox", "tx")) - - def gen_schematic( - circuit, - filepath=".", - top_name=get_script_name(), - title="SKiDL-Generated Schematic", + circuit: Module, + transformer: SchTransformer, + top_name, + title="Faebryk Schematic", flatness=0.0, - retries=2, - **options, + **options: Unpack[Options], ): - """Create a schematic file from a Circuit object. - - Args: - circuit (Circuit): The Circuit object that will have a schematic generated for it. - filepath (str, optional): The directory where the schematic files are placed. Defaults to ".". - top_name (str, optional): The name for the top of the circuit hierarchy. Defaults to get_script_name(). - title (str, optional): The title of the schematic. Defaults to "SKiDL-Generated Schematic". - flatness (float, optional): Determines how much the hierarchy is flattened in the schematic. Defaults to 0.0 (completely hierarchical). - retries (int, optional): Number of times to re-try if routing fails. Defaults to 2. - options (dict, optional): Dict of options and values, usually for drawing/debugging. """ + Create a schematic - from skidl import KICAD - from skidl.schematics.node import Node - from skidl.tools import tool_modules + Recommendation on RoutingFailure: expansion_factor *= 1.5 + Recommendation on PlacementFailure: try again with a different random seed + """ from .place import PlacementFailure from .route import RoutingFailure @@ -767,56 +745,36 @@ def gen_schematic( # Start with default routing area. expansion_factor = 1.0 - # Try to place & route one or more times. - for _ in range(retries): - preprocess_circuit(circuit, **options) + preprocess_circuit(circuit, transformer, **options) - node = Node(circuit, tool_modules[KICAD], filepath, top_name, title, flatness) + node = SchNode(circuit, tool_modules[KICAD], filepath, top_name, title, flatness) - try: - # Place parts. - node.place(expansion_factor=expansion_factor, **options) + try: + # Place parts. + node.place(expansion_factor=expansion_factor, **options) - # Route parts. - node.route(**options) + # Route parts. + node.route(**options) - except PlacementFailure: - # Placement failed, so clean up ... - finalize_parts_and_nets(circuit, **options) - # ... and try again. - continue - - except RoutingFailure: - # Routing failed, so clean up ... - finalize_parts_and_nets(circuit, **options) - # ... and expand routing area ... - expansion_factor *= 1.5 # HACK: Ad-hoc increase of expansion factor. - # ... and try again. - continue - - # Generate EESCHEMA code for the schematic. - node_to_eeschema(node) - - # Append place & route statistics for the schematic to a file. - if options.get("collect_stats"): - stats = node.collect_stats(**options) - with open(options["stats_file"], "a") as f: - f.write(stats) + except PlacementFailure: + # Placement failed, so clean up ... + finalize_parts_and_nets(circuit, **options) + # ... and try again. + continue - # Clean up. + except RoutingFailure: + # Routing failed, so clean up ... finalize_parts_and_nets(circuit, **options) + # ... and expand routing area ... + expansion_factor *= 1.5 # HACK: Ad-hoc increase of expansion factor. + # ... and try again. + continue - # Place & route was successful if we got here, so exit. - return + # Generate EESCHEMA code for the schematic. + node_to_eeschema(node) - # Append failed place & route statistics for the schematic to a file. + # Append place & route statistics for the schematic to a file. if options.get("collect_stats"): - stats = "-1\n" + stats = node.collect_stats(**options) with open(options["stats_file"], "a") as f: f.write(stats) - - # Clean-up after failure. - finalize_parts_and_nets(circuit, **options) - - # Exited the loop without successful routing. - raise (RoutingFailure) From 2aaa9b9a08d3751cf39cf97340c78857773285e5 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 15:37:21 +0200 Subject: [PATCH 07/85] Revert "Too many modifications to the original code. Trying another method" This reverts commit b130c4e8daa66a36f055fb5b5ae77003886fef1e. --- .../exporters/schematic/kicad/bboxes.py | 19 +- .../exporters/schematic/kicad/draw_objs.py | 41 -- .../schematic/kicad/{sch_node.py => node.py} | 71 +- .../exporters/schematic/kicad/place.py | 4 + .../exporters/schematic/kicad/schematic.py | 680 ++++++++++-------- 5 files changed, 423 insertions(+), 392 deletions(-) delete mode 100644 src/faebryk/exporters/schematic/kicad/draw_objs.py rename src/faebryk/exporters/schematic/kicad/{sch_node.py => node.py} (89%) diff --git a/src/faebryk/exporters/schematic/kicad/bboxes.py b/src/faebryk/exporters/schematic/kicad/bboxes.py index 5f883aab..88368986 100644 --- a/src/faebryk/exporters/schematic/kicad/bboxes.py +++ b/src/faebryk/exporters/schematic/kicad/bboxes.py @@ -6,26 +6,26 @@ Calculate bounding boxes for part symbols and hierarchical sheets. """ -import logging from collections import namedtuple -from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE -from .draw_objs import * -from .geometry import ( +from skidl.logger import active_logger +from skidl.schematics.geometry import ( + Tx, BBox, Point, - Tx, Vector, tx_rot_0, tx_rot_90, tx_rot_180, tx_rot_270, ) - -logger = logging.getLogger(__name__) - +from skidl.utilities import export_to_all +from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE +from skidl.schematics.geometry import BBox, Point, Tx, Vector +from .draw_objs import * +@export_to_all def calc_symbol_bbox(part, **options): """ Return the bounding box of the part symbol. @@ -270,7 +270,7 @@ def make_pin_dir_tbl(abs_xoff=20): obj_bbox.add(end) else: - logger.error( + active_logger.error( "Unknown graphical object {} in part symbol {}.".format( type(obj), part.name ) @@ -293,6 +293,7 @@ def make_pin_dir_tbl(abs_xoff=20): return unit_bboxes +@export_to_all def calc_hier_label_bbox(label, dir): """Calculate the bounding box for a hierarchical label. diff --git a/src/faebryk/exporters/schematic/kicad/draw_objs.py b/src/faebryk/exporters/schematic/kicad/draw_objs.py deleted file mode 100644 index f4703981..00000000 --- a/src/faebryk/exporters/schematic/kicad/draw_objs.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - -""" -KiCad 5 drawing objects. -""" - -from collections import namedtuple - - -DrawDef = namedtuple( - "DrawDef", - "name ref zero name_offset show_nums show_names num_units lock_units power_symbol", -) - -DrawF0 = namedtuple("DrawF0", "ref x y size orientation visibility halign valign") - -DrawF1 = namedtuple( - "DrawF1", "name x y size orientation visibility halign valign fieldname" -) - -DrawArc = namedtuple( - "DrawArc", - "cx cy radius start_angle end_angle unit dmg thickness fill startx starty endx endy", -) - -DrawCircle = namedtuple("DrawCircle", "cx cy radius unit dmg thickness fill") - -DrawPoly = namedtuple("DrawPoly", "point_count unit dmg thickness points fill") - -DrawRect = namedtuple("DrawRect", "x1 y1 x2 y2 unit dmg thickness fill") - -DrawText = namedtuple( - "DrawText", "angle x y size hidden unit dmg text italic bold halign valign" -) - -DrawPin = namedtuple( - "DrawPin", - "name num x y length orientation num_size name_size unit dmg electrical_type shape", -) diff --git a/src/faebryk/exporters/schematic/kicad/sch_node.py b/src/faebryk/exporters/schematic/kicad/node.py similarity index 89% rename from src/faebryk/exporters/schematic/kicad/sch_node.py rename to src/faebryk/exporters/schematic/kicad/node.py index da8b690a..9c658051 100644 --- a/src/faebryk/exporters/schematic/kicad/sch_node.py +++ b/src/faebryk/exporters/schematic/kicad/node.py @@ -1,6 +1,3 @@ -""" -Node class for storing circuit hierarchy. -""" # -*- coding: utf-8 -*- # The MIT License (MIT) - Copyright (c) Dave Vandenbout. @@ -9,17 +6,19 @@ from collections import defaultdict from itertools import chain -from faebryk.core.module import Module -import faebryk.library._F as F -from faebryk.core.graphinterface import Graph -from faebryk.core.node import Node - +from skidl.utilities import export_to_all, rmv_attr from .geometry import BBox, Point, Tx, Vector from .place import Placer from .route import Router -class SchNode(Placer, Router, Node): +""" +Node class for storing circuit hierarchy. +""" + + +@export_to_all +class Node(Placer, Router): """Data structure for holding information about a node in the circuit hierarchy.""" filename_sz = 20 @@ -27,7 +26,8 @@ class SchNode(Placer, Router, Node): def __init__( self, - circuit: Graph | None = None, + circuit=None, + tool_module=None, filepath=".", top_name="", title="", @@ -35,14 +35,16 @@ def __init__( ): self.parent = None self.children = defaultdict( - lambda: SchNode(None, top_name, title, flatness) + lambda: Node(None, tool_module, filepath, top_name, title, flatness) ) + self.filepath = filepath self.top_name = top_name self.sheet_name = None self.sheet_filename = None self.title = title self.flatness = flatness self.flattened = False + self.tool_module = tool_module # Backend tool. self.parts = [] self.wires = defaultdict(list) self.junctions = defaultdict(list) @@ -52,26 +54,41 @@ def __init__( if circuit: self.add_circuit(circuit) - class _HasSchNode(Module.TraitT.decless(())): - def __init__(self, node: "SchNode"): - self.node = node + def find_node_with_part(self, part): + """Find the node that contains the part based on its hierarchy. - def find_node_with_part(self, part: F.Symbol) -> "SchNode": - return part.get_trait(self._HasSchNode).node + Args: + part (Part): The part being searched for in the node hierarchy. - def add_circuit(self, circuit: Graph): - """Add parts in circuit to node and its children.""" + Returns: + Node: The Node object containing the part. + """ + + from skidl.circuit import HIER_SEP + + level_names = part.hierarchy.split(HIER_SEP) + node = self + for lvl_nm in level_names[1:]: + node = node.children[lvl_nm] + assert part in node.parts + return node + + def add_circuit(self, circuit): + """Add parts in circuit to node and its children. + + Args: + circuit (Circuit): Circuit object. + """ # Build the circuit node hierarchy by adding the parts. - for part, trait in circuit.nodes_with_trait(F.Symbol.has_symbol): + for part in circuit.parts: self.add_part(part) # Add terminals to nodes in the hierarchy for nets that span across nodes. for net in circuit.nets: - # TODO: # Skip nets that are stubbed since there will be no wire to attach to the NetTerminal. - # if getattr(net, "stub", False): - # continue + if getattr(net, "stub", False): + continue # Search for pins in different nodes. for pin1, pin2 in zip(net.pins[:-1], net.pins[1:]): @@ -113,8 +130,14 @@ def add_circuit(self, circuit: Graph): self.flatten(self.flatness) def add_part(self, part, level=0): - """Add a part to the node at the appropriate level of the hierarchy.""" + """Add a part to the node at the appropriate level of the hierarchy. + Args: + part (Part): Part to be added to this node or one of its children. + level (int, optional): The current level (depth) of the node in the hierarchy. Defaults to 0. + """ + + from skidl.circuit import HIER_SEP # Get list of names of hierarchical levels (in order) leading to this part. level_names = part.hierarchy.split(HIER_SEP) @@ -157,6 +180,8 @@ def add_terminal(self, net): Args: net (Net): The net to be added to this node. """ + + from skidl.circuit import HIER_SEP from .net_terminal import NetTerminal nt = NetTerminal(net, self.tool_module) diff --git a/src/faebryk/exporters/schematic/kicad/place.py b/src/faebryk/exporters/schematic/kicad/place.py index 3be76204..38e7b1b0 100644 --- a/src/faebryk/exporters/schematic/kicad/place.py +++ b/src/faebryk/exporters/schematic/kicad/place.py @@ -14,6 +14,8 @@ from collections import defaultdict from copy import copy +from skidl import Pin, rmv_attr + import faebryk.library._F as F from .debug_draw import ( @@ -64,6 +66,8 @@ class PlacementFailure(Exception): """Exception raised when parts or blocks could not be placed.""" + pass + # Small functions for summing Points and Vectors. pt_sum = lambda pts: sum(pts, Point(0, 0)) diff --git a/src/faebryk/exporters/schematic/kicad/schematic.py b/src/faebryk/exporters/schematic/kicad/schematic.py index ce49c2e4..a231f4aa 100644 --- a/src/faebryk/exporters/schematic/kicad/schematic.py +++ b/src/faebryk/exporters/schematic/kicad/schematic.py @@ -3,21 +3,20 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. +from dataclasses import dataclass import datetime import os.path import re import time from collections import Counter, OrderedDict -from dataclasses import dataclass -from typing import TypedDict, Unpack -from faebryk.exporters.schematic.kicad.sch_node import SchNode import faebryk.library._F as F from faebryk.core.graphinterface import Graph from faebryk.core.module import Module from faebryk.core.node import Node from faebryk.core.trait import Trait -from faebryk.exporters.schematic.kicad.transformer import SCH, SchTransformer +from faebryk.exporters.schematic.kicad.transformer import SchTransformer +from faebryk.libs.util import cast_assert from .bboxes import calc_hier_label_bbox, calc_symbol_bbox from .constants import BLK_INT_PAD, BOX_LABEL_FONT_SIZE, GRID, PIN_LABEL_FONT_SIZE @@ -34,7 +33,7 @@ @dataclass -class Options(TypedDict): +class Options: allow_routing_failure: bool = False compress_before_place: bool = False dont_rotate_pin_count: int = 10000 @@ -60,260 +59,260 @@ class Options(TypedDict): use_push_pull: bool = True -# def bbox_to_eeschema(bbox, tx, name=None): -# """Create a bounding box using EESCHEMA graphic lines.""" - -# # Make sure the box corners are integers. -# bbox = (bbox * tx).round() - -# graphic_box = [] - -# if name: -# # Place name at the lower-left corner of the box. -# name_pt = bbox.ul -# graphic_box.append( -# "Text Notes {} {} 0 {} ~ 20\n{}".format( -# name_pt.x, name_pt.y, BOX_LABEL_FONT_SIZE, name -# ) -# ) - -# graphic_box.append("Wire Notes Line") -# graphic_box.append( -# " {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.lr.x, bbox.lr.y) -# ) -# graphic_box.append("Wire Notes Line") -# graphic_box.append( -# " {} {} {} {}".format(bbox.lr.x, bbox.lr.y, bbox.ur.x, bbox.ur.y) -# ) -# graphic_box.append("Wire Notes Line") -# graphic_box.append( -# " {} {} {} {}".format(bbox.ur.x, bbox.ur.y, bbox.ul.x, bbox.ul.y) -# ) -# graphic_box.append("Wire Notes Line") -# graphic_box.append( -# " {} {} {} {}".format(bbox.ul.x, bbox.ul.y, bbox.ll.x, bbox.ll.y) -# ) -# graphic_box.append("") # For blank line at end. - -# return "\n".join(graphic_box) - - -# def net_to_eeschema(self, tx): -# """Generate the EESCHEMA code for the net terminal. - -# Args: -# tx (Tx): Transformation matrix for the node containing this net terminal. - -# Returns: -# str: EESCHEMA code string. -# """ -# self.pins[0].stub = True -# self.pins[0].orientation = "R" -# return pin_label_to_eeschema(self.pins[0], tx) -# # return pin_label_to_eeschema(self.pins[0], tx) + bbox_to_eeschema(self.bbox, self.tx * tx) - - -# def part_to_eeschema(part, tx): -# """Create EESCHEMA code for a part. - -# Args: -# part (Part): SKiDL part. -# tx (Tx): Transformation matrix. - -# Returns: -# string: EESCHEMA code for the part. - -# Notes: -# https://en.wikibooks.org/wiki/Kicad/file_formats#Schematic_Files_Format -# """ - -# tx = part.tx * tx -# origin = tx.origin.round() -# time_hex = hex(int(time.time()))[2:] -# unit_num = getattr(part, "num", 1) - -# eeschema = [] -# eeschema.append("$Comp") -# lib = os.path.splitext(part.lib.filename)[0] -# eeschema.append("L {}:{} {}".format(lib, part.name, part.ref)) -# eeschema.append("U {} 1 {}".format(unit_num, time_hex)) -# eeschema.append("P {} {}".format(str(origin.x), str(origin.y))) - -# # Add part symbols. For now we are only adding the designator -# n_F0 = 1 -# for i in range(len(part.draw)): -# if re.search("^DrawF0", str(part.draw[i])): -# n_F0 = i -# break -# eeschema.append( -# 'F 0 "{}" {} {} {} {} {} {} {}'.format( -# part.ref, -# part.draw[n_F0].orientation, -# str(origin.x + part.draw[n_F0].x), -# str(origin.y + part.draw[n_F0].y), -# part.draw[n_F0].size, -# "000", # TODO: Refine this to match part def. -# part.draw[n_F0].halign, -# part.draw[n_F0].valign, -# ) -# ) - -# # Part value. -# n_F1 = 1 -# for i in range(len(part.draw)): -# if re.search("^DrawF1", str(part.draw[i])): -# n_F1 = i -# break -# eeschema.append( -# 'F 1 "{}" {} {} {} {} {} {} {}'.format( -# str(part.value), -# part.draw[n_F1].orientation, -# str(origin.x + part.draw[n_F1].x), -# str(origin.y + part.draw[n_F1].y), -# part.draw[n_F1].size, -# "000", # TODO: Refine this to match part def. -# part.draw[n_F1].halign, -# part.draw[n_F1].valign, -# ) -# ) - -# # Part footprint. -# n_F2 = 2 -# for i in range(len(part.draw)): -# if re.search("^DrawF2", str(part.draw[i])): -# n_F2 = i -# break -# eeschema.append( -# 'F 2 "{}" {} {} {} {} {} {} {}'.format( -# part.footprint, -# part.draw[n_F2].orientation, -# str(origin.x + part.draw[n_F2].x), -# str(origin.y + part.draw[n_F2].y), -# part.draw[n_F2].size, -# "001", # TODO: Refine this to match part def. -# part.draw[n_F2].halign, -# part.draw[n_F2].valign, -# ) -# ) -# eeschema.append(" 1 {} {}".format(str(origin.x), str(origin.y))) -# eeschema.append(" {} {} {} {}".format(tx.a, tx.b, tx.c, tx.d)) -# eeschema.append("$EndComp") -# eeschema.append("") # For blank line at end. - -# # For debugging: draws a bounding box around a part. -# # eeschema.append(bbox_to_eeschema(part.bbox, tx)) -# # eeschema.append(bbox_to_eeschema(part.place_bbox, tx)) - -# return "\n".join(eeschema) - - -# def wire_to_eeschema(net, wire, tx): -# """Create EESCHEMA code for a multi-segment wire. - -# Args: -# net (Net): Net associated with the wire. -# wire (list): List of Segments for a wire. -# tx (Tx): transformation matrix for each point in the wire. - -# Returns: -# string: Text to be placed into EESCHEMA file. -# """ - -# eeschema = [] -# for segment in wire: -# eeschema.append("Wire Wire Line") -# w = (segment * tx).round() -# eeschema.append(" {} {} {} {}".format(w.p1.x, w.p1.y, w.p2.x, w.p2.y)) -# eeschema.append("") # For blank line at end. -# return "\n".join(eeschema) - - -# def junction_to_eeschema(net, junctions, tx): -# eeschema = [] -# for junction in junctions: -# pt = (junction * tx).round() -# eeschema.append("Connection ~ {} {}".format(pt.x, pt.y)) -# eeschema.append("") # For blank line at end. -# return "\n".join(eeschema) - - -# def power_part_to_eeschema(part, tx=Tx()): -# return "" # REMOVE: Remove this. -# out = [] -# for pin in part.pins: -# try: -# if not (pin.net is None): -# if pin.net.netclass == "Power": -# # strip out the '_...' section from power nets -# t = pin.net.name -# u = t.split("_") -# symbol_name = u[0] -# # find the stub in the part -# time_hex = hex(int(time.time()))[2:] -# pin_pt = (part.origin + offset + Point(pin.x, pin.y)).round() -# x, y = pin_pt.x, pin_pt.y -# out.append("$Comp\n") -# out.append("L power:{} #PWR?\n".format(symbol_name)) -# out.append("U 1 1 {}\n".format(time_hex)) -# out.append("P {} {}\n".format(str(x), str(y))) -# # Add part symbols. For now we are only adding the designator -# n_F0 = 1 -# for i in range(len(part.draw)): -# if re.search("^DrawF0", str(part.draw[i])): -# n_F0 = i -# break -# part_orientation = part.draw[n_F0].orientation -# part_horizontal_align = part.draw[n_F0].halign -# part_vertical_align = part.draw[n_F0].valign - -# # check if the pin orientation will clash with the power part -# if "+" in symbol_name: -# # voltage sources face up, so check if the pin is facing down (opposite logic y-axis) -# if pin.orientation == "U": -# orientation = [-1, 0, 0, 1] -# elif "gnd" in symbol_name.lower(): -# # gnd points down so check if the pin is facing up (opposite logic y-axis) -# if pin.orientation == "D": -# orientation = [-1, 0, 0, 1] -# out.append( -# 'F 0 "{}" {} {} {} {} {} {} {}\n'.format( -# "#PWR?", -# part_orientation, -# str(x + 25), -# str(y + 25), -# str(40), -# "001", -# part_horizontal_align, -# part_vertical_align, -# ) -# ) -# out.append( -# 'F 1 "{}" {} {} {} {} {} {} {}\n'.format( -# symbol_name, -# part_orientation, -# str(x + 25), -# str(y + 25), -# str(40), -# "000", -# part_horizontal_align, -# part_vertical_align, -# ) -# ) -# out.append(" 1 {} {}\n".format(str(x), str(y))) -# out.append( -# " {} {} {} {}\n".format( -# orientation[0], -# orientation[1], -# orientation[2], -# orientation[3], -# ) -# ) -# out.append("$EndComp\n") -# except Exception as inst: -# print(type(inst)) -# print(inst.args) -# print(inst) -# return "\n" + "".join(out) +def bbox_to_eeschema(bbox, tx, name=None): + """Create a bounding box using EESCHEMA graphic lines.""" + + # Make sure the box corners are integers. + bbox = (bbox * tx).round() + + graphic_box = [] + + if name: + # Place name at the lower-left corner of the box. + name_pt = bbox.ul + graphic_box.append( + "Text Notes {} {} 0 {} ~ 20\n{}".format( + name_pt.x, name_pt.y, BOX_LABEL_FONT_SIZE, name + ) + ) + + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.lr.x, bbox.lr.y) + ) + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.lr.x, bbox.lr.y, bbox.ur.x, bbox.ur.y) + ) + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.ur.x, bbox.ur.y, bbox.ul.x, bbox.ul.y) + ) + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.ul.x, bbox.ul.y, bbox.ll.x, bbox.ll.y) + ) + graphic_box.append("") # For blank line at end. + + return "\n".join(graphic_box) + + +def net_to_eeschema(self, tx): + """Generate the EESCHEMA code for the net terminal. + + Args: + tx (Tx): Transformation matrix for the node containing this net terminal. + + Returns: + str: EESCHEMA code string. + """ + self.pins[0].stub = True + self.pins[0].orientation = "R" + return pin_label_to_eeschema(self.pins[0], tx) + # return pin_label_to_eeschema(self.pins[0], tx) + bbox_to_eeschema(self.bbox, self.tx * tx) + + +def part_to_eeschema(part, tx): + """Create EESCHEMA code for a part. + + Args: + part (Part): SKiDL part. + tx (Tx): Transformation matrix. + + Returns: + string: EESCHEMA code for the part. + + Notes: + https://en.wikibooks.org/wiki/Kicad/file_formats#Schematic_Files_Format + """ + + tx = part.tx * tx + origin = tx.origin.round() + time_hex = hex(int(time.time()))[2:] + unit_num = getattr(part, "num", 1) + + eeschema = [] + eeschema.append("$Comp") + lib = os.path.splitext(part.lib.filename)[0] + eeschema.append("L {}:{} {}".format(lib, part.name, part.ref)) + eeschema.append("U {} 1 {}".format(unit_num, time_hex)) + eeschema.append("P {} {}".format(str(origin.x), str(origin.y))) + + # Add part symbols. For now we are only adding the designator + n_F0 = 1 + for i in range(len(part.draw)): + if re.search("^DrawF0", str(part.draw[i])): + n_F0 = i + break + eeschema.append( + 'F 0 "{}" {} {} {} {} {} {} {}'.format( + part.ref, + part.draw[n_F0].orientation, + str(origin.x + part.draw[n_F0].x), + str(origin.y + part.draw[n_F0].y), + part.draw[n_F0].size, + "000", # TODO: Refine this to match part def. + part.draw[n_F0].halign, + part.draw[n_F0].valign, + ) + ) + + # Part value. + n_F1 = 1 + for i in range(len(part.draw)): + if re.search("^DrawF1", str(part.draw[i])): + n_F1 = i + break + eeschema.append( + 'F 1 "{}" {} {} {} {} {} {} {}'.format( + str(part.value), + part.draw[n_F1].orientation, + str(origin.x + part.draw[n_F1].x), + str(origin.y + part.draw[n_F1].y), + part.draw[n_F1].size, + "000", # TODO: Refine this to match part def. + part.draw[n_F1].halign, + part.draw[n_F1].valign, + ) + ) + + # Part footprint. + n_F2 = 2 + for i in range(len(part.draw)): + if re.search("^DrawF2", str(part.draw[i])): + n_F2 = i + break + eeschema.append( + 'F 2 "{}" {} {} {} {} {} {} {}'.format( + part.footprint, + part.draw[n_F2].orientation, + str(origin.x + part.draw[n_F2].x), + str(origin.y + part.draw[n_F2].y), + part.draw[n_F2].size, + "001", # TODO: Refine this to match part def. + part.draw[n_F2].halign, + part.draw[n_F2].valign, + ) + ) + eeschema.append(" 1 {} {}".format(str(origin.x), str(origin.y))) + eeschema.append(" {} {} {} {}".format(tx.a, tx.b, tx.c, tx.d)) + eeschema.append("$EndComp") + eeschema.append("") # For blank line at end. + + # For debugging: draws a bounding box around a part. + # eeschema.append(bbox_to_eeschema(part.bbox, tx)) + # eeschema.append(bbox_to_eeschema(part.place_bbox, tx)) + + return "\n".join(eeschema) + + +def wire_to_eeschema(net, wire, tx): + """Create EESCHEMA code for a multi-segment wire. + + Args: + net (Net): Net associated with the wire. + wire (list): List of Segments for a wire. + tx (Tx): transformation matrix for each point in the wire. + + Returns: + string: Text to be placed into EESCHEMA file. + """ + + eeschema = [] + for segment in wire: + eeschema.append("Wire Wire Line") + w = (segment * tx).round() + eeschema.append(" {} {} {} {}".format(w.p1.x, w.p1.y, w.p2.x, w.p2.y)) + eeschema.append("") # For blank line at end. + return "\n".join(eeschema) + + +def junction_to_eeschema(net, junctions, tx): + eeschema = [] + for junction in junctions: + pt = (junction * tx).round() + eeschema.append("Connection ~ {} {}".format(pt.x, pt.y)) + eeschema.append("") # For blank line at end. + return "\n".join(eeschema) + + +def power_part_to_eeschema(part, tx=Tx()): + return "" # REMOVE: Remove this. + out = [] + for pin in part.pins: + try: + if not (pin.net is None): + if pin.net.netclass == "Power": + # strip out the '_...' section from power nets + t = pin.net.name + u = t.split("_") + symbol_name = u[0] + # find the stub in the part + time_hex = hex(int(time.time()))[2:] + pin_pt = (part.origin + offset + Point(pin.x, pin.y)).round() + x, y = pin_pt.x, pin_pt.y + out.append("$Comp\n") + out.append("L power:{} #PWR?\n".format(symbol_name)) + out.append("U 1 1 {}\n".format(time_hex)) + out.append("P {} {}\n".format(str(x), str(y))) + # Add part symbols. For now we are only adding the designator + n_F0 = 1 + for i in range(len(part.draw)): + if re.search("^DrawF0", str(part.draw[i])): + n_F0 = i + break + part_orientation = part.draw[n_F0].orientation + part_horizontal_align = part.draw[n_F0].halign + part_vertical_align = part.draw[n_F0].valign + + # check if the pin orientation will clash with the power part + if "+" in symbol_name: + # voltage sources face up, so check if the pin is facing down (opposite logic y-axis) + if pin.orientation == "U": + orientation = [-1, 0, 0, 1] + elif "gnd" in symbol_name.lower(): + # gnd points down so check if the pin is facing up (opposite logic y-axis) + if pin.orientation == "D": + orientation = [-1, 0, 0, 1] + out.append( + 'F 0 "{}" {} {} {} {} {} {} {}\n'.format( + "#PWR?", + part_orientation, + str(x + 25), + str(y + 25), + str(40), + "001", + part_horizontal_align, + part_vertical_align, + ) + ) + out.append( + 'F 1 "{}" {} {} {} {} {} {} {}\n'.format( + symbol_name, + part_orientation, + str(x + 25), + str(y + 25), + str(40), + "000", + part_horizontal_align, + part_vertical_align, + ) + ) + out.append(" 1 {} {}\n".format(str(x), str(y))) + out.append( + " {} {} {} {}\n".format( + orientation[0], + orientation[1], + orientation[2], + orientation[3], + ) + ) + out.append("$EndComp\n") + except Exception as inst: + print(type(inst)) + print(inst.args) + print(inst) + return "\n" + "".join(out) # Sizes of EESCHEMA schematic pages from smallest to largest. Dimensions in mils. @@ -554,12 +553,12 @@ def node_to_eeschema(node, sheet_tx=Tx()): Generate a KiCad EESCHEMA schematic from a Circuit object. """ -class has_symbol_layout_data(Trait.decless()): +class has_symbol_layout_data(Trait): tx: Tx orientation_locked: bool -class has_pin_layout_data(Trait.decless()): +class has_pin_layout_data(Trait): pt: Point routed: bool @@ -572,44 +571,47 @@ class Impl(trait.impl()): return node.add(Impl()) -def preprocess_circuit(circuit: Graph, transformer: SchTransformer, **options: Unpack[Options]): +def preprocess_circuit(circuit: Graph, **options): """Add stuff to parts & nets for doing placement and routing of schematics.""" - def units(part: F.Symbol) -> list[F.Symbol]: + def units(part: Module): return [part] + # TODO: handle units within parts + # if len(part.unit) == 0: + # return [part] + # else: + # return part.unit.values() - def initialize(part: F.Symbol): + def initialize(part: Module): """Initialize part or its part units.""" # Initialize the units of the part, or the part itself if it has no units. pin_limit = options.get("orientation_pin_limit", 44) for part_unit in units(part): - layout_data = part.add(has_symbol_layout_data()) + cmp_data_trait = _add_data_trait(part, has_symbol_layout_data) # Initialize transform matrix. - user_layout_data = part_unit.try_get_trait( - F.has_symbol_layout - ) or part_unit.add(F.has_symbol_layout()) - layout_data.tx = Tx.from_symtx(user_layout_data.translations) + layout_data = part_unit.get_trait(F.has_symbol_layout) or part_unit.add(F.has_symbol_layout_defined()) + cmp_data_trait.tx = Tx.from_symtx(layout_data.translations) # Lock part orientation if symtx was specified. Also lock parts with a lot of pins # since they're typically drawn the way they're supposed to be oriented. # And also lock single-pin parts because these are usually power/ground and # they shouldn't be flipped around. num_pins = len(part_unit.get_children(direct_only=True, types=F.Symbol.Pin)) - layout_data.orientation_locked = bool(user_layout_data.translations) or ( - num_pins >= pin_limit + cmp_data_trait.orientation_locked = bool(layout_data.translations) or not ( + 1 < num_pins <= pin_limit ) # Initialize pin attributes used for generating schematics. for pin in part_unit.get_children(direct_only=True, types=F.Symbol.Pin): - pin_data = pin.add(has_pin_layout_data()) + pin_data_trait = _add_data_trait(pin, has_pin_layout_data) lib_pin = SchTransformer.get_lib_pin(pin) # TODO: what to do with pin rotation? - pin_data.pt = Point(lib_pin.at.x, lib_pin.at.y) - pin_data.routed = False + pin_data_trait.pt = Point(lib_pin.at.x, lib_pin.at.y) + pin_data_trait.routed = False - def rotate_power_pins(part: F.Symbol): + def rotate_power_pins(part: Module): """Rotate a part based on the direction of its power pins. This function is to make sure that voltage sources face up and gnd pins @@ -621,14 +623,10 @@ def rotate_power_pins(part: F.Symbol): return def is_pwr(net: F.Electrical): - if power := net.get_parent_of_type(F.ElectricPower): - return net is power.hv - return False + F.ElectricPower - def is_gnd(net: F.Electrical): - if power := net.get_parent_of_type(F.ElectricPower): - return net is power.lv - return False + def is_gnd(net): + return "gnd" in net_name.lower() dont_rotate_pin_cnt = options.get("dont_rotate_pin_count", 10000) @@ -639,10 +637,9 @@ def is_gnd(net: F.Electrical): # Tally what rotation would make each pwr/gnd pin point up or down. rotation_tally = Counter() - for pin in part_unit.get_children(direct_only=True, types=F.Symbol.Pin): - lib_pin = transformer.get_lib_pin(pin) - lib_pin.at.r - if is_gnd(pin.represents): + for pin in part_unit: + net_name = getattr(pin.net, "name", "").lower() + if is_gnd(net_name): if pin.orientation == "U": rotation_tally[0] += 1 if pin.orientation == "D": @@ -651,7 +648,7 @@ def is_gnd(net: F.Electrical): rotation_tally[90] += 1 if pin.orientation == "R": rotation_tally[270] += 1 - elif is_pwr(pin.represents): + elif is_pwr(net_name): if pin.orientation == "D": rotation_tally[0] += 1 if pin.orientation == "U": @@ -706,7 +703,7 @@ def calc_part_bbox(part: Module): # Pre-process parts # TODO: complete criteria on what schematic symbols we can handle - for part, has_symbol_trait in circuit.nodes_with_trait(F.Symbol.has_symbol): + for part, has_symbol_trait in circuit.nodes_with_trait(F.has_symbol): symbol = has_symbol_trait.get_symbol() # Initialize part attributes used for generating schematics. initialize(symbol) @@ -718,21 +715,46 @@ def calc_part_bbox(part: Module): calc_part_bbox(symbol) +def finalize_parts_and_nets(circuit, **options): + """Restore parts and nets after place & route is done.""" + + # Remove any NetTerminals that were added. + net_terminals = (p for p in circuit.parts if isinstance(p, NetTerminal)) + circuit.rmv_parts(*net_terminals) + + # Return pins from the part units to their parent part. + for part in circuit.parts: + part.grab_pins() + + # Remove some stuff added to parts during schematic generation process. + rmv_attr(circuit.parts, ("force", "bbox", "lbl_bbox", "tx")) + + def gen_schematic( - circuit: Module, - transformer: SchTransformer, - top_name, - title="Faebryk Schematic", + circuit, + filepath=".", + top_name=get_script_name(), + title="SKiDL-Generated Schematic", flatness=0.0, - **options: Unpack[Options], + retries=2, + **options, ): - """ - Create a schematic + """Create a schematic file from a Circuit object. - Recommendation on RoutingFailure: expansion_factor *= 1.5 - Recommendation on PlacementFailure: try again with a different random seed + Args: + circuit (Circuit): The Circuit object that will have a schematic generated for it. + filepath (str, optional): The directory where the schematic files are placed. Defaults to ".". + top_name (str, optional): The name for the top of the circuit hierarchy. Defaults to get_script_name(). + title (str, optional): The title of the schematic. Defaults to "SKiDL-Generated Schematic". + flatness (float, optional): Determines how much the hierarchy is flattened in the schematic. Defaults to 0.0 (completely hierarchical). + retries (int, optional): Number of times to re-try if routing fails. Defaults to 2. + options (dict, optional): Dict of options and values, usually for drawing/debugging. """ + from skidl import KICAD + from skidl.schematics.node import Node + from skidl.tools import tool_modules + from .place import PlacementFailure from .route import RoutingFailure @@ -745,36 +767,56 @@ def gen_schematic( # Start with default routing area. expansion_factor = 1.0 - preprocess_circuit(circuit, transformer, **options) + # Try to place & route one or more times. + for _ in range(retries): + preprocess_circuit(circuit, **options) - node = SchNode(circuit, tool_modules[KICAD], filepath, top_name, title, flatness) + node = Node(circuit, tool_modules[KICAD], filepath, top_name, title, flatness) - try: - # Place parts. - node.place(expansion_factor=expansion_factor, **options) + try: + # Place parts. + node.place(expansion_factor=expansion_factor, **options) - # Route parts. - node.route(**options) + # Route parts. + node.route(**options) - except PlacementFailure: - # Placement failed, so clean up ... - finalize_parts_and_nets(circuit, **options) - # ... and try again. - continue + except PlacementFailure: + # Placement failed, so clean up ... + finalize_parts_and_nets(circuit, **options) + # ... and try again. + continue + + except RoutingFailure: + # Routing failed, so clean up ... + finalize_parts_and_nets(circuit, **options) + # ... and expand routing area ... + expansion_factor *= 1.5 # HACK: Ad-hoc increase of expansion factor. + # ... and try again. + continue - except RoutingFailure: - # Routing failed, so clean up ... + # Generate EESCHEMA code for the schematic. + node_to_eeschema(node) + + # Append place & route statistics for the schematic to a file. + if options.get("collect_stats"): + stats = node.collect_stats(**options) + with open(options["stats_file"], "a") as f: + f.write(stats) + + # Clean up. finalize_parts_and_nets(circuit, **options) - # ... and expand routing area ... - expansion_factor *= 1.5 # HACK: Ad-hoc increase of expansion factor. - # ... and try again. - continue - # Generate EESCHEMA code for the schematic. - node_to_eeschema(node) + # Place & route was successful if we got here, so exit. + return - # Append place & route statistics for the schematic to a file. + # Append failed place & route statistics for the schematic to a file. if options.get("collect_stats"): - stats = node.collect_stats(**options) + stats = "-1\n" with open(options["stats_file"], "a") as f: f.write(stats) + + # Clean-up after failure. + finalize_parts_and_nets(circuit, **options) + + # Exited the loop without successful routing. + raise (RoutingFailure) From 8009559abc0544e0f9c0b95c09933747be4ced0e Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 15:40:28 +0200 Subject: [PATCH 08/85] Restart with skidl --- .../exporters/schematic/kicad/__init__.py | 0 .../exporters/schematic/kicad/bboxes.py | 326 -- .../exporters/schematic/kicad/constants.py | 16 - .../exporters/schematic/kicad/debug_draw.py | 338 -- .../exporters/schematic/kicad/geometry.py | 485 --- .../exporters/schematic/kicad/net_terminal.py | 65 - src/faebryk/exporters/schematic/kicad/node.py | 354 -- .../exporters/schematic/kicad/place.py | 1554 -------- .../exporters/schematic/kicad/route.py | 3413 ----------------- .../exporters/schematic/kicad/schematic.py | 822 ---- 10 files changed, 7373 deletions(-) delete mode 100644 src/faebryk/exporters/schematic/kicad/__init__.py delete mode 100644 src/faebryk/exporters/schematic/kicad/bboxes.py delete mode 100644 src/faebryk/exporters/schematic/kicad/constants.py delete mode 100644 src/faebryk/exporters/schematic/kicad/debug_draw.py delete mode 100644 src/faebryk/exporters/schematic/kicad/geometry.py delete mode 100644 src/faebryk/exporters/schematic/kicad/net_terminal.py delete mode 100644 src/faebryk/exporters/schematic/kicad/node.py delete mode 100644 src/faebryk/exporters/schematic/kicad/place.py delete mode 100644 src/faebryk/exporters/schematic/kicad/route.py delete mode 100644 src/faebryk/exporters/schematic/kicad/schematic.py diff --git a/src/faebryk/exporters/schematic/kicad/__init__.py b/src/faebryk/exporters/schematic/kicad/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/faebryk/exporters/schematic/kicad/bboxes.py b/src/faebryk/exporters/schematic/kicad/bboxes.py deleted file mode 100644 index 88368986..00000000 --- a/src/faebryk/exporters/schematic/kicad/bboxes.py +++ /dev/null @@ -1,326 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - -""" -Calculate bounding boxes for part symbols and hierarchical sheets. -""" - -from collections import namedtuple - -from skidl.logger import active_logger -from skidl.schematics.geometry import ( - Tx, - BBox, - Point, - Vector, - tx_rot_0, - tx_rot_90, - tx_rot_180, - tx_rot_270, -) -from skidl.utilities import export_to_all -from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE -from skidl.schematics.geometry import BBox, Point, Tx, Vector -from .draw_objs import * - - -@export_to_all -def calc_symbol_bbox(part, **options): - """ - Return the bounding box of the part symbol. - - Args: - part: Part object for which an SVG symbol will be created. - options (dict): Various options to control bounding box calculation: - graphics_only (boolean): If true, compute bbox of graphics (no text). - - Returns: List of BBoxes for all units in the part symbol. - - Note: V5 library format: https://www.compuphase.com/electronics/LibraryFileFormats.pdf - """ - - # Named tuples for part KiCad V5 DRAW primitives. - - def make_pin_dir_tbl(abs_xoff=20): - # abs_xoff is the absolute distance of name/num from the end of the pin. - rel_yoff_num = -0.15 # Relative distance of number above pin line. - rel_yoff_name = ( - 0.2 # Relative distance that places name midline even with pin line. - ) - - # Tuple for storing information about pins in each of four directions: - # direction: The direction the pin line is drawn from start to end. - # side: The side of the symbol the pin is on. (Opposite of the direction.) - # angle: The angle of the name/number text for the pin (usually 0, -90.). - # num_justify: Text justification of the pin number. - # name_justify: Text justification of the pin name. - # num_offset: (x,y) offset of the pin number w.r.t. the end of the pin. - # name_offset: (x,y) offset of the pin name w.r.t. the end of the pin. - PinDir = namedtuple( - "PinDir", - "direction side angle num_justify name_justify num_offset name_offset net_offset", - ) - - return { - "U": PinDir( - Point(0, 1), - "bottom", - -90, - "end", - "start", - Point(-abs_xoff, rel_yoff_num), - Point(abs_xoff, rel_yoff_name), - Point(abs_xoff, rel_yoff_num), - ), - "D": PinDir( - Point(0, -1), - "top", - -90, - "start", - "end", - Point(abs_xoff, rel_yoff_num), - Point(-abs_xoff, rel_yoff_name), - Point(-abs_xoff, rel_yoff_num), - ), - "L": PinDir( - Point(-1, 0), - "right", - 0, - "start", - "end", - Point(abs_xoff, rel_yoff_num), - Point(-abs_xoff, rel_yoff_name), - Point(-abs_xoff, rel_yoff_num), - ), - "R": PinDir( - Point(1, 0), - "left", - 0, - "end", - "start", - Point(-abs_xoff, rel_yoff_num), - Point(abs_xoff, rel_yoff_name), - Point(abs_xoff, rel_yoff_num), - ), - } - - default_pin_name_offset = 20 - - # Go through each graphic object that makes up the component symbol. - for obj in part.draw: - obj_bbox = BBox() # Bounding box of all the component objects. - thickness = 0 - - if isinstance(obj, DrawDef): - def_ = obj - # Make pin direction table with symbol-specific name offset. - pin_dir_tbl = make_pin_dir_tbl(def_.name_offset or default_pin_name_offset) - # Make structures for holding info on each part unit. - num_units = def_.num_units - unit_bboxes = [BBox() for _ in range(num_units + 1)] - - elif isinstance(obj, DrawF0) and not options.get("graphics_only", False): - # obj attributes: x y size orientation visibility halign valign - # Skip if the object is invisible. - if obj.visibility.upper() == "I": - continue - - # Calculate length and height of part reference. - # Use ref from the SKiDL part since the ref in the KiCAD part - # hasn't been updated from its generic value. - length = len(part.ref) * obj.size - height = obj.size - - # Create bbox with lower-left point at (0, 0). - bbox = BBox(Point(0, 0), Point(length, height)) - - # Rotate bbox around origin. - rot_tx = {"H": Tx(), "V": tx_rot_90}[obj.orientation.upper()] - bbox *= rot_tx - - # Horizontally align bbox. - halign = obj.halign.upper() - if halign == "L": - pass - elif halign == "R": - bbox *= Tx().move(Point(-bbox.w, 0)) - elif halign == "C": - bbox *= Tx().move(Point(-bbox.w / 2, 0)) - else: - raise Exception("Inconsistent horizontal alignment: {}".format(halign)) - - # Vertically align bbox. - valign = obj.valign[:1].upper() # valign is first letter. - if valign == "B": - pass - elif valign == "T": - bbox *= Tx().move(Point(0, -bbox.h)) - elif valign == "C": - bbox *= Tx().move(Point(0, -bbox.h / 2)) - else: - raise Exception("Inconsistent vertical alignment: {}".format(valign)) - - bbox *= Tx().move(Point(obj.x, obj.y)) - obj_bbox.add(bbox) - - elif isinstance(obj, DrawF1) and not options.get("graphics_only", False): - # Skip if the object is invisible. - if obj.visibility.upper() == "I": - continue - - # Calculate length and height of part value. - # Use value from the SKiDL part since the value in the KiCAD part - # hasn't been updated from its generic value. - length = len(str(part.value)) * obj.size - height = obj.size - - # Create bbox with lower-left point at (0, 0). - bbox = BBox(Point(0, 0), Point(length, height)) - - # Rotate bbox around origin. - rot_tx = {"H": Tx(), "V": tx_rot_90}[obj.orientation.upper()] - bbox *= rot_tx - - # Horizontally align bbox. - halign = obj.halign.upper() - if halign == "L": - pass - elif halign == "R": - bbox *= Tx().move(Point(-bbox.w, 0)) - elif halign == "C": - bbox *= Tx().move(Point(-bbox.w / 2, 0)) - else: - raise Exception("Inconsistent horizontal alignment: {}".format(halign)) - - # Vertically align bbox. - valign = obj.valign[:1].upper() # valign is first letter. - if valign == "B": - pass - elif valign == "T": - bbox *= Tx().move(Point(0, -bbox.h)) - elif valign == "C": - bbox *= Tx().move(Point(0, -bbox.h / 2)) - else: - raise Exception("Inconsistent vertical alignment: {}".format(valign)) - - bbox *= Tx().move(Point(obj.x, obj.y)) - obj_bbox.add(bbox) - - elif isinstance(obj, DrawArc): - arc = obj - center = Point(arc.cx, arc.cy) - thickness = arc.thickness - radius = arc.radius - start = Point(arc.startx, arc.starty) - end = Point(arc.endx, arc.endy) - start_angle = arc.start_angle / 10 - end_angle = arc.end_angle / 10 - clock_wise = int(end_angle < start_angle) - large_arc = int(abs(end_angle - start_angle) > 180) - radius_pt = Point(radius, radius) - obj_bbox.add(center - radius_pt) - obj_bbox.add(center + radius_pt) - - elif isinstance(obj, DrawCircle): - circle = obj - center = Point(circle.cx, circle.cy) - thickness = circle.thickness - radius = circle.radius - radius_pt = Point(radius, radius) - obj_bbox.add(center - radius_pt) - obj_bbox.add(center + radius_pt) - - elif isinstance(obj, DrawPoly): - poly = obj - thickness = obj.thickness - pts = [Point(x, y) for x, y in zip(poly.points[0::2], poly.points[1::2])] - path = [] - for pt in pts: - obj_bbox.add(pt) - - elif isinstance(obj, DrawRect): - rect = obj - thickness = obj.thickness - start = Point(rect.x1, rect.y1) - end = Point(rect.x2, rect.y2) - obj_bbox.add(start) - obj_bbox.add(end) - - elif isinstance(obj, DrawText) and not options.get("graphics_only", False): - pass - - elif isinstance(obj, DrawPin): - pin = obj - - try: - visible = pin.shape[0] != "N" - except IndexError: - visible = True # No pin shape given, so it is visible by default. - - if visible: - # Draw pin if it's not invisible. - - # Create line for pin lead. - dir = pin_dir_tbl[pin.orientation].direction - start = Point(pin.x, pin.y) - l = dir * pin.length - end = start + l - obj_bbox.add(start) - obj_bbox.add(end) - - else: - active_logger.error( - "Unknown graphical object {} in part symbol {}.".format( - type(obj), part.name - ) - ) - - # REMOVE: Maybe we shouldn't do this? - # Expand bounding box to account for object line thickness. - # obj_bbox.resize(Vector(round(thickness / 2), round(thickness / 2))) - - # Enter the current object into the SVG for this part. - unit = getattr(obj, "unit", 0) - if unit == 0: - for bbox in unit_bboxes: - bbox.add(obj_bbox) - else: - unit_bboxes[unit].add(obj_bbox) - - # End of loop through all the component objects. - - return unit_bboxes - - -@export_to_all -def calc_hier_label_bbox(label, dir): - """Calculate the bounding box for a hierarchical label. - - Args: - label (str): String for the label. - dir (str): Orientation ("U", "D", "L", "R"). - - Returns: - BBox: Bounding box for the label and hierarchical terminal. - """ - - # Rotation matrices for each direction. - lbl_tx = { - "U": tx_rot_90, # Pin on bottom pointing upwards. - "D": tx_rot_270, # Pin on top pointing down. - "L": tx_rot_180, # Pin on right pointing left. - "R": tx_rot_0, # Pin on left pointing right. - } - - # Calculate length and height of label + hierarchical marker. - lbl_len = len(label) * PIN_LABEL_FONT_SIZE + HIER_TERM_SIZE - lbl_hgt = max(PIN_LABEL_FONT_SIZE, HIER_TERM_SIZE) - - # Create bbox for label on left followed by marker on right. - bbox = BBox(Point(0, lbl_hgt / 2), Point(-lbl_len, -lbl_hgt / 2)) - - # Rotate the bbox in the given direction. - bbox *= lbl_tx[dir] - - return bbox diff --git a/src/faebryk/exporters/schematic/kicad/constants.py b/src/faebryk/exporters/schematic/kicad/constants.py deleted file mode 100644 index a7de8044..00000000 --- a/src/faebryk/exporters/schematic/kicad/constants.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - -""" -Constants used when generating schematics. -""" - -# Constants for KiCad. -GRID = 50 -PIN_LABEL_FONT_SIZE = 50 -BOX_LABEL_FONT_SIZE = 50 -BLK_INT_PAD = 2 * GRID -BLK_EXT_PAD = 2 * GRID -DRAWING_BOX_RESIZE = 100 -HIER_TERM_SIZE = 50 diff --git a/src/faebryk/exporters/schematic/kicad/debug_draw.py b/src/faebryk/exporters/schematic/kicad/debug_draw.py deleted file mode 100644 index f9b0eaec..00000000 --- a/src/faebryk/exporters/schematic/kicad/debug_draw.py +++ /dev/null @@ -1,338 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - -""" -Drawing routines used for debugging place & route. -""" - -from collections import defaultdict -from random import randint - -from .geometry import BBox, Point, Segment, Tx, Vector - -# Dictionary for storing colors to visually distinguish routed nets. -net_colors = defaultdict(lambda: (randint(0, 200), randint(0, 200), randint(0, 200))) - - -def draw_box(bbox, scr, tx, color=(192, 255, 192), thickness=0): - """Draw a box in the drawing area. - - Args: - bbox (BBox): Bounding box for the box. - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - color (tuple, optional): Box color. Defaults to (192, 255, 192). - - Returns: - None. - """ - import pygame - - bbox = bbox * tx - corners = ( - (bbox.min.x, bbox.min.y), - (bbox.min.x, bbox.max.y), - (bbox.max.x, bbox.max.y), - (bbox.max.x, bbox.min.y), - ) - pygame.draw.polygon(scr, color, corners, thickness) - - -def draw_endpoint(pt, scr, tx, color=(100, 100, 100), dot_radius=10): - """Draw a line segment endpoint in the drawing area. - - Args: - pt (Point): A point with (x,y) coords. - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - color (tuple, optional): Segment color. Defaults to (192, 255, 192). - dot_Radius (int, optional): Endpoint dot radius. Defaults to 3. - """ - import pygame - - pt = pt * tx # Convert to drawing coords. - - # Draw diamond for terminal. - sz = dot_radius / 2 * tx.a # Scale for drawing coords. - corners = ( - (pt.x, pt.y + sz), - (pt.x + sz, pt.y), - (pt.x, pt.y - sz), - (pt.x - sz, pt.y), - ) - pygame.draw.polygon(scr, color, corners, 0) - - # Draw dot for terminal. - radius = dot_radius * tx.a - pygame.draw.circle(scr, color, (pt.x, pt.y), radius) - - -def draw_seg(seg, scr, tx, color=(100, 100, 100), thickness=5, dot_radius=10): - """Draw a line segment in the drawing area. - - Args: - seg (Segment, Interval, NetInterval): An object with two endpoints. - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - color (tuple, optional): Segment color. Defaults to (192, 255, 192). - seg_thickness (int, optional): Segment line thickness. Defaults to 5. - dot_Radius (int, optional): Endpoint dot radius. Defaults to 3. - """ - import pygame - - # Use net color if object has a net. Otherwise use input color. - try: - color = net_colors[seg.net] - except AttributeError: - pass - - # draw endpoints. - draw_endpoint(seg.p1, scr, tx, color=color, dot_radius=dot_radius) - draw_endpoint(seg.p2, scr, tx, color=color, dot_radius=dot_radius) - - # Transform segment coords to screen coords. - seg = seg * tx - - # Draw segment. - pygame.draw.line( - scr, color, (seg.p1.x, seg.p1.y), (seg.p2.x, seg.p2.y), width=thickness - ) - - -def draw_text(txt, pt, scr, tx, font, color=(100, 100, 100), real=True): - """Render text in drawing area. - - Args: - txt (str): Text string to be rendered. - pt (Point): Real or screen coord for start of rendered text. - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - font (PyGame font): Font for rendering text. - color (tuple, optional): Segment color. Defaults to (100,100,100). - real (Boolean): If true, transform real pt to screen coords. Otherwise, pt is screen coords. - """ - - # Transform real text starting point to screen coords. - if real: - pt = pt * tx - - # Render text. - font.render_to(scr, (pt.x, pt.y), txt, color) - - -def draw_part(part, scr, tx, font): - """Draw part bounding box. - - Args: - part (Part): Part to draw. - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - font (PyGame font): Font for rendering text. - """ - tx_bbox = ( - getattr(part, "lbl_bbox", getattr(part, "place_bbox", Vector(0, 0))) * part.tx - ) - draw_box(tx_bbox, scr, tx, color=(180, 255, 180), thickness=0) - draw_box(tx_bbox, scr, tx, color=(90, 128, 90), thickness=5) - draw_text(part.ref, tx_bbox.ctr, scr, tx, font) - try: - for pin in part: - if hasattr(pin, "place_pt"): - pt = pin.place_pt * part.tx - draw_endpoint(pt, scr, tx, color=(200, 0, 200), dot_radius=10) - except TypeError: - # Probably trying to draw a block of parts which has no pins and can't iterate thru them. - pass - - -def draw_net(net, parts, scr, tx, font, color=(0, 0, 0), thickness=2, dot_radius=5): - """Draw net and connected terminals. - - Args: - net (Net): Net of conmnected terminals. - parts (list): List of parts to which net will be drawn. - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - font (PyGame font): Font for rendering text. - color (tuple, optional): Segment color. Defaults to (0,0,0). - thickness (int, optional): Thickness of net line. Defaults to 2. - dot_radius (int, optional): Radius of terminals on net. Defaults to 5. - """ - pts = [] - for pin in net.pins: - part = pin.part - if part in parts: - pt = pin.route_pt * part.tx - pts.append(pt) - for pt1, pt2 in zip(pts[:-1], pts[1:]): - draw_seg( - Segment(pt1, pt2), - scr, - tx, - color=color, - thickness=thickness, - dot_radius=dot_radius, - ) - - -def draw_force(part, force, scr, tx, font, color=(128, 0, 0)): - """Draw force vector affecting a part. - - Args: - part (Part): The part affected by the force. - force (Vector): The force vector. - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - font (PyGame font): Font for rendering text. - color (tuple, optional): Segment color. Defaults to (0,0,0). - """ - force *= 1 - anchor = part.place_bbox.ctr * part.tx - draw_seg( - Segment(anchor, anchor + force), scr, tx, color=color, thickness=5, dot_radius=5 - ) - - -def draw_placement(parts, nets, scr, tx, font): - """Draw placement of parts and interconnecting nets. - - Args: - parts (list): List of Part objects. - nets (list): List of Net objects. - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - font (PyGame font): Font for rendering text. - """ - draw_clear(scr) - for part in parts: - draw_part(part, scr, tx, font) - draw_force(part, getattr(part, "force", Vector(0, 0)), scr, tx, font) - for net in nets: - draw_net(net, parts, scr, tx, font) - draw_redraw() - - -def draw_routing(node, bbox, parts, *other_stuff, **options): - """Draw routing for debugging purposes. - - Args: - node (Node): Hierarchical node. - bbox (BBox): Bounding box of drawing area. - node (Node): The Node being routed. - parts (list): List of Parts. - other_stuff (list): Other stuff with a draw() method. - options (dict, optional): Dictionary of options and values. Defaults to {}. - """ - - # Initialize drawing area. - draw_scr, draw_tx, draw_font = draw_start(bbox) - - # Draw parts. - for part in parts: - draw_part(part, draw_scr, draw_tx, draw_font) - - # Draw wiring. - for wires in node.wires.values(): - for wire in wires: - draw_seg(wire, draw_scr, draw_tx, (255, 0, 255), 3, dot_radius=10) - - # Draw other stuff (global routes, switchbox routes, etc.) that has a draw() method. - for stuff in other_stuff: - for obj in stuff: - obj.draw(draw_scr, draw_tx, draw_font, **options) - - draw_end() - - -def draw_clear(scr, color=(255, 255, 255)): - """Clear drawing area. - - Args: - scr (PyGame screen): Screen object to be cleared. - color (tuple, optional): Background color. Defaults to (255, 255, 255). - """ - scr.fill(color) - - -def draw_start(bbox): - """ - Initialize PyGame drawing area. - - Args: - bbox: Bounding box of object to be drawn. - - Returns: - scr: PyGame screen that is drawn upon. - tx: Matrix to transform from real coords to screen coords. - font: PyGame font for rendering text. - """ - - # Only import pygame if drawing is being done to avoid the startup message. - import pygame - import pygame.freetype - - # Screen drawing area. - scr_bbox = BBox(Point(0, 0), Point(2000, 1500)) - - # Place a blank region around the object by expanding it's bounding box. - border = max(bbox.w, bbox.h) / 20 - bbox = bbox.resize(Vector(border, border)) - bbox = bbox.round() - - # Compute the scaling from real to screen coords. - scale = min(scr_bbox.w / bbox.w, scr_bbox.h / bbox.h) - scale_tx = Tx(a=scale, d=scale) - - # Flip the Y coord. - flip_tx = Tx(d=-1) - - # Compute the translation of the object center to the drawing area center - new_bbox = bbox * scale_tx * flip_tx # Object bbox transformed to screen coords. - move = scr_bbox.ctr - new_bbox.ctr # Vector to move object ctr to drawing ctr. - move_tx = Tx(dx=move.x, dy=move.y) - - # The final transformation matrix will scale the object's real coords, - # flip the Y coord, and then move the object to the center of the drawing area. - tx = scale_tx * flip_tx * move_tx - - # Initialize drawing area. - pygame.init() - scr = pygame.display.set_mode((scr_bbox.w, scr_bbox.h)) - - # Set font for text rendering. - font = pygame.freetype.SysFont("consolas", 24) - - # Clear drawing area. - draw_clear(scr) - - # Return drawing screen, transformation matrix, and font. - return scr, tx, font - - -def draw_redraw(): - """Redraw the PyGame display.""" - import pygame - pygame.display.flip() - - -def draw_pause(): - """Pause drawing and then resume after button press.""" - import pygame - - # Display drawing. - draw_redraw() - - # Wait for user to press a key or close PyGame window. - running = True - while running: - for event in pygame.event.get(): - if event.type in (pygame.KEYDOWN, pygame.QUIT): - running = False - - -def draw_end(): - """Display drawing and wait for user to close PyGame window.""" - import pygame - draw_pause() - pygame.quit() diff --git a/src/faebryk/exporters/schematic/kicad/geometry.py b/src/faebryk/exporters/schematic/kicad/geometry.py deleted file mode 100644 index 3006c069..00000000 --- a/src/faebryk/exporters/schematic/kicad/geometry.py +++ /dev/null @@ -1,485 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - -from math import sqrt, sin, cos, radians -from copy import copy - -from ..utilities import export_to_all - -__all__ = [ - "mms_per_mil", - "mils_per_mm", - "Vector", - "tx_rot_0", - "tx_rot_90", - "tx_rot_180", - "tx_rot_270", - "tx_flip_x", - "tx_flip_y", -] - - -""" -Stuff for handling geometry: - transformation matrices, - points, - bounding boxes, - line segments. -""" - -# Millimeters/thousandths-of-inch conversion factor. -mils_per_mm = 39.37008 -mms_per_mil = 0.0254 - - -@export_to_all -def to_mils(mm): - """Convert millimeters to thousandths-of-inch and return.""" - return mm * mils_per_mm - - -@export_to_all -def to_mms(mils): - """Convert thousandths-of-inch to millimeters and return.""" - return mils * mms_per_mil - - -@export_to_all -class Tx: - def __init__(self, a=1, b=0, c=0, d=1, dx=0, dy=0): - """Create a transformation matrix. - tx = [ - a b 0 - c d 0 - dx dy 1 - ] - x' = a*x + c*y + dx - y' = b*x + d*y + dy - """ - self.a = a - self.b = b - self.c = c - self.d = d - self.dx = dx - self.dy = dy - - @classmethod - def from_symtx(cls, symtx: str): - """Return a Tx() object that implements the "HVLR" geometric operation sequence. - - Args: - symtx (str): A string of H, V, L, R operations that are applied in sequence left-to-right. - - Returns: - Tx: A transformation matrix that implements the sequence of geometric operations. - """ - op_dict = { - "H": Tx(a=-1, c=0, b=0, d=1), # Horizontal flip. - "V": Tx(a=1, c=0, b=0, d=-1), # Vertical flip. - "L": Tx(a=0, c=-1, b=1, d=0), # Rotate 90 degrees left (counter-clockwise). - "R": Tx(a=0, c=1, b=-1, d=0), # Rotate 90 degrees right (clockwise). - } - - tx = Tx() - for op in symtx.upper(): - tx *= op_dict[op] - return tx - - def __repr__(self): - return "{self.__class__}({self.a}, {self.b}, {self.c}, {self.d}, {self.dx}, {self.dy})".format( - self=self - ) - - def __str__(self): - return "[{self.a}, {self.b}, {self.c}, {self.d}, {self.dx}, {self.dy}]".format( - self=self - ) - - def __mul__(self, m): - """Return the product of two transformation matrices.""" - if isinstance(m, Tx): - tx = m - else: - # Assume m is a scalar, so convert it to a scaling Tx matrix. - tx = Tx(a=m, d=m) - return Tx( - a=self.a * tx.a + self.b * tx.c, - b=self.a * tx.b + self.b * tx.d, - c=self.c * tx.a + self.d * tx.c, - d=self.c * tx.b + self.d * tx.d, - dx=self.dx * tx.a + self.dy * tx.c + tx.dx, - dy=self.dx * tx.b + self.dy * tx.d + tx.dy, - ) - - @property - def origin(self): - """Return the (dx, dy) translation as a Point.""" - return Point(self.dx, self.dy) - - # This setter doesn't work in Python 2.7.18. - # @origin.setter - # def origin(self, pt): - # """Set the (dx, dy) translation from an (x,y) Point.""" - # self.dx, self.dy = pt.x, pt.y - - @property - def scale(self): - """Return the scaling factor.""" - return (Point(1, 0) * self - Point(0, 0) * self).magnitude - - def move(self, vec): - """Return Tx with movement vector applied.""" - return self * Tx(dx=vec.x, dy=vec.y) - - def rot_90cw(self): - """Return Tx with 90-deg clock-wise rotation around (0, 0).""" - return self * Tx(a=0, b=1, c=-1, d=0) - - def rot(self, degs): - """Return Tx rotated by the given angle (in degrees).""" - rads = radians(degs) - return self * Tx(a=cos(rads), b=sin(rads), c=-sin(rads), d=cos(rads)) - - def flip_x(self): - """Return Tx with X coords flipped around (0, 0).""" - return self * Tx(a=-1) - - def flip_y(self): - """Return Tx with Y coords flipped around (0, 0).""" - return self * Tx(d=-1) - - def no_translate(self): - """Return Tx with translation set to (0,0).""" - return Tx(a=self.a, b=self.b, c=self.c, d=self.d) - - -# Some common rotations. -tx_rot_0 = Tx(a=1, b=0, c=0, d=1) -tx_rot_90 = Tx(a=0, b=1, c=-1, d=0) -tx_rot_180 = Tx(a=-1, b=0, c=0, d=-1) -tx_rot_270 = Tx(a=0, b=-1, c=1, d=0) - -# Some common flips. -tx_flip_x = Tx(a=-1, b=0, c=0, d=1) -tx_flip_y = Tx(a=1, b=0, c=0, d=-1) - - -@export_to_all -class Point: - def __init__(self, x, y): - """Create a Point with coords x,y.""" - self.x = x - self.y = y - - def __hash__(self): - """Return hash of X,Y tuple.""" - return hash((self.x, self.y)) - - def __eq__(self, other): - """Return true if (x,y) tuples of self and other are the same.""" - return (self.x, self.y) == (other.x, other.y) - - def __lt__(self, other): - """Return true if (x,y) tuple of self compares as less than (x,y) tuple of other.""" - return (self.x, self.y) < (other.x, other.y) - - def __ne__(self, other): - """Return true if (x,y) tuples of self and other differ.""" - return not (self == other) - - def __add__(self, pt): - """Add the x,y coords of pt to self and return the resulting Point.""" - if not isinstance(pt, Point): - pt = Point(pt, pt) - return Point(self.x + pt.x, self.y + pt.y) - - def __sub__(self, pt): - """Subtract the x,y coords of pt from self and return the resulting Point.""" - if not isinstance(pt, Point): - pt = Point(pt, pt) - return Point(self.x - pt.x, self.y - pt.y) - - def __mul__(self, m): - """Apply transformation matrix or scale factor to a point and return a point.""" - if isinstance(m, Tx): - return Point( - self.x * m.a + self.y * m.c + m.dx, self.x * m.b + self.y * m.d + m.dy - ) - elif isinstance(m, Point): - return Point(self.x * m.x, self.y * m.y) - else: - return Point(m * self.x, m * self.y) - - def __rmul__(self, m): - if isinstance(m, Tx): - raise ValueError - else: - return self * m - - def xprod(self, pt): - """Cross-product of two 2D vectors returns scalar in Z coord.""" - return self.x * pt.y - self.y * pt.x - - def mask(self, msk): - """Multiply the X & Y coords by the elements of msk.""" - return Point(self.x * msk[0], self.y * msk[1]) - - def __neg__(self): - """Negate both coords.""" - return Point(-self.x, -self.y) - - def __truediv__(self, d): - """Divide the x,y coords by d.""" - return Point(self.x / d, self.y / d) - - def __div__(self, d): - """Divide the x,y coords by d.""" - return Point(self.x / d, self.y / d) - - def round(self): - return Point(int(round(self.x)), int(round(self.y))) - - def __str__(self): - return "{} {}".format(self.x, self.y) - - def snap(self, grid_spacing): - """Snap point x,y coords to the given grid spacing.""" - snap_func = lambda x: int(grid_spacing * round(x / grid_spacing)) - return Point(snap_func(self.x), snap_func(self.y)) - - def min(self, pt): - """Return a Point with coords that are the min x,y of both points.""" - return Point(min(self.x, pt.x), min(self.y, pt.y)) - - def max(self, pt): - """Return a Point with coords that are the max x,y of both points.""" - return Point(max(self.x, pt.x), max(self.y, pt.y)) - - @property - def magnitude(self): - """Get the distance of the point from the origin.""" - return sqrt(self.x**2 + self.y**2) - - @property - def norm(self): - """Return a unit vector pointing from the origin to the point.""" - try: - return self / self.magnitude - except ZeroDivisionError: - return Point(0, 0) - - def flip_xy(self): - """Flip X-Y coordinates of point.""" - self.x, self.y = self.y, self.x - - def __repr__(self): - return "{self.__class__}({self.x}, {self.y})".format(self=self) - - def __str__(self): - return "({}, {})".format(self.x, self.y) - - -Vector = Point - - -@export_to_all -class BBox: - def __init__(self, *pts): - """Create a bounding box surrounding the given points.""" - inf = float("inf") - self.min = Point(inf, inf) - self.max = Point(-inf, -inf) - self.add(*pts) - - def __add__(self, obj): - """Return the merged BBox of two BBoxes or a BBox and a Point.""" - sum_ = BBox() - if isinstance(obj, Point): - sum_.min = self.min.min(obj) - sum_.max = self.max.max(obj) - elif isinstance(obj, BBox): - sum_.min = self.min.min(obj.min) - sum_.max = self.max.max(obj.max) - else: - raise NotImplementedError - return sum_ - - def __iadd__(self, obj): - """Update BBox bt adding another Point or BBox""" - sum_ = self + obj - self.min = sum_.min - self.max = sum_.max - return self - - def add(self, *objs): - """Update the bounding box size by adding Point/BBox objects.""" - for obj in objs: - self += obj - return self - - def __mul__(self, m): - return BBox(self.min * m, self.max * m) - - def round(self): - return BBox(self.min.round(), self.max.round()) - - def is_inside(self, pt): - """Return True if point is inside bounding box.""" - return (self.min.x <= pt.x <= self.max.x) and (self.min.y <= pt.y <= self.max.y) - - def intersects(self, bbox): - """Return True if the two bounding boxes intersect.""" - return ( - (self.min.x < bbox.max.x) - and (self.max.x > bbox.min.x) - and (self.min.y < bbox.max.y) - and (self.max.y > bbox.min.y) - ) - - def intersection(self, bbox): - """Return the bounding box of the intersection between the two bounding boxes.""" - if not self.intersects(bbox): - return None - corner1 = self.min.max(bbox.min) - corner2 = self.max.min(bbox.max) - return BBox(corner1, corner2) - - def resize(self, vector): - """Expand/contract the bounding box by applying vector to its corner points.""" - return BBox(self.min - vector, self.max + vector) - - def snap_resize(self, grid_spacing): - """Resize bbox so max and min points are on grid. - - Args: - grid_spacing (float): Grid spacing. - """ - bbox = self.resize(Point(grid_spacing - 1, grid_spacing - 1)) - bbox.min = bbox.min.snap(grid_spacing) - bbox.max = bbox.max.snap(grid_spacing) - return bbox - - @property - def area(self): - """Return area of bounding box.""" - return self.w * self.h - - @property - def w(self): - """Return the bounding box width.""" - return abs(self.max.x - self.min.x) - - @property - def h(self): - """Return the bounding box height.""" - return abs(self.max.y - self.min.y) - - @property - def ctr(self): - """Return center point of bounding box.""" - return (self.max + self.min) / 2 - - @property - def ll(self): - """Return lower-left point of bounding box.""" - return Point(self.min.x, self.min.y) - - @property - def lr(self): - """Return lower-right point of bounding box.""" - return Point(self.max.x, self.min.y) - - @property - def ul(self): - """Return upper-left point of bounding box.""" - return Point(self.min.x, self.max.y) - - @property - def ur(self): - """Return upper-right point of bounding box.""" - return Point(self.max.x, self.max.y) - - def __repr__(self): - return "{self.__class__}(Point({self.min}), Point({self.max}))".format( - self=self - ) - - def __str__(self): - return "[{}, {}]".format(self.min, self.max) - - -@export_to_all -class Segment: - def __init__(self, p1, p2): - "Create a line segment between two points." - self.p1 = copy(p1) - self.p2 = copy(p2) - - def __mul__(self, m): - """Apply transformation matrix to a segment and return a segment.""" - return Segment(self.p1 * m, self.p2 * m) - - def round(self): - return Segment(self.p1.round(), self.p2.round()) - - def __str__(self): - return "{} {}".format(str(self.p1), str(self.p2)) - - def flip_xy(self): - """Flip the X-Y coordinates of the segment.""" - self.p1.flip_xy() - self.p2.flip_xy() - - def intersects(self, other): - """Return true if the segments intersect.""" - - # FIXME: This fails if the segments are parallel! - raise NotImplementedError - - # Given two segments: - # self: p1 + (p2-p1) * t1 - # other: p3 + (p4-p3) * t2 - # Look for a solution t1, t2 that solves: - # p1x + (p2x-p1x)*t1 = p3x + (p4x-p3x)*t2 - # p1y + (p2y-p1y)*t1 = p3y + (p4y-p3y)*t2 - # If t1 and t2 are both in range [0,1], then the two segments intersect. - - p1x, p1y, p2x, p2y = self.p1.x, self.p1.y, self.p2.x, self.p2.y - p3x, p3y, p4x, p4y = other.p1.x, other.p1.y, other.p2.x, other.p2.y - - # denom = p1x*p3y - p1x*p4y - p1y*p3x + p1y*p4x - p2x*p3y + p2x*p4y + p2y*p3x - p2y*p4x - # denom = p1x * (p3y - p4y) + p1y * (p4x - p3x) + p2x * (p4y - p3y) + p2y * (p3x - p4x) - denom = (p1x - p2x) * (p3y - p4y) + (p1y - p2y) * (p4x - p3x) - - try: - # t1 = (p1x*p3y - p1x*p4y - p1y*p3x + p1y*p4x + p3x*p4y - p3y*p4x) / denom - # t2 = (-p1x*p2y + p1x*p3y + p1y*p2x - p1y*p3x - p2x*p3y + p2y*p3x) / denom - t1 = ((p1y - p3y) * (p4x - p3x) - (p1x - p3x) * (p4y - p3y)) / denom - t2 = ((p1y - p3y) * (p2x - p3x) - (p1x - p3x) * (p2y - p3y)) / denom - except ZeroDivisionError: - return False - - return (0 <= t1 <= 1) and (0 <= t2 <= 1) - - def shadows(self, other): - """Return true if two segments overlap each other even if they aren't on the same horiz or vertical track.""" - - if self.p1.x == self.p2.x and other.p1.x == other.p2.x: - # Horizontal segments. See if their vertical extents overlap. - self_min = min(self.p1.y, self.p2.y) - self_max = max(self.p1.y, self.p2.y) - other_min = min(other.p1.y, other.p2.y) - other_max = max(other.p1.y, other.p2.y) - elif self.p1.y == self.p2.y and other.p1.y == other.p2.y: - # Verttical segments. See if their horizontal extents overlap. - self_min = min(self.p1.x, self.p2.x) - self_max = max(self.p1.x, self.p2.x) - other_min = min(other.p1.x, other.p2.x) - other_max = max(other.p1.x, other.p2.x) - else: - # Segments aren't horizontal or vertical, so neither can shadow the other. - return False - - # Overlap conditions based on segment endpoints. - return other_min < self_max and other_max > self_min diff --git a/src/faebryk/exporters/schematic/kicad/net_terminal.py b/src/faebryk/exporters/schematic/kicad/net_terminal.py deleted file mode 100644 index e1d9e869..00000000 --- a/src/faebryk/exporters/schematic/kicad/net_terminal.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - -from skidl import Part, Pin -from skidl.utilities import export_to_all -from .geometry import Point, Tx, Vector - - -""" -Net_Terminal class for handling net labels. -""" - - -@export_to_all -class NetTerminal(Part): - def __init__(self, net, tool_module): - """Specialized Part with a single pin attached to a net. - - This is intended for attaching to nets to label them, typically when - the net spans across levels of hierarchical nodes. - """ - - # Create a Part. - from skidl import SKIDL - - super().__init__(name="NT", ref_prefix="NT", tool=SKIDL) - - # Set a default transformation matrix for this part. - self.tx = Tx() - - # Add a single pin to the part. - pin = Pin(num="1", name="~") - self.add_pins(pin) - - # Connect the pin to the net. - pin += net - - # Set the pin at point (0,0) and pointing leftward toward the part body - # (consisting of just the net label for this type of part) so any attached routing - # will go to the right. - pin.x, pin.y = 0, 0 - pin.pt = Point(pin.x, pin.y) - pin.orientation = "L" - - # Calculate the bounding box, but as if the pin were pointed right so - # the associated label text would go to the left. - self.bbox = tool_module.calc_hier_label_bbox(net.name, "R") - - # Resize bbox so it's an integer number of GRIDs. - self.bbox = self.bbox.snap_resize(tool_module.constants.GRID) - - # Extend the bounding box a bit so any attached routing will come straight in. - self.bbox.max += Vector(tool_module.constants.GRID, 0) - self.lbl_bbox = self.bbox - - # Flip the NetTerminal horizontally if it is an output net (label on the right). - netio = getattr(net, "netio", "").lower() - self.orientation_locked = bool(netio in ("i", "o")) - if getattr(net, "netio", "").lower() == "o": - origin = Point(0, 0) - term_origin = self.tx.origin - self.tx = ( - self.tx.move(origin - term_origin).flip_x().move(term_origin - origin) - ) diff --git a/src/faebryk/exporters/schematic/kicad/node.py b/src/faebryk/exporters/schematic/kicad/node.py deleted file mode 100644 index 9c658051..00000000 --- a/src/faebryk/exporters/schematic/kicad/node.py +++ /dev/null @@ -1,354 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - -import re -from collections import defaultdict -from itertools import chain - -from skidl.utilities import export_to_all, rmv_attr -from .geometry import BBox, Point, Tx, Vector -from .place import Placer -from .route import Router - - -""" -Node class for storing circuit hierarchy. -""" - - -@export_to_all -class Node(Placer, Router): - """Data structure for holding information about a node in the circuit hierarchy.""" - - filename_sz = 20 - name_sz = 40 - - def __init__( - self, - circuit=None, - tool_module=None, - filepath=".", - top_name="", - title="", - flatness=0.0, - ): - self.parent = None - self.children = defaultdict( - lambda: Node(None, tool_module, filepath, top_name, title, flatness) - ) - self.filepath = filepath - self.top_name = top_name - self.sheet_name = None - self.sheet_filename = None - self.title = title - self.flatness = flatness - self.flattened = False - self.tool_module = tool_module # Backend tool. - self.parts = [] - self.wires = defaultdict(list) - self.junctions = defaultdict(list) - self.tx = Tx() - self.bbox = BBox() - - if circuit: - self.add_circuit(circuit) - - def find_node_with_part(self, part): - """Find the node that contains the part based on its hierarchy. - - Args: - part (Part): The part being searched for in the node hierarchy. - - Returns: - Node: The Node object containing the part. - """ - - from skidl.circuit import HIER_SEP - - level_names = part.hierarchy.split(HIER_SEP) - node = self - for lvl_nm in level_names[1:]: - node = node.children[lvl_nm] - assert part in node.parts - return node - - def add_circuit(self, circuit): - """Add parts in circuit to node and its children. - - Args: - circuit (Circuit): Circuit object. - """ - - # Build the circuit node hierarchy by adding the parts. - for part in circuit.parts: - self.add_part(part) - - # Add terminals to nodes in the hierarchy for nets that span across nodes. - for net in circuit.nets: - # Skip nets that are stubbed since there will be no wire to attach to the NetTerminal. - if getattr(net, "stub", False): - continue - - # Search for pins in different nodes. - for pin1, pin2 in zip(net.pins[:-1], net.pins[1:]): - if pin1.part.hierarchy != pin2.part.hierarchy: - # Found pins in different nodes, so break and add terminals to nodes below. - break - else: - if len(net.pins) == 1: - # Single pin on net and not stubbed, so add a terminal to it below. - pass - elif not net.is_implicit(): - # The net has a user-assigned name, so add a terminal to it below. - pass - else: - # No need for net terminal because there are multiple pins - # and they are all in the same node. - continue - - # Add a single terminal to each node that contains one or more pins of the net. - visited = [] - for pin in net.pins: - # A stubbed pin can't be used to add NetTerminal since there is no explicit wire. - if pin.stub: - continue - - part = pin.part - - if part.hierarchy in visited: - # Already added a terminal to this node, so don't add another. - continue - - # Add NetTerminal to the node with this part/pin. - self.find_node_with_part(part).add_terminal(net) - - # Record that this hierarchical node was visited. - visited.append(part.hierarchy) - - # Flatten the hierarchy as specified by the flatness parameter. - self.flatten(self.flatness) - - def add_part(self, part, level=0): - """Add a part to the node at the appropriate level of the hierarchy. - - Args: - part (Part): Part to be added to this node or one of its children. - level (int, optional): The current level (depth) of the node in the hierarchy. Defaults to 0. - """ - - from skidl.circuit import HIER_SEP - - # Get list of names of hierarchical levels (in order) leading to this part. - level_names = part.hierarchy.split(HIER_SEP) - - # Get depth in hierarchy for this part. - part_level = len(level_names) - 1 - assert part_level >= level - - # Node name is the name assigned to this level of the hierarchy. - self.name = level_names[level] - - # File name for storing the schematic for this node. - base_filename = "_".join([self.top_name] + level_names[0 : level + 1]) + ".sch" - self.sheet_filename = base_filename - - if part_level == level: - # Add part to node at this level in the hierarchy. - if not part.unit: - # Monolithic part so just add it to the node. - self.parts.append(part) - else: - # Multi-unit part so add each unit to the node. - # FIXME: Some part units might be split into other nodes. - for p in part.unit.values(): - self.parts.append(p) - else: - # Part is at a level below the current node. Get the child node using - # the name of the next level in the hierarchy for this part. - child_node = self.children[level_names[level + 1]] - - # Attach the child node to this node. (It may have just been created.) - child_node.parent = self - - # Add part to the child node (or one of its children). - child_node.add_part(part, level + 1) - - def add_terminal(self, net): - """Add a terminal for this net to the node. - - Args: - net (Net): The net to be added to this node. - """ - - from skidl.circuit import HIER_SEP - from .net_terminal import NetTerminal - - nt = NetTerminal(net, self.tool_module) - self.parts.append(nt) - - def external_bbox(self): - """Return the bounding box of a hierarchical sheet as seen by its parent node.""" - bbox = BBox(Point(0, 0), Point(500, 500)) - bbox.add(Point(len("File: " + self.sheet_filename) * self.filename_sz, 0)) - bbox.add(Point(len("Sheet: " + self.name) * self.name_sz, 0)) - - # Pad the bounding box for extra spacing when placed. - bbox = bbox.resize(Vector(100, 100)) - - return bbox - - def internal_bbox(self): - """Return the bounding box for the circuitry contained within this node.""" - - # The bounding box is determined by the arrangement of the node's parts and child nodes. - bbox = BBox() - for obj in chain(self.parts, self.children.values()): - tx_bbox = obj.bbox * obj.tx - bbox.add(tx_bbox) - - # Pad the bounding box for extra spacing when placed. - bbox = bbox.resize(Vector(100, 100)) - - return bbox - - def calc_bbox(self): - """Compute the bounding box for the node in the circuit hierarchy.""" - - if self.flattened: - self.bbox = self.internal_bbox() - else: - # Use hierarchical bounding box if node has not been flattened. - self.bbox = self.external_bbox() - - return self.bbox - - def flatten(self, flatness=0.0): - """Flatten node hierarchy according to flatness parameter. - - Args: - flatness (float, optional): Degree of hierarchical flattening (0=completely hierarchical, 1=totally flat). Defaults to 0.0. - - Create hierarchical sheets for the node and its child nodes. Complexity (or size) of a node - and its children is the total number of part pins they contain. The sum of all the child sizes - multiplied by the flatness is the number of part pins that can be shown on the schematic - page before hierarchy is used. The instances of each type of child are flattened and placed - directly in the sheet as long as the sum of their sizes is below the slack. Otherwise, the - children are included using hierarchical sheets. The children are handled in order of - increasing size so small children are more likely to be flattened while large, complicated - children are included using hierarchical sheets. - """ - - # Create sheets and compute complexity for any circuitry in hierarchical child nodes. - for child in self.children.values(): - child.flatten(flatness) - - # Complexity of the parts directly instantiated at this hierarchical level. - self.complexity = sum((len(part) for part in self.parts)) - - # Sum the child complexities and use it to compute the number of pins that can be - # shown before hierarchical sheets are used. - child_complexity = sum((child.complexity for child in self.children.values())) - slack = child_complexity * flatness - - # Group the children according to what types of modules they are by removing trailing instance ids. - child_types = defaultdict(list) - for child_id, child in self.children.items(): - child_types[re.sub(r"\d+$", "", child_id)].append(child) - - # Compute the total size of each type of children. - child_type_sizes = dict() - for child_type, children in child_types.items(): - child_type_sizes[child_type] = sum((child.complexity for child in children)) - - # Sort the groups from smallest total size to largest. - sorted_child_type_sizes = sorted( - child_type_sizes.items(), key=lambda item: item[1] - ) - - # Flatten each instance in a group until the slack is used up. - for child_type, child_type_size in sorted_child_type_sizes: - if child_type_size <= slack: - # Include the circuitry of each child instance directly in the sheet. - for child in child_types[child_type]: - child.flattened = True - # Reduce the slack by the sum of the child sizes. - slack -= child_type_size - else: - # Not enough slack left. Add these children as hierarchical sheets. - for child in child_types[child_type]: - child.flattened = False - - def get_internal_nets(self): - """Return a list of nets that have at least one pin on a part in this node.""" - - processed_nets = [] - internal_nets = [] - for part in self.parts: - for part_pin in part: - # No explicit wire for pins connected to labeled stub nets. - if part_pin.stub: - continue - - # No explicit wires if the pin is not connected to anything. - if not part_pin.is_connected(): - continue - - net = part_pin.net - - # Skip nets that have already been processed. - if net in processed_nets: - continue - - processed_nets.append(net) - - # Skip stubbed nets. - if getattr(net, "stub", False) is True: - continue - - # Add net to collection if at least one pin is on one of the parts of the node. - for net_pin in net.pins: - if net_pin.part in self.parts: - internal_nets.append(net) - break - - return internal_nets - - def get_internal_pins(self, net): - """Return the pins on the net that are on parts in the node. - - Args: - net (Net): The net whose pins are being examined. - - Returns: - list: List of pins on the net that are on parts in this node. - """ - - # Skip pins on stubbed nets. - if getattr(net, "stub", False) is True: - return [] - - return [pin for pin in net.pins if pin.stub is False and pin.part in self.parts] - - def collect_stats(self, **options): - """Return comma-separated string with place & route statistics of a schematic.""" - - def get_wire_length(node): - """Return the sum of the wire segment lengths between parts in a routed node.""" - - wire_length = 0 - - # Sum wire lengths for child nodes. - for child in node.children.values(): - wire_length += get_wire_length(child) - - # Add the wire lengths between parts in the top node. - for wire_segs in node.wires.values(): - for seg in wire_segs: - len_x = abs(seg.p1.x - seg.p2.x) - len_y = abs(seg.p1.y - seg.p2.y) - wire_length += len_x + len_y - - return wire_length - - return "{}\n".format(get_wire_length(self)) diff --git a/src/faebryk/exporters/schematic/kicad/place.py b/src/faebryk/exporters/schematic/kicad/place.py deleted file mode 100644 index 38e7b1b0..00000000 --- a/src/faebryk/exporters/schematic/kicad/place.py +++ /dev/null @@ -1,1554 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - -""" -Autoplacer for arranging symbols in a schematic. -""" - -import functools -import itertools -import math -import random -import sys -from collections import defaultdict -from copy import copy - -from skidl import Pin, rmv_attr - -import faebryk.library._F as F - -from .debug_draw import ( - draw_pause, - draw_placement, - draw_redraw, - draw_start, - draw_text, -) -from .geometry import BBox, Point, Tx, Vector - -__all__ = [ - "PlacementFailure", -] - - -################################################################### -# -# OVERVIEW OF AUTOPLACER -# -# The input is a Node containing child nodes and parts. The parts in -# each child node are placed, and then the blocks for each child are -# placed along with the parts in this node. -# -# The individual parts in a node are separated into groups: -# 1) multiple groups of parts that are all interconnected by one or -# more nets, and 2) a single group of parts that are not connected -# by any explicit nets (i.e., floating parts). -# -# Each group of connected parts are placed using force-directed placement. -# Each net exerts an attractive force pulling parts together, and -# any overlap of parts exerts a repulsive force pushing them apart. -# Initially, the attractive force is dominant but, over time, it is -# decreased while the repulsive force is increased using a weighting -# factor. After that, any part overlaps are cleared and the parts -# are aligned to the routing grid. -# -# Force-directed placement is also used with the floating parts except -# the non-existent net forces are replaced by a measure of part similarity. -# This collects similar parts (such as bypass capacitors) together. -# -# The child-node blocks are then arranged with the blocks of connected -# and floating parts to arrive at a total placement for this node. -# -################################################################### - - -class PlacementFailure(Exception): - """Exception raised when parts or blocks could not be placed.""" - - pass - - -# Small functions for summing Points and Vectors. -pt_sum = lambda pts: sum(pts, Point(0, 0)) -force_sum = lambda forces: sum(forces, Vector(0, 0)) - - -def is_net_terminal(part): - from skidl.schematics.net_terminal import NetTerminal - - return isinstance(part, NetTerminal) - - -def get_snap_pt(part_or_blk): - """Get the point for snapping the Part or PartBlock to the grid. - - Args: - part_or_blk (Part | PartBlock): Object with snap point. - - Returns: - Point: Point for snapping to grid or None if no point found. - """ - try: - return part_or_blk.pins[0].pt - except AttributeError: - try: - return part_or_blk.snap_pt - except AttributeError: - return None - - -def snap_to_grid(part_or_blk): - """Snap Part or PartBlock to grid. - - Args: - part (Part | PartBlk): Object to snap to grid. - """ - - # Get the position of the current snap point. - pt = get_snap_pt(part_or_blk) * part_or_blk.tx - - # This is where the snap point should be on the grid. - snap_pt = pt.snap(GRID) - - # This is the required movement to get on-grid. - mv = snap_pt - pt - - # Update the object's transformation matrix. - snap_tx = Tx(dx=mv.x, dy=mv.y) - part_or_blk.tx *= snap_tx - - -def add_placement_bboxes(parts, **options): - """Expand part bounding boxes to include space for subsequent routing.""" - from skidl.schematics.net_terminal import NetTerminal - - for part in parts: - # Placement bbox starts off with the part bbox (including any net labels). - part.place_bbox = BBox() - part.place_bbox.add(part.lbl_bbox) - - # Compute the routing area for each side based on the number of pins on each side. - padding = {"U": 1, "D": 1, "L": 1, "R": 1} # Min padding of 1 channel per side. - for pin in part: - if pin.stub is False and pin.is_connected(): - padding[pin.orientation] += 1 - - # expansion_factor > 1 is used to expand the area for routing around each part, - # usually in response to a failed routing phase. But don't expand the routing - # around NetTerminals since those are just used to label wires. - if isinstance(part, NetTerminal): - expansion_factor = 1 - else: - expansion_factor = options.get("expansion_factor", 1.0) - - # Add padding for routing to the right and upper sides. - part.place_bbox.add( - part.place_bbox.max - + (Point(padding["L"], padding["D"]) * GRID * expansion_factor) - ) - - # Add padding for routing to the left and lower sides. - part.place_bbox.add( - part.place_bbox.min - - (Point(padding["R"], padding["U"]) * GRID * expansion_factor) - ) - - -def get_enclosing_bbox(parts): - """Return bounding box that encloses all the parts.""" - return BBox().add(*(part.place_bbox * part.tx for part in parts)) - - -def add_anchor_pull_pins(parts, nets, **options): - """Add positions of anchor and pull pins for attractive net forces between parts. - - Args: - part (list): List of movable parts. - nets (list): List of attractive nets between parts. - options (dict): Dict of options and values that enable/disable functions. - """ - - def add_place_pt(part, pin): - """Add the point for a pin on the placement boundary of a part.""" - - pin.route_pt = pin.pt # For drawing of nets during debugging. - pin.place_pt = Point(pin.pt.x, pin.pt.y) - if pin.orientation == "U": - pin.place_pt.y = part.place_bbox.min.y - elif pin.orientation == "D": - pin.place_pt.y = part.place_bbox.max.y - elif pin.orientation == "L": - pin.place_pt.x = part.place_bbox.max.x - elif pin.orientation == "R": - pin.place_pt.x = part.place_bbox.min.x - else: - raise RuntimeError("Unknown pin orientation.") - - # Remove any existing anchor and pull pins before making new ones. - rmv_attr(parts, ("anchor_pins", "pull_pins")) - - # Add dicts for anchor/pull pins and pin centroids to each movable part. - for part in parts: - part.anchor_pins = defaultdict(list) - part.pull_pins = defaultdict(list) - part.pin_ctrs = dict() - - if nets: - # If nets exist, then these parts are interconnected so - # assign pins on each net to part anchor and pull pin lists. - for net in nets: - # Get net pins that are on movable parts. - pins = {pin for pin in net.pins if pin.part in parts} - - # Get the set of parts with pins on the net. - net.parts = {pin.part for pin in pins} - - # Add each pin as an anchor on the part that contains it and - # as a pull pin on all the other parts that will be pulled by this part. - for pin in pins: - pin.part.anchor_pins[net].append(pin) - add_place_pt(pin.part, pin) - for part in net.parts - {pin.part}: - # NetTerminals are pulled towards connected parts, but - # those parts are not attracted towards NetTerminals. - if not is_net_terminal(pin.part): - part.pull_pins[net].append(pin) - - # For each net, assign the centroid of the part's anchor pins for that net. - for net in nets: - for part in net.parts: - if part.anchor_pins[net]: - part.pin_ctrs[net] = pt_sum( - pin.place_pt for pin in part.anchor_pins[net] - ) / len(part.anchor_pins[net]) - - else: - # There are no nets so these parts are floating freely. - # Floating parts are all pulled by each other. - all_pull_pins = [] - for part in parts: - try: - # Set anchor at top-most pin so floating part tops will align. - anchor_pull_pin = max(part.pins, key=lambda pin: pin.pt.y) - add_place_pt(part, anchor_pull_pin) - except ValueError: - # Set anchor for part with no pins at all. - anchor_pull_pin = Pin() - anchor_pull_pin.place_pt = part.place_bbox.max - part.anchor_pins["similarity"] = [anchor_pull_pin] - part.pull_pins["similarity"] = all_pull_pins - all_pull_pins.append(anchor_pull_pin) - - -def save_anchor_pull_pins(parts): - """Save anchor/pull pins for each part before they are changed.""" - for part in parts: - part.saved_anchor_pins = copy(part.anchor_pins) - part.saved_pull_pins = copy(part.pull_pins) - - -def restore_anchor_pull_pins(parts): - """Restore the original anchor/pull pin lists for each Part.""" - - for part in parts: - if hasattr(part, "saved_anchor_pins"): - # Saved pin lists exist, so restore them to the original anchor/pull pin lists. - part.anchor_pins = part.saved_anchor_pins - part.pull_pins = part.saved_pull_pins - - # Remove the attributes where the original lists were saved. - rmv_attr(parts, ("saved_anchor_pins", "saved_pull_pins")) - - -def adjust_orientations(parts, **options): - """Adjust orientation of parts. - - Args: - parts (list): List of Parts to adjust. - options (dict): Dict of options and values that enable/disable functions. - - Returns: - bool: True if one or more part orientations were changed. Otherwise, False. - """ - - def find_best_orientation(part): - """Each part has 8 possible orientations. Find the best of the 7 alternatives from the starting one.""" - - # Store starting orientation. - part.prev_tx = copy(part.tx) - - # Get centerpoint of part for use when doing rotations/flips. - part_ctr = (part.place_bbox * part.tx).ctr - - # Now find the orientation that has the largest decrease (or smallest increase) in cost. - # Go through four rotations, then flip the part and go through the rotations again. - best_delta_cost = float("inf") - calc_starting_cost = True - for i in range(2): - for j in range(4): - if calc_starting_cost: - # Calculate the cost of the starting orientation before any changes in orientation. - starting_cost = net_tension(part, **options) - # Skip the starting orientation but set flag to process the others. - calc_starting_cost = False - else: - # Calculate the cost of the current orientation. - delta_cost = net_tension(part, **options) - starting_cost - if delta_cost < best_delta_cost: - # Save the largest decrease in cost and the associated orientation. - best_delta_cost = delta_cost - best_tx = copy(part.tx) - - # Proceed to the next rotation. - part.tx = part.tx.move(-part_ctr).rot_90cw().move(part_ctr) - - # Flip the part and go through the rotations again. - part.tx = part.tx.move(-part_ctr).flip_x().move(part_ctr) - - # Save the largest decrease in cost and the associated orientation. - part.delta_cost = best_delta_cost - part.delta_cost_tx = best_tx - - # Restore the original orientation. - part.tx = part.prev_tx - - # Get the list of parts that don't have their orientations locked. - movable_parts = [part for part in parts if not part.orientation_locked] - - if not movable_parts: - # No movable parts, so exit without doing anything. - return - - # Kernighan-Lin algorithm for finding near-optimal part orientations. - # Because of the way the tension for part alignment is computed based on - # the nearest part, it is possible for an infinite loop to occur. - # Hence the ad-hoc loop limit. - for iter_cnt in range(10): - # Find the best part to move and move it until there are no more parts to move. - moved_parts = [] - unmoved_parts = movable_parts[:] - while unmoved_parts: - # Find the best current orientation for each unmoved part. - for part in unmoved_parts: - find_best_orientation(part) - - # Find the part that has the largest decrease in cost. - part_to_move = min(unmoved_parts, key=lambda p: p.delta_cost) - - # Reorient the part with the Tx that created the largest decrease in cost. - part_to_move.tx = part_to_move.delta_cost_tx - - # Transfer the part from the unmoved to the moved part list. - unmoved_parts.remove(part_to_move) - moved_parts.append(part_to_move) - - # Find the point at which the cost reaches its lowest point. - # delta_cost at location i is the change in cost *before* part i is moved. - # Start with cost change of zero before any parts are moved. - delta_costs = [ - 0, - ] - delta_costs.extend((part.delta_cost for part in moved_parts)) - try: - cost_seq = list(itertools.accumulate(delta_costs)) - except AttributeError: - # Python 2.7 doesn't have itertools.accumulate(). - cost_seq = list(delta_costs) - for i in range(1, len(cost_seq)): - cost_seq[i] = cost_seq[i - 1] + cost_seq[i] - min_cost = min(cost_seq) - min_index = cost_seq.index(min_cost) - - # Move all the parts after that point back to their starting positions. - for part in moved_parts[min_index:]: - part.tx = part.prev_tx - - # Terminate the search if no part orientations were changed. - if min_index == 0: - break - - rmv_attr(parts, ("prev_tx", "delta_cost", "delta_cost_tx")) - - # Return True if one or more iterations were done, indicating part orientations were changed. - return iter_cnt > 0 - - -def net_tension_dist(part, **options): - """Calculate the tension of the nets trying to rotate/flip the part. - - Args: - part (Part): Part affected by forces from other connected parts. - options (dict): Dict of options and values that enable/disable functions. - - Returns: - float: Total tension on the part. - """ - - # Compute the force for each net attached to the part. - tension = 0.0 - for net, anchor_pins in part.anchor_pins.items(): - pull_pins = part.pull_pins[net] - - if not anchor_pins or not pull_pins: - # Skip nets without pulling or anchor points. - continue - - # Compute the net force acting on each anchor point on the part. - for anchor_pin in anchor_pins: - # Compute the anchor point's (x,y). - anchor_pt = anchor_pin.place_pt * anchor_pin.part.tx - - # Find the dist from the anchor point to each pulling point. - dists = [ - (anchor_pt - pp.place_pt * pp.part.tx).magnitude for pp in pull_pins - ] - - # Only the closest pulling point affects the tension since that is - # probably where the wire routing will go to. - tension += min(dists) - - return tension - - -def net_torque_dist(part, **options): - """Calculate the torque of the nets trying to rotate/flip the part. - - Args: - part (Part): Part affected by forces from other connected parts. - options (dict): Dict of options and values that enable/disable functions. - - Returns: - float: Total torque on the part. - """ - - # Part centroid for computing torque. - ctr = part.place_bbox.ctr * part.tx - - # Get the force multiplier applied to point-to-point nets. - pt_to_pt_mult = options.get("pt_to_pt_mult", 1) - - # Compute the torque for each net attached to the part. - torque = 0.0 - for net, anchor_pins in part.anchor_pins.items(): - pull_pins = part.pull_pins[net] - - if not anchor_pins or not pull_pins: - # Skip nets without pulling or anchor points. - continue - - pull_pin_pts = [pin.place_pt * pin.part.tx for pin in pull_pins] - - # Multiply the force exerted by point-to-point nets. - force_mult = pt_to_pt_mult if len(pull_pin_pts) <= 1 else 1 - - # Compute the net torque acting on each anchor point on the part. - for anchor_pin in anchor_pins: - # Compute the anchor point's (x,y). - anchor_pt = anchor_pin.place_pt * part.tx - - # Compute torque around part center from force between anchor & pull pins. - normalize = len(pull_pin_pts) - lever_norm = (anchor_pt - ctr).norm - for pull_pt in pull_pin_pts: - frc_norm = (pull_pt - anchor_pt).norm - torque += lever_norm.xprod(frc_norm) * force_mult / normalize - - return abs(torque) - - -# Select the net tension method used for the adjusting the orientation of parts. -net_tension = net_tension_dist -# net_tension = net_torque_dist - - -def net_force_dist(part, **options): - """Compute attractive force on a part from all the other parts connected to it. - - Args: - part (Part): Part affected by forces from other connected parts. - options (dict): Dict of options and values that enable/disable functions. - - Returns: - Vector: Force upon given part. - """ - - # Get the anchor and pull pins for each net connected to this part. - anchor_pins = part.anchor_pins - pull_pins = part.pull_pins - - # Get the force multiplier applied to point-to-point nets. - pt_to_pt_mult = options.get("pt_to_pt_mult", 1) - - # Compute the total force on the part from all the anchor/pulling points on each net. - total_force = Vector(0, 0) - - # Parts with a lot of pins can accumulate large net forces that move them very quickly. - # Accumulate the number of individual net forces and use that to attenuate - # the total force, effectively normalizing the forces between large & small parts. - net_normalizer = 0 - - # Compute the force for each net attached to the part. - for net in anchor_pins.keys(): - if not anchor_pins[net] or not pull_pins[net]: - # Skip nets without pulling or anchor points. - continue - - # Multiply the force exerted by point-to-point nets. - force_mult = pt_to_pt_mult if len(pull_pins[net]) <= 1 else 1 - - # Initialize net force. - net_force = Vector(0, 0) - - pin_normalizer = 0 - - # Compute the anchor and pulling point (x,y)s for the net. - anchor_pts = [pin.place_pt * pin.part.tx for pin in anchor_pins[net]] - pull_pts = [pin.place_pt * pin.part.tx for pin in pull_pins[net]] - - # Compute the net force acting on each anchor point on the part. - for anchor_pt in anchor_pts: - # Sum the forces from each pulling point on the anchor point. - for pull_pt in pull_pts: - # Get the distance from the pull pt to the anchor point. - dist_vec = pull_pt - anchor_pt - - # Add the force on the anchor pin from the pulling pin. - net_force += dist_vec - - # Increment the normalizer for every pull force added to the net force. - pin_normalizer += 1 - - if options.get("pin_normalize"): - # Normalize the net force across all the anchor & pull pins. - pin_normalizer = pin_normalizer or 1 # Prevent div-by-zero. - net_force /= pin_normalizer - - # Accumulate force from this net into the total force on the part. - # Multiply force if the net meets stated criteria. - total_force += net_force * force_mult - - # Increment the normalizer for every net force added to the total force. - net_normalizer += 1 - - if options.get("net_normalize"): - # Normalize the total force across all the nets. - net_normalizer = net_normalizer or 1 # Prevent div-by-zero. - total_force /= net_normalizer - - return total_force - - -# Select the net force method used for the attraction of parts during placement. -attractive_force = net_force_dist - - -def overlap_force(part, parts, **options): - """Compute the repulsive force on a part from overlapping other parts. - - Args: - part (Part): Part affected by forces from other overlapping parts. - parts (list): List of parts to check for overlaps. - options (dict): Dict of options and values that enable/disable functions. - - Returns: - Vector: Force upon given part. - """ - - # Bounding box of given part. - part_bbox = part.place_bbox * part.tx - - # Compute the overlap force of the bbox of this part with every other part. - total_force = Vector(0, 0) - for other_part in set(parts) - {part}: - other_part_bbox = other_part.place_bbox * other_part.tx - - # No force unless parts overlap. - if part_bbox.intersects(other_part_bbox): - # Compute the movement needed to separate the bboxes in left/right/up/down directions. - # Add some small random offset to break symmetry when parts exactly overlay each other. - # Move right edge of part to the left of other part's left edge, etc... - moves = [] - rnd = Vector(random.random() - 0.5, random.random() - 0.5) - for edges, dir in ( - (("ll", "lr"), Vector(1, 0)), - (("ul", "ll"), Vector(0, 1)), - ): - move = ( - getattr(other_part_bbox, edges[0]) - - getattr(part_bbox, edges[1]) - - rnd - ) * dir - moves.append([move.magnitude, move]) - # Flip edges... - move = ( - getattr(other_part_bbox, edges[1]) - - getattr(part_bbox, edges[0]) - - rnd - ) * dir - moves.append([move.magnitude, move]) - - # Select the smallest move that separates the parts. - move = min(moves, key=lambda m: m[0]) - - # Add the move to the total force on the part. - total_force += move[1] - - return total_force - - -def overlap_force_rand(part, parts, **options): - """Compute the repulsive force on a part from overlapping other parts. - - Args: - part (Part): Part affected by forces from other overlapping parts. - parts (list): List of parts to check for overlaps. - options (dict): Dict of options and values that enable/disable functions. - - Returns: - Vector: Force upon given part. - """ - - # Bounding box of given part. - part_bbox = part.place_bbox * part.tx - - # Compute the overlap force of the bbox of this part with every other part. - total_force = Vector(0, 0) - for other_part in set(parts) - {part}: - other_part_bbox = other_part.place_bbox * other_part.tx - - # No force unless parts overlap. - if part_bbox.intersects(other_part_bbox): - # Compute the movement needed to clear the bboxes in left/right/up/down directions. - # Add some small random offset to break symmetry when parts exactly overlay each other. - # Move right edge of part to the left of other part's left edge. - moves = [] - rnd = Vector(random.random() - 0.5, random.random() - 0.5) - for edges, dir in ( - (("ll", "lr"), Vector(1, 0)), - (("lr", "ll"), Vector(1, 0)), - (("ul", "ll"), Vector(0, 1)), - (("ll", "ul"), Vector(0, 1)), - ): - move = ( - getattr(other_part_bbox, edges[0]) - - getattr(part_bbox, edges[1]) - - rnd - ) * dir - moves.append([move.magnitude, move]) - accum = 0 - for move in moves: - accum += move[0] - for move in moves: - move[0] = accum - move[0] - new_accum = 0 - for move in moves: - move[0] += new_accum - new_accum = move[0] - select = new_accum * random.random() - for move in moves: - if move[0] >= select: - total_force += move[1] - break - - return total_force - - -# Select the overlap force method used for the repulsion of parts during placement. -repulsive_force = overlap_force -# repulsive_force = overlap_force_rand - - -def scale_attractive_repulsive_forces(parts, force_func, **options): - """Set scaling between attractive net forces and repulsive part overlap forces.""" - - # Store original part placement. - for part in parts: - part.original_tx = copy(part.tx) - - # Find attractive forces when they are maximized by random part placement. - random_placement(parts, **options) - attractive_forces_sum = sum( - force_func(p, parts, alpha=0, scale=1, **options).magnitude for p in parts - ) - - # Find repulsive forces when they are maximized by compacted part placement. - central_placement(parts, **options) - repulsive_forces_sum = sum( - force_func(p, parts, alpha=1, scale=1, **options).magnitude for p in parts - ) - - # Restore original part placement. - for part in parts: - part.tx = part.original_tx - rmv_attr(parts, ["original_tx"]) - - # Return scaling factor that makes attractive forces about the same as repulsive forces. - try: - return repulsive_forces_sum / attractive_forces_sum - except ZeroDivisionError: - # No attractive forces, so who cares about scaling? Set it to 1. - return 1 - - -def total_part_force(part, parts, scale, alpha, **options): - """Compute the total of the attractive net and repulsive overlap forces on a part. - - Args: - part (Part): Part affected by forces from other overlapping parts. - parts (list): List of parts to check for overlaps. - scale (float): Scaling factor for net forces to make them equivalent to overlap forces. - alpha (float): Fraction of the total that is the overlap force (range [0,1]). - options (dict): Dict of options and values that enable/disable functions. - - Returns: - Vector: Weighted total of net attractive and overlap repulsion forces. - """ - force = scale * (1 - alpha) * attractive_force( - part, **options - ) + alpha * repulsive_force(part, parts, **options) - part.force = force # For debug drawing. - return force - - -def similarity_force(part, parts, similarity, **options): - """Compute attractive force on a part from all the other parts connected to it. - - Args: - part (Part): Part affected by similarity forces with other parts. - similarity (dict): Similarity score for any pair of parts used as keys. - options (dict): Dict of options and values that enable/disable functions. - - Returns: - Vector: Force upon given part. - """ - - # Get the single anchor point for similarity forces affecting this part. - anchor_pt = part.anchor_pins["similarity"][0].place_pt * part.tx - - # Compute the combined force of all the similarity pulling points. - total_force = Vector(0, 0) - for pull_pin in part.pull_pins["similarity"]: - pull_pt = pull_pin.place_pt * pull_pin.part.tx - # Force from pulling to anchor point is proportional to part similarity and distance. - total_force += (pull_pt - anchor_pt) * similarity[part][pull_pin.part] - - return total_force - - -def total_similarity_force(part, parts, similarity, scale, alpha, **options): - """Compute the total of the attractive similarity and repulsive overlap forces on a part. - - Args: - part (Part): Part affected by forces from other overlapping parts. - parts (list): List of parts to check for overlaps. - similarity (dict): Similarity score for any pair of parts used as keys. - scale (float): Scaling factor for similarity forces to make them equivalent to overlap forces. - alpha (float): Proportion of the total that is the overlap force (range [0,1]). - options (dict): Dict of options and values that enable/disable functions. - - Returns: - Vector: Weighted total of net attractive and overlap repulsion forces. - """ - force = scale * (1 - alpha) * similarity_force( - part, parts, similarity, **options - ) + alpha * repulsive_force(part, parts, **options) - part.force = force # For debug drawing. - return force - - -def define_placement_bbox(parts, **options): - """Return a bounding box big enough to hold the parts being placed.""" - - # Compute appropriate size to hold the parts based on their areas. - area = 0 - for part in parts: - area += part.place_bbox.area - side = 3 * math.sqrt(area) # HACK: Multiplier is ad-hoc. - return BBox(Point(0, 0), Point(side, side)) - - -def central_placement(parts, **options): - """Cluster all part centroids onto a common point. - - Args: - parts (list): List of Parts. - options (dict): Dict of options and values that enable/disable functions. - """ - - if len(parts) <= 1: - # No need to do placement if there's less than two parts. - return - - # Find the centroid of all the parts. - ctr = get_enclosing_bbox(parts).ctr - - # Collapse all the parts to the centroid. - for part in parts: - mv = ctr - part.place_bbox.ctr * part.tx - part.tx *= Tx(dx=mv.x, dy=mv.y) - - -def random_placement(parts, **options): - """Randomly place parts within an appropriately-sized area. - - Args: - parts (list): List of Parts to place. - """ - - # Compute appropriate size to hold the parts based on their areas. - bbox = define_placement_bbox(parts, **options) - - # Place parts randomly within area. - for part in parts: - pt = Point(random.random() * bbox.w, random.random() * bbox.h) - part.tx = part.tx.move(pt) - - -def push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options): - """Move parts under influence of attractive nets and repulsive part overlaps. - - Args: - anchored_parts (list): Set of immobile Parts whose position affects placement. - mobile_parts (list): Set of Parts that can be moved. - nets (list): List of nets that interconnect parts. - force_func: Function for calculating forces between parts. - options (dict): Dict of options and values that enable/disable functions. - """ - - if not options.get("use_push_pull"): - # Abort if push & pull of parts is disabled. - return - - if not mobile_parts: - # No need to do placement if there's nothing to move. - return - - def cost(parts, alpha): - """Cost function for use in debugging. Should decrease as parts move.""" - for part in parts: - part.force = force_func(part, parts, scale=scale, alpha=alpha, **options) - return sum((part.force.magnitude for part in parts)) - - # Get PyGame screen, real-to-screen coord Tx matrix, font for debug drawing. - scr = options.get("draw_scr") - tx = options.get("draw_tx") - font = options.get("draw_font") - txt_org = Point(10, 10) - - # Create the total set of parts exerting forces on each other. - parts = anchored_parts + mobile_parts - - # If there are no anchored parts, then compute the overall drift force - # across all the parts. This will be subtracted so the - # entire group of parts doesn't just continually drift off in one direction. - # This only needs to be done if ALL parts are mobile (i.e., no anchored parts). - rmv_drift = not anchored_parts - - # Set scale factor between attractive net forces and repulsive part overlap forces. - scale = scale_attractive_repulsive_forces(parts, force_func, **options) - - # Setup the schedule for adjusting the alpha coefficient that weights the - # combination of the attractive net forces and the repulsive part overlap forces. - # Start at 0 (all attractive) and gradually progress to 1 (all repulsive). - # Also, set parameters for determining when parts are stable and for restricting - # movements in the X & Y directions when parts are being aligned. - force_schedule = [ - (0.50, 0.0, 0.1, False, (1, 1)), # Attractive forces only. - (0.25, 0.0, 0.01, False, (1, 1)), # Attractive forces only. - # (0.25, 0.2, 0.01, False, (1,1)), # Some repulsive forces. - (0.25, 0.4, 0.1, False, (1, 1)), # More repulsive forces. - # (0.25, 0.6, 0.01, False, (1,1)), # More repulsive forces. - (0.25, 0.8, 0.1, False, (1, 1)), # More repulsive forces. - # (0.25, 0.7, 0.01, True, (1,0)), # Align parts horiz. - # (0.25, 0.7, 0.01, True, (0,1)), # Align parts vert. - # (0.25, 0.7, 0.01, True, (1,0)), # Align parts horiz. - # (0.25, 0.7, 0.01, True, (0,1)), # Align parts vert. - (0.25, 1.0, 0.01, False, (1, 1)), # Remove any part overlaps. - ] - # N = 7 - # force_schedule = [(0.50, i/N, 0.01, False, (1,1)) for i in range(N+1)] - - # Step through the alpha sequence going from all-attractive to all-repulsive forces. - for speed, alpha, stability_coef, align_parts, force_mask in force_schedule: - if align_parts: - # Align parts by only using forces between the closest anchor/pull pins. - retain_closest_anchor_pull_pins(mobile_parts) - else: - # For general placement, use forces between all anchor/pull pins. - restore_anchor_pull_pins(mobile_parts) - - # This stores the threshold below which all the parts are assumed to be stabilized. - # Since it can never be negative, set it to -1 to indicate it's uninitialized. - stable_threshold = -1 - - # Move parts for this alpha until they all settle into fixed positions. - # Place an iteration limit to prevent an infinite loop. - for _ in range(1000): # HACK: Ad-hoc iteration limit. - # Compute forces exerted on the parts by each other. - sum_of_forces = 0 - for part in mobile_parts: - part.force = force_func( - part, parts, scale=scale, alpha=alpha, **options - ) - # Mask X or Y component of force during part alignment. - part.force = part.force.mask(force_mask) - sum_of_forces += part.force.magnitude - - if rmv_drift: - # Calculate the drift force across all parts and subtract it from each part - # to prevent them from continually drifting in one direction. - drift_force = force_sum([part.force for part in mobile_parts]) / len( - mobile_parts - ) - for part in mobile_parts: - part.force -= drift_force - - # Apply movements to part positions. - for part in mobile_parts: - part.mv = part.force * speed - part.tx *= Tx(dx=part.mv.x, dy=part.mv.y) - - # Keep iterating until all the parts are still. - if stable_threshold < 0: - # Set the threshold after the first iteration. - initial_sum_of_forces = sum_of_forces - stable_threshold = sum_of_forces * stability_coef - elif sum_of_forces <= stable_threshold: - # Part positions have stabilized if forces have dropped below threshold. - break - elif sum_of_forces > 10 * initial_sum_of_forces: - # If the forces are getting higher, then that usually means the parts are - # spreading out. This can happen if speed is too large, so reduce it so - # the forces may start to decrease. - speed *= 0.50 - - if scr: - # Draw current part placement for debugging purposes. - draw_placement(parts, nets, scr, tx, font) - draw_text( - "alpha:{alpha:3.2f} iter:{_} force:{sum_of_forces:.1f} stable:{stable_threshold}".format( - **locals() - ), - txt_org, - scr, - tx, - font, - color=(0, 0, 0), - real=False, - ) - draw_redraw() - - -def evolve_placement(anchored_parts, mobile_parts, nets, force_func, **options): - """Evolve part placement looking for optimum using force function. - - Args: - anchored_parts (list): Set of immobile Parts whose position affects placement. - mobile_parts (list): Set of Parts that can be moved. - nets (list): List of nets that interconnect parts. - force_func (function): Computes the force affecting part positions. - options (dict): Dict of options and values that enable/disable functions. - """ - - parts = anchored_parts + mobile_parts - - # Force-directed placement. - push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options) - - # Snap parts to grid. - for part in parts: - snap_to_grid(part) - - -def place_net_terminals(net_terminals, placed_parts, nets, force_func, **options): - """Place net terminals around already-placed parts. - - Args: - net_terminals (list): List of NetTerminals - placed_parts (list): List of placed Parts. - nets (list): List of nets that interconnect parts. - force_func (function): Computes the force affecting part positions. - options (dict): Dict of options and values that enable/disable functions. - """ - - def trim_pull_pins(terminals, bbox): - """Trim pullpins of NetTerminals to the part pins closest to an edge of the bounding box of placed parts. - - Args: - terminals (list): List of NetTerminals. - bbox (BBox): Bounding box of already-placed parts. - - Note: - The rationale for this is that pin closest to an edge of the bounding box will be easier to access. - """ - - for terminal in terminals: - for net, pull_pins in terminal.pull_pins.items(): - insets = [] - for pull_pin in pull_pins: - pull_pt = pull_pin.place_pt * pull_pin.part.tx - - # Get the inset of the terminal pulling pin from each side of the placement area. - # Left side. - insets.append((abs(pull_pt.x - bbox.ll.x), pull_pin)) - # Right side. - insets.append((abs(pull_pt.x - bbox.lr.x), pull_pin)) - # Top side. - insets.append((abs(pull_pt.y - bbox.ul.y), pull_pin)) - # Bottom side. - insets.append((abs(pull_pt.y - bbox.ll.y), pull_pin)) - - # Retain only the pulling pin closest to an edge of the bounding box (i.e., minimum inset). - terminal.pull_pins[net] = [min(insets, key=lambda off: off[0])[1]] - - def orient(terminals, bbox): - """Set orientation of NetTerminals to point away from closest bounding box edge. - - Args: - terminals (list): List of NetTerminals. - bbox (BBox): Bounding box of already-placed parts. - """ - - for terminal in terminals: - # A NetTerminal should already be trimmed so it is attached to a single pin of a part on a single net. - pull_pin = list(terminal.pull_pins.values())[0][0] - pull_pt = pull_pin.place_pt * pull_pin.part.tx - - # Get the inset of the terminal pulling pin from each side of the placement area - # and the Tx() that should be applied if the terminal is placed on that side. - insets = [] - # Left side, so terminal label juts out to the left. - insets.append((abs(pull_pt.x - bbox.ll.x), Tx())) - # Right side, so terminal label flipped to jut out to the right. - insets.append((abs(pull_pt.x - bbox.lr.x), Tx().flip_x())) - # Top side, so terminal label rotated by 270 to jut out to the top. - insets.append( - (abs(pull_pt.y - bbox.ul.y), Tx().rot_90cw().rot_90cw().rot_90cw()) - ) - # Bottom side. so terminal label rotated 90 to jut out to the bottom. - insets.append((abs(pull_pt.y - bbox.ll.y), Tx().rot_90cw())) - - # Apply the Tx() for the side the terminal is closest to. - terminal.tx = min(insets, key=lambda inset: inset[0])[1] - - def move_to_pull_pin(terminals): - """Move NetTerminals immediately to their pulling pins.""" - for terminal in terminals: - anchor_pin = list(terminal.anchor_pins.values())[0][0] - anchor_pt = anchor_pin.place_pt * anchor_pin.part.tx - pull_pin = list(terminal.pull_pins.values())[0][0] - pull_pt = pull_pin.place_pt * pull_pin.part.tx - terminal.tx = terminal.tx.move(pull_pt - anchor_pt) - - def evolution(net_terminals, placed_parts, bbox): - """Evolve placement of NetTerminals starting from outermost from center to innermost.""" - - evolution_type = options.get("terminal_evolution", "all_at_once") - - if evolution_type == "all_at_once": - evolve_placement( - placed_parts, net_terminals, nets, total_part_force, **options - ) - - elif evolution_type == "outer_to_inner": - # Start off with the previously-placed parts as anchored parts. NetTerminals will be added to this as they are placed. - anchored_parts = copy(placed_parts) - - # Sort terminals from outermost to innermost w.r.t. the center. - def dist_to_bbox_edge(term): - pt = term.pins[0].place_pt * term.tx - return min( - ( - abs(pt.x - bbox.ll.x), - abs(pt.x - bbox.lr.x), - abs(pt.y - bbox.ll.y), - abs(pt.y - bbox.ul.y), - ) - ) - - terminals = sorted( - net_terminals, - key=lambda term: dist_to_bbox_edge(term), - reverse=True, - ) - - # Grab terminals starting from the outside and work towards the inside until a terminal intersects a previous one. - mobile_terminals = [] - mobile_bboxes = [] - for terminal in terminals: - terminal_bbox = terminal.place_bbox * terminal.tx - mobile_terminals.append(terminal) - mobile_bboxes.append(terminal_bbox) - for bbox in mobile_bboxes[:-1]: - if terminal_bbox.intersects(bbox): - # The current NetTerminal intersects one of the previously-selected mobile terminals, so evolve the - # placement of all the mobile terminals except the current one. - evolve_placement( - anchored_parts, - mobile_terminals[:-1], - nets, - force_func, - **options, - ) - # Anchor the mobile terminals after their placement is done. - anchored_parts.extend(mobile_terminals[:-1]) - # Remove the placed terminals, leaving only the current terminal. - mobile_terminals = mobile_terminals[-1:] - mobile_bboxes = mobile_bboxes[-1:] - - if mobile_terminals: - # Evolve placement of any remaining terminals. - evolve_placement( - anchored_parts, mobile_terminals, nets, total_part_force, **options - ) - - bbox = get_enclosing_bbox(placed_parts) - save_anchor_pull_pins(net_terminals) - trim_pull_pins(net_terminals, bbox) - orient(net_terminals, bbox) - move_to_pull_pin(net_terminals) - evolution(net_terminals, placed_parts, bbox) - restore_anchor_pull_pins(net_terminals) - - -class Placer: - """Mixin to add place function to Node class.""" - - def group_parts(node, **options): - """Group parts in the Node that are connected by internal nets - - Args: - node (Node): Node with parts. - options (dict, optional): Dictionary of options and values. Defaults to {}. - - Returns: - list: List of lists of Parts that are connected. - list: List of internal nets connecting parts. - list: List of Parts that are not connected to anything (floating). - """ - - if not node.parts: - return [], [], [] - - # Extract list of nets having at least one pin in the node. - internal_nets = node.get_internal_nets() - - # Group all the parts that have some interconnection to each other. - # Start with groups of parts on each individual net. - connected_parts = [ - set(pin.part for pin in net.pins if pin.part in node.parts) - for net in internal_nets - ] - - # Now join groups that have parts in common. - for i in range(len(connected_parts) - 1): - group1 = connected_parts[i] - for j in range(i + 1, len(connected_parts)): - group2 = connected_parts[j] - if group1 & group2: - # If part groups intersect, collect union of parts into one group - # and empty-out the other. - connected_parts[j] = connected_parts[i] | connected_parts[j] - connected_parts[i] = set() - # No need to check against group1 any more since it has been - # unioned into group2 that will be checked later in the loop. - break - - # Remove any empty groups that were unioned into other groups. - connected_parts = [group for group in connected_parts if group] - - # Find parts that aren't connected to anything. - floating_parts = set(node.parts) - set(itertools.chain(*connected_parts)) - - return connected_parts, internal_nets, floating_parts - - def place_connected_parts(node, parts, nets, **options): - """Place individual parts. - - Args: - node (Node): Node with parts. - parts (list): List of Part sets connected by nets. - nets (list): List of internal Nets connecting the parts. - options (dict): Dict of options and values that enable/disable functions. - """ - - if not parts: - # Abort if nothing to place. - return - - # Add bboxes with surrounding area so parts are not butted against each other. - add_placement_bboxes(parts, **options) - - # Set anchor and pull pins that determine attractive forces between parts. - add_anchor_pull_pins(parts, nets, **options) - - # Randomly place connected parts. - random_placement(parts) - - if options.get("draw_placement"): - # Draw the placement for debug purposes. - bbox = get_enclosing_bbox(parts) - draw_scr, draw_tx, draw_font = draw_start(bbox) - options.update( - {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} - ) - - if options.get("compress_before_place"): - central_placement(parts, **options) - - # Do force-directed placement of the parts in the parts. - - # Separate the NetTerminals from the other parts. - net_terminals = [part for part in parts if is_net_terminal(part)] - real_parts = [part for part in parts if not is_net_terminal(part)] - - # Do the first trial placement. - evolve_placement([], real_parts, nets, total_part_force, **options) - - if options.get("rotate_parts"): - # Adjust part orientations after first trial placement is done. - if adjust_orientations(real_parts, **options): - # Some part orientations were changed, so re-do placement. - evolve_placement([], real_parts, nets, total_part_force, **options) - - # Place NetTerminals after all the other parts. - place_net_terminals( - net_terminals, real_parts, nets, total_part_force, **options - ) - - if options.get("draw_placement"): - # Pause to look at placement for debugging purposes. - draw_pause() - - def place_floating_parts(node, parts, **options): - """Place individual parts. - - Args: - node (Node): Node with parts. - parts (list): List of Parts not connected by explicit nets. - options (dict): Dict of options and values that enable/disable functions. - """ - - if not parts: - # Abort if nothing to place. - return - - # Add bboxes with surrounding area so parts are not butted against each other. - add_placement_bboxes(parts) - - # Set anchor and pull pins that determine attractive forces between similar parts. - add_anchor_pull_pins(parts, [], **options) - - # Randomly place the floating parts. - random_placement(parts) - - if options.get("draw_placement"): - # Compute the drawing area for the floating parts - bbox = get_enclosing_bbox(parts) - draw_scr, draw_tx, draw_font = draw_start(bbox) - options.update( - {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} - ) - - # For non-connected parts, do placement based on their similarity to each other. - part_similarity = defaultdict(lambda: defaultdict(lambda: 0)) - for part in parts: - for other_part in parts: - # Don't compute similarity of a part to itself. - if other_part is part: - continue - - # HACK: Get similarity forces right-sized. - part_similarity[part][other_part] = part.similarity(other_part) / 100 - # part_similarity[part][other_part] = 0.1 - - # Select the top-most pin in each part as the anchor point for force-directed placement. - # tx = part.tx - # part.anchor_pin = max(part.anchor_pins, key=lambda pin: (pin.place_pt * tx).y) - - force_func = functools.partial( - total_similarity_force, similarity=part_similarity - ) - - if options.get("compress_before_place"): - # Compress all floating parts together. - central_placement(parts, **options) - - # Do force-directed placement of the parts in the group. - evolve_placement([], parts, [], force_func, **options) - - if options.get("draw_placement"): - # Pause to look at placement for debugging purposes. - draw_pause() - - def place_blocks(node, connected_parts, floating_parts, children, **options): - """Place blocks of parts and hierarchical sheets. - - Args: - node (Node): Node with parts. - connected_parts (list): List of Part sets connected by nets. - floating_parts (set): Set of Parts not connected by any of the internal nets. - children (list): Child nodes in the hierarchy. - non_sheets (list): Hierarchical set of Parts that are visible. - sheets (list): List of hierarchical blocks. - options (dict): Dict of options and values that enable/disable functions. - """ - - # Global dict of pull pins for all blocks as they each pull on each other the same way. - block_pull_pins = defaultdict(list) - - # Class for movable groups of parts/child nodes. - class PartBlock: - def __init__(self, src, bbox, anchor_pt, snap_pt, tag): - self.src = src # Source for this block. - self.place_bbox = bbox # FIXME: Is this needed if place_bbox includes room for routing? - - # Create anchor pin to which forces are applied to this block. - anchor_pin = Pin() - anchor_pin.part = self - anchor_pin.place_pt = anchor_pt - - # This block has only a single anchor pin, but it needs to be in a list - # in a dict so it can be processed by the part placement functions. - self.anchor_pins = dict() - self.anchor_pins["similarity"] = [anchor_pin] - - # Anchor pin for this block is also a pulling pin for all other blocks. - block_pull_pins["similarity"].append(anchor_pin) - - # All blocks have the same set of pulling pins because they all pull each other. - self.pull_pins = block_pull_pins - - self.snap_pt = snap_pt # For snapping to grid. - self.tx = Tx() # For placement. - self.ref = "REF" # Name for block in debug drawing. - self.tag = tag # FIXME: what is this for? - - # Create a list of blocks from the groups of interconnected parts and the group of floating parts. - part_blocks = [] - for part_list in connected_parts + [floating_parts]: - if not part_list: - # No parts in this list for some reason... - continue - - # Find snapping point and bounding box for this group of parts. - snap_pt = None - bbox = BBox() - for part in part_list: - bbox.add(part.lbl_bbox * part.tx) - if not snap_pt: - # Use the first snapping point of a part you can find. - snap_pt = get_snap_pt(part) - - # Tag indicates the type of part block. - tag = 2 if (part_list is floating_parts) else 1 - - # pad the bounding box so part blocks don't butt-up against each other. - pad = BLK_EXT_PAD - bbox = bbox.resize(Vector(pad, pad)) - - # Create the part block and place it on the list. - part_blocks.append(PartBlock(part_list, bbox, bbox.ctr, snap_pt, tag)) - - # Add part blocks for child nodes. - for child in children: - # Calculate bounding box of child node. - bbox = child.calc_bbox() - - # Set padding for separating bounding box from others. - if child.flattened: - # This is a flattened node so the parts will be shown. - # Set the padding to include a pad between the parts and the - # graphical box that contains them, plus the padding around - # the outside of the graphical box. - pad = BLK_INT_PAD + BLK_EXT_PAD - else: - # This is an unflattened child node showing no parts on the inside - # so just pad around the outside of its graphical box. - pad = BLK_EXT_PAD - bbox = bbox.resize(Vector(pad, pad)) - - # Set the grid snapping point and tag for this child node. - snap_pt = child.get_snap_pt() - tag = 3 # Standard child node. - if not snap_pt: - # No snap point found, so just use the center of the bounding box. - snap_pt = bbox.ctr - tag = 4 # A child node with no snapping point. - - # Create the child block and place it on the list. - part_blocks.append(PartBlock(child, bbox, bbox.ctr, snap_pt, tag)) - - # Get ordered list of all block tags. Use this list to tell if tags are - # adjacent since there may be missing tags if a particular type of block - # isn't present. - tags = sorted({blk.tag for blk in part_blocks}) - - # Tie the blocks together with strong links between blocks with the same tag, - # and weaker links between blocks with adjacent tags. This ties similar - # blocks together into "super blocks" and ties the super blocks into a linear - # arrangement (1 -> 2 -> 3 ->...). - blk_attr = defaultdict(lambda: defaultdict(lambda: 0)) - for blk in part_blocks: - for other_blk in part_blocks: - if blk is other_blk: - # No attraction between a block and itself. - continue - if blk.tag == other_blk.tag: - # Large attraction between blocks of same type. - blk_attr[blk][other_blk] = 1 - elif abs(tags.index(blk.tag) - tags.index(other_blk.tag)) == 1: - # Some attraction between blocks of adjacent types. - blk_attr[blk][other_blk] = 0.1 - else: - # Otherwise, no attraction between these blocks. - blk_attr[blk][other_blk] = 0 - - if not part_blocks: - # Abort if nothing to place. - return - - # Start off with a random placement of part blocks. - random_placement(part_blocks) - - if options.get("draw_placement"): - # Setup to draw the part block placement for debug purposes. - bbox = get_enclosing_bbox(part_blocks) - draw_scr, draw_tx, draw_font = draw_start(bbox) - options.update( - {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} - ) - - # Arrange the part blocks with similarity force-directed placement. - force_func = functools.partial(total_similarity_force, similarity=blk_attr) - evolve_placement([], part_blocks, [], force_func, **options) - - if options.get("draw_placement"): - # Pause to look at placement for debugging purposes. - draw_pause() - - # Apply the placement moves of the part blocks to their underlying sources. - for blk in part_blocks: - try: - # Update the Tx matrix of the source (usually a child node). - blk.src.tx = blk.tx - except AttributeError: - # The source doesn't have a Tx so it must be a collection of parts. - # Apply the block placement to the Tx of each part. - for part in blk.src: - part.tx *= blk.tx - - def get_attrs(node): - """Return dict of attribute sets for the parts, pins, and nets in a node.""" - attrs = {"parts": set(), "pins": set(), "nets": set()} - for part in node.parts: - attrs["parts"].update(set(dir(part))) - for pin in part.pins: - attrs["pins"].update(set(dir(pin))) - for net in node.get_internal_nets(): - attrs["nets"].update(set(dir(net))) - return attrs - - def show_added_attrs(node): - """Show attributes that were added to parts, pins, and nets in a node.""" - current_attrs = node.get_attrs() - for key in current_attrs.keys(): - print( - "added {} attrs: {}".format(key, current_attrs[key] - node.attrs[key]) - ) - - def rmv_placement_stuff(node): - """Remove attributes added to parts, pins, and nets of a node during the placement phase.""" - - for part in node.parts: - rmv_attr(part.pins, ("route_pt", "place_pt")) - rmv_attr( - node.parts, - ("anchor_pins", "pull_pins", "pin_ctrs", "force", "mv"), - ) - rmv_attr(node.get_internal_nets(), ("parts",)) - - def place(node, tool=None, **options): - """Place the parts and children in this node. - - Args: - node (Node): Hierarchical node containing the parts and children to be placed. - tool (str): Backend tool for schematics. - options (dict): Dictionary of options and values to control placement. - """ - - # Inject the constants for the backend tool into this module. - import skidl - from skidl.tools import tool_modules - - tool = tool or skidl.config.tool - this_module = sys.modules[__name__] - this_module.__dict__.update(tool_modules[tool].constants.__dict__) - - random.seed(options.get("seed")) - - # Store the starting attributes of the node's parts, pins, and nets. - node.attrs = node.get_attrs() - - try: - # First, recursively place children of this node. - # TODO: Child nodes are independent, so can they be processed in parallel? - for child in node.children.values(): - child.place(tool=tool, **options) - - # Group parts into those that are connected by explicit nets and - # those that float freely connected only by stub nets. - connected_parts, internal_nets, floating_parts = node.group_parts(**options) - - # Place each group of connected parts. - for group in connected_parts: - node.place_connected_parts(list(group), internal_nets, **options) - - # Place the floating parts that have no connections to anything else. - node.place_floating_parts(list(floating_parts), **options) - - # Now arrange all the blocks of placed parts and the child nodes within this node. - node.place_blocks( - connected_parts, floating_parts, node.children.values(), **options - ) - - # Remove any stuff leftover from this place & route run. - # print(f"added part attrs = {new_part_attrs}") - node.rmv_placement_stuff() - # node.show_added_attrs() - - # Calculate the bounding box for the node after placement of parts and children. - node.calc_bbox() - - except PlacementFailure: - node.rmv_placement_stuff() - raise PlacementFailure - - def get_snap_pt(node): - """Get a Point to use for snapping the node to the grid. - - Args: - node (Node): The Node to which the snapping point applies. - - Returns: - Point: The snapping point or None. - """ - - if node.flattened: - # Look for a snapping point based on one of its parts. - for part in node.parts: - snap_pt = get_snap_pt(part) - if snap_pt: - return snap_pt - - # If no part snapping point, look for one in its children. - for child in node.children.values(): - if child.flattened: - snap_pt = child.get_snap_pt() - if snap_pt: - # Apply the child transformation to its snapping point. - return snap_pt * child.tx - - # No snapping point if node is not flattened or no parts in it or its children. - return None diff --git a/src/faebryk/exporters/schematic/kicad/route.py b/src/faebryk/exporters/schematic/kicad/route.py deleted file mode 100644 index b5d449f8..00000000 --- a/src/faebryk/exporters/schematic/kicad/route.py +++ /dev/null @@ -1,3413 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - -""" -Autorouter for generating wiring between symbols in a schematic. -""" - -import copy -import random -import sys -from collections import Counter, defaultdict -from enum import Enum -from itertools import chain, zip_longest - -from skidl import Part -from skidl.utilities import export_to_all, rmv_attr -from .geometry import BBox, Point, Segment, Tx, Vector, tx_rot_90 -from faebryk.exporters.visualize.util import generate_pastel_palette - - -__all__ = ["RoutingFailure", "GlobalRoutingFailure", "SwitchboxRoutingFailure"] - - -################################################################### -# -# OVERVIEW OF SCHEMATIC AUTOROUTER -# -# The input is a Node containing child nodes and parts, each with a -# bounding box and an assigned (x,y) position. The following operations -# are done for each child node, and then for the parts within this node. -# -# The edges of each part bbox are extended to form tracks that divide the -# routing area into a set of four-sided, non-overlapping switchboxes. Each -# side of a switchbox is a Face, and each Face is a member of two adjoining -# switchboxes (except those Faces on the boundary of the total -# routing area.) Each face is adjacent to the six other faces of -# the two switchboxes it is part of. -# -# Each face has a capacity that indicates the number of wires that can -# cross through it. The capacity is the length of the face divided by the -# routing grid. (Faces on a part boundary have zero capacity to prevent -# routing from entering a part.) -# -# Each face on a part bbox is assigned terminals associated with the I/O -# pins of that symbol. -# -# After creating the faces and terminals, the global routing phase creates -# wires that connect the part pins on the nets. Each wire passes from -# a face of a switchbox to one of the other three faces, either directly -# across the switchbox to the opposite face or changing direction to -# either of the right-angle faces. The global router is basically a maze -# router that uses the switchboxes as high-level grid squares. -# -# After global routing, each net has a sequence of switchbox faces -# through which it will transit. The exact coordinate that each net -# enters a face is then assigned to create a Terminal. -# -# At this point there are a set of switchboxes which have fixed terminals located -# along their four faces. A greedy switchbox router -# (https://doi.org/10.1016/0167-9260(85)90029-X) -# does the detailed routing within each switchbox. -# -# The detailed wiring within all the switchboxes is combined and output -# as the total wiring for the parts in the Node. -# -################################################################### - - -try: - from .debug_draw import draw_end, draw_endpoint, draw_routing, draw_seg, draw_start -except ImportError: - - def _raise_on_call(*args, **kwargs): - raise RuntimeError("Function not available.") - - draw_end = _raise_on_call - draw_endpoint = _raise_on_call - draw_routing = _raise_on_call - draw_seg = _raise_on_call - draw_start = _raise_on_call - - -# Orientations and directions. -class Orientation(Enum): - HORZ = 1 - VERT = 2 - - -class Direction(Enum): - LEFT = 3 - RIGHT = 4 - - -# Dictionary for storing colors to visually distinguish routed nets. -# TODO: replace with generate_pastel_palette -net_colors = defaultdict( - lambda: (random.randint(0, 200), random.randint(0, 200), random.randint(0, 200)) -) - - -class NoSwitchBox(Exception): - """Exception raised when a switchbox cannot be generated.""" - - pass - - -class TerminalClashException(Exception): - """Exception raised when trying to place two terminals at the same coord on a Face.""" - - pass - - -class RoutingFailure(Exception): - """Exception raised when a net connecting pins cannot be routed.""" - - pass - - -class GlobalRoutingFailure(RoutingFailure): - """Failure during global routing phase.""" - - pass - - -class SwitchboxRoutingFailure(RoutingFailure): - """Failure during switchbox routing phase.""" - - pass - - -class Boundary: - """Class for indicating a boundary. - - When a Boundary object is placed in the part attribute of a Face, it - indicates the Face is on the outer boundary of the Node routing area - and no routes can pass through it. - """ - - pass - - -# Boundary object for placing in the bounding Faces of the Node routing area. -boundary = Boundary() - -# Absolute coords of all part pins. Used when trimming stub nets. -pin_pts = [] - - -class Terminal: - def __init__(self, net, face, coord): - """Terminal on a Face from which a net is routed within a SwitchBox. - - Args: - net (Net): Net upon which the Terminal resides. - face (Face): SwitchBox Face upon which the Terminal resides. - coord (int): Absolute position along the track the face is in. - - Notes: - A terminal exists on a Face and is assigned to a net. - The terminal's (x,y) position is determined by the terminal's - absolute coordinate along the track parallel to the face, - and by the Face's absolute coordinate in the orthogonal direction. - """ - - self.net = net - self.face = face - self.coord = coord - - @property - def route_pt(self): - """Return (x,y) Point for a Terminal on a Face.""" - track = self.face.track - if track.orientation == HORZ: - return Point(self.coord, track.coord) - else: - return Point(track.coord, self.coord) - - def get_next_terminal(self, next_face): - """Get the terminal on the next face that lies on the same net as this terminal. - - This method assumes the terminal's face and the next face are faces of the - same switchbox. Hence, they're either parallel and on opposite sides, or they're - at right angles so they meet at a corner. - - Args: - next_face (Face): Face to search for a terminal on the same net as this. - - Raises: - RoutingFailure: If no terminal exists. - - Returns: - Terminal: The terminal found on the next face. - """ - - from_face = self.face - if next_face.track in (from_face.beg, from_face.end): - # The next face bounds the interval of the terminals's face, so - # they're at right angles. With right angle faces, we want to - # select a terminal on the next face that's close to this corner - # because that will minimize the length of wire needed to make - # the connection. - if next_face.beg == from_face.track: - # next_face is oriented upward or rightward w.r.t. from_face. - # Start searching for a terminal from the lowest index - # because this is closest to the corner. - search_terminals = next_face.terminals - elif next_face.end == from_face.track: - # next_face is oriented downward or leftward w.r.t. from_face. - # Start searching for a terminal from the highest index - # because this is closest to the corner. - search_terminals = next_face.terminals[::-1] - else: - raise GlobalRoutingFailure - else: - # The next face must be the parallel face on the other side of the - # switchbox. With parallel faces, we want to selected a terminal - # having close to the same position as the given terminal. - # So if the given terminal is at position i, then search for the - # next terminal on the other face at positions i, i+1, i-1, i+2, i-2... - coord = self.coord - lower_terminals = [t for t in next_face.terminals if t.coord <= coord] - lower_terminals.sort(key=lambda t: t.coord, reverse=True) - upper_terminals = [t for t in next_face.terminals if t.coord > coord] - upper_terminals.sort(key=lambda t: t.coord, reverse=False) - search_terminals = list( - chain(*zip_longest(lower_terminals, upper_terminals)) - ) - search_terminals = [t for t in search_terminals if t is not None] - - # Search to find a terminal on the same net. - for terminal in search_terminals: - if terminal.net is self.net: - return terminal # Return found terminal. - - # No terminal on the same net, so search to find an unassigned terminal. - for terminal in search_terminals: - if terminal.net is None: - terminal.net = self.net # Assign net to terminal. - return terminal # Return newly-assigned terminal. - - # Well, something went wrong. Should have found *something*! - raise GlobalRoutingFailure - - def draw(self, scr, tx, **options): - """Draw a Terminal for debugging purposes. - - Args: - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - options (dict, optional): Dictionary of options and values. Defaults to {}. - """ - - # Don't draw terminal if it isn't on a net. It's just a placeholder. - if self.net or options.get("draw_all_terminals"): - draw_endpoint(self.route_pt, scr, tx, color=(255, 0, 0)) - # draw_endpoint(self.route_pt, scr, tx, color=net_colors[self.net]) - - -class Interval(object): - def __init__(self, beg, end): - """Define an interval with a beginning and an end. - - Args: - beg (GlobalTrack): Beginning orthogonal track that bounds interval. - end (GlobalTrack): Ending orthogonal track that bounds interval. - - Note: The beginning and ending Tracks are orthogonal to the Track containing - the interval. - Also, beg and end are sorted so beg <= end. - """ - - # Order beginning and end so beginning <= end. - if beg > end: - beg, end = end, beg - self.beg = beg - self.end = end - - def __bool__(self): - """An Interval object always returns True.""" - return True - - @property - def len(self): - """Return the length of the interval.""" - return self.end - self.beg - - def __len__(self): - """Return the length of the interval.""" - return self.len - - def intersects(self, other): - """Return True if the intervals overlap (even if only at one point).""" - return not ((self.beg > other.end) or (self.end < other.beg)) - - def interval_intersection(self, other): - """Return intersection of two intervals as an interval, otherwise None.""" - if self.intersects(other): - beg = max(self.beg, other.beg) - end = min(self.end, other.end) - assert beg <= end - if beg != end: - return Interval(beg, end) - return None - - def merge(self, other): - """ - Return a merged interval if the given intervals intersect, otherwise return - None. - """ - if Interval.intersects(self, other): - return Interval(min(self.beg, other.beg), max(self.end, other.end)) - return None - - -class NetInterval(Interval): - def __init__(self, net, beg, end): - """Define an Interval with an associated net (useful for wire traces in a - switchbox). - - Args: - net (Net): Net associated with interval. - beg (GlobalTrack): Beginning orthogonal track that bounds interval. - end (GlobalTrack): Ending track that bounds interval. - """ - super().__init__(beg, end) - self.net = net - - def obstructs(self, other): - """Return True if the intervals intersect and have different nets.""" - return super().intersects(other) and (self.net is not other.net) - - def merge(self, other): - """Return a merged interval if the given intervals intersect and are on the - same net, otherwise return None.""" - if self.net is other.net: - merged_intvl = super().merge(other) - if merged_intvl: - merged_intvl = NetInterval(self.net, merged_intvl.beg, merged_intvl.end) - return merged_intvl - return None - - -class Adjacency: - def __init__(self, from_face, to_face): - """Define an adjacency between two Faces. - - Args: - from_face (Face): One Face. - to_face (Face): The other Face. - - Note: The Adjacency object will be associated with the from_face object, so there's - no need to store from_face in the Adjacency object. - """ - - self.face = to_face - if from_face.track.orientation == to_face.track.orientation: - # Parallel faces, either both vertical or horizontal. - # Distance straight-across from one face to the other. - dist_a = abs(from_face.track.coord - to_face.track.coord) - # Average distance parallel to the faces. - dist_b = (from_face.length + to_face.length) / 2 - # Compute the average distance from a terminal on one face to the other. - self.dist = dist_a + dist_b / 2 - else: - # Else, orthogonal faces. - # Compute the average face-to-face distance. - dist_a = from_face.length - dist_b = to_face.length - # Average distance of dogleg route from a terminal on one face to the other. - self.dist = (dist_a + dist_b) / 2 - - -class Face(Interval): - """A side of a rectangle bounding a routing switchbox.""" - - def __init__(self, part, track, beg, end): - """One side of a routing switchbox. - - Args: - part (set,Part,Boundary): Element(s) the Face is part of. - track (GlobalTrack): Horz/vert track the Face is on. - beg (GlobalTrack): Vert/horz track the Face begins at. - end (GlobalTrack): Vert/horz track the Face ends at. - - Notes: - The beg and end tracks have to be in the same direction - (i.e., both vertical or both horizontal) and orthogonal - to the track containing the face. - """ - - # Initialize the interval beginning and ending defining the Face. - super().__init__(beg, end) - - # Store Part/Boundary the Face is part of, if any. - self.part = set() - if isinstance(part, set): - self.part.update(part) - elif part is not None: - self.part.add(part) - - # Storage for any part pins that lie along this Face. - self.pins = [] - - # Storage for routing terminals along this face. - self.terminals = [] - - # Set of Faces adjacent to this one. (Starts empty.) - self.adjacent = set() - - # Add this new face to the track it belongs to so it isn't lost. - self.track = track - track.add_face(self) - - # Storage for switchboxes this face is part of. - self.switchboxes = set() - - def combine(self, other): - """Combine information from other face into this one. - - Args: - other (Face): Other Face. - - Returns: - None. - """ - - self.pins.extend(other.pins) - self.terminals.extend(other.terminals) - self.part.update(other.part) - self.adjacent.update(other.adjacent) - self.switchboxes.update(other.switchboxes) - - @property - def length(self): - """Return the length of the face.""" - return self.end.coord - self.beg.coord - - @property - def bbox(self): - """Return the bounding box of the 1-D face segment.""" - bbox = BBox() - - if self.track.orientation == VERT: - # Face runs vertically, so bbox width is zero. - bbox.add(Point(self.track.coord, self.beg.coord)) - bbox.add(Point(self.track.coord, self.end.coord)) - else: - # Face runs horizontally, so bbox height is zero. - bbox.add(Point(self.beg.coord, self.track.coord)) - bbox.add(Point(self.end.coord, self.track.coord)) - - return bbox - - def add_terminal(self, net, coord): - """Create a Terminal on the Face. - - Args: - net (Net): The net the terminal is on. - coord (int): The absolute coordinate along the track containing the Face. - - Raises: - TerminalClashException: - """ - - if self.part and not net: - # Don't add pin terminals with no net to a Face on a part or boundary. - return - - # Search for pre-existing terminal at the same coordinate. - for terminal in self.terminals: - if terminal.coord == coord: - # There is a pre-existing terminal at this coord. - if not net: - # The new terminal has no net (i.e., non-pin terminal), - # so just quit and don't bother to add it. The pre-existing - # terminal is retained. - return - elif terminal.net and terminal.net is not net: - # The pre-existing and new terminals have differing nets, so - # raise an exception. - raise TerminalClashException - # The pre-existing and new terminals have the same net. - # Remove the pre-existing terminal. It will be replaced - # with the new terminal below. - self.terminals.remove(terminal) - - # Create a new Terminal and add it to the list of terminals for this face. - self.terminals.append(Terminal(net, self, coord)) - - def trim_repeated_terminals(self): - """Remove all but one terminal of each individual net from the face. - - Notes: - A non-part Face with multiple terminals on the same net will lead - to multi-path routing. - """ - - # Find the intersection of every non-part face in the track with this one. - intersections = [] - for face in self.track: - if not face.part: - intersection = self.interval_intersection(face) - if intersection: - intersections.append(intersection) - - # Merge any overlapping intersections to create larger ones. - for i in range(len(intersections)): - for j in range(i + 1, len(intersections)): - merge = intersections[i].merge(intersections[j]) - if merge: - intersections[j] = merge - intersections[i] = None - break - - # Remove None from the list of intersections. - intersections = list(set(intersections) - {None}) - - # The intersections are now as large as they can be and not associated - # with any parts, so there are no terminals associated with part pins. - # Look for terminals within an intersection on the same net and - # remove all but one of them. - for intersection in intersections: - # Make a dict with nets and the terminals on each one. - net_term_dict = defaultdict(list) - for terminal in self.terminals: - if intersection.beg.coord <= terminal.coord <= intersection.end.coord: - net_term_dict[terminal.net].append(terminal) - if None in net_term_dict.keys(): - del net_term_dict[None] # Get rid of terminals not assigned to nets. - - # For each multi-terminal net, remove all but one terminal. - # This terminal must be removed from all faces on the track. - for terminals in net_term_dict.values(): - for terminal in terminals[1:]: # Keep only the 1st terminal. - self.track.remove_terminal(terminal) - - def create_nonpin_terminals(self): - """Create unassigned terminals along a non-part Face with GRID spacing. - - These terminals will be used during global routing of nets from - face-to-face and during switchbox routing. - """ - - # Add terminals along a Face. A terminal can be right at the start if the Face - # starts on a grid point, but there cannot be a terminal at the end - # if the Face ends on a grid point. Otherwise, there would be two terminals - # at exactly the same point (one at the ending point of a Face and the - # other at the beginning point of the next Face). - # FIXME: This seems to cause wiring with a lot of doglegs. - if self.end.coord - self.beg.coord <= GRID: - # Allow a terminal right at the start of the Face if the Face is small. - beg = (self.beg.coord + GRID - 1) // GRID * GRID - else: - # For larger faces with lengths greater than the GRID spacing, - # don't allow terminals right at the start of the Face. - beg = (self.beg.coord + GRID) // GRID * GRID - end = self.end.coord - - # Create terminals along the Face. - for coord in range(beg, end, GRID): - self.add_terminal(None, coord) - - def set_capacity(self): - """Set the wire routing capacity of a Face.""" - - if self.part: - # Part/boundary faces have zero capacity for wires to pass thru. - self.capacity = 0 - else: - # Wire routing capacity for other faces is the number of terminals they - # have. - self.capacity = len(self.terminals) - - def has_nets(self): - """Return True if any Terminal on the Face is attached to a net.""" - return any((terminal.net for terminal in self.terminals)) - - def add_adjacencies(self): - """Add adjacent faces of the switchbox having this face as the top face.""" - - # Create a temporary switchbox. - try: - swbx = SwitchBox(self) - except NoSwitchBox: - # This face doesn't belong to a valid switchbox. - return - - def add_adjacency(from_, to): - # Faces on the boundary can never accept wires so they are never - # adjacent to any other face. - if boundary in from_.part or boundary in to.part: - return - - # If a face is an edge of a part, then it can never be adjacent to - # another face on the *same part* or else wires might get routed over - # the part bounding box. - if from_.part.intersection(to.part): - return - - # OK, no parts in common between the two faces so they can be adjacent. - from_.adjacent.add(Adjacency(from_, to)) - to.adjacent.add(Adjacency(to, from_)) - - # Add adjacent faces. - add_adjacency(swbx.top_face, swbx.bottom_face) - add_adjacency(swbx.left_face, swbx.right_face) - add_adjacency(swbx.left_face, swbx.top_face) - add_adjacency(swbx.left_face, swbx.bottom_face) - add_adjacency(swbx.right_face, swbx.top_face) - add_adjacency(swbx.right_face, swbx.bottom_face) - - # Get rid of the temporary switchbox. - del swbx - - def extend(self, orthogonal_tracks): - """Extend a Face along its track until it is blocked by an orthogonal face. - - This is used to create Faces that form the irregular grid of switchboxes. - - Args: - orthogonal_tracks (list): List of tracks at right-angle to this face. - """ - - # Only extend faces that compose part bounding boxes. - if not self.part: - return - - # Extend the face backward from its beginning and forward from its end. - for start, dir in ((self.beg, -1), (self.end, 1)): - # Get tracks to extend face towards. - search_tracks = orthogonal_tracks[start.idx :: dir] - - # The face extension starts off non-blocked by any orthogonal faces. - blocked = False - - # Search for a orthogonal face in a track that intersects this extension. - for ortho_track in search_tracks: - for ortho_face in ortho_track: - # Intersection only occurs if the extending face hits the open - # interval of the orthogonal face, not if it touches an endpoint. - if ortho_face.beg < self.track < ortho_face.end: - # OK, this face intersects the extension. It also means the - # extending face will block the face just found, so split - # each track at the intersection point. - ortho_track.add_split(self.track) - self.track.add_split(ortho_track) - - # If the intersecting face is also a face of a part bbox, - # then the extension is blocked, so create the extended face - # and stop the extension. - if ortho_face.part: - # This creates a face and adds it to the track. - Face(None, self.track, start, ortho_track) - blocked = True - - # Stop checking faces in this track after an intersection is - # found. - break - - # Stop checking any further tracks once the face extension is blocked. - if blocked: - break - - def split(self, trk): - """ - If a track intersects in the middle of a face, split the face into two faces. - """ - - if self.beg < trk < self.end: - # Add a Face from beg to trk to self.track. - Face(self.part, self.track, self.beg, trk) - # Move the beginning of the original Face to trk. - self.beg = trk - - def coincides_with(self, other_face): - """Return True if both faces have the same beginning and ending point on the - same track.""" - return (self.beg, self.end) == (other_face.beg, other_face.end) - - def has_overlap(self, other_face): - """Return True if the two faces overlap.""" - return self.beg < other_face.end and self.end > other_face.beg - - def audit(self): - """Raise exception if face is malformed.""" - assert len(self.switchboxes) <= 2 - - @property - def seg(self): - """Return a Segment that coincides with the Face.""" - - if self.track.orientation == VERT: - p1 = Point(self.track.coord, self.beg.coord) - p2 = Point(self.track.coord, self.end.coord) - else: - p1 = Point(self.beg.coord, self.track.coord) - p2 = Point(self.end.coord, self.track.coord) - - return Segment(p1, p2) - - def draw( - self, scr, tx, font, color=(128, 128, 128), thickness=2, dot_radius=0, **options - ): - """Draw a Face in the drawing area. - - Args: - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - font (PyGame font): Font for rendering text. - options (dict, optional): Dictionary of options and values. - - Returns: - None. - """ - - # Draw a line segment for the Face. - draw_seg( - self.seg, scr, tx, color=color, thickness=thickness, dot_radius=dot_radius - ) - - # Draw the terminals on the Face. - for terminal in self.terminals: - terminal.draw(scr, tx, **options) - - if options.get("show_capacities"): - # Show the wiring capacity at the midpoint of the Face. - mid_pt = (self.seg.p1 + self.seg.p2) / 2 - draw_text(str(self.capacity), mid_pt, scr, tx, font=font, color=color) - - -class GlobalWire(list): - def __init__(self, net, *args, **kwargs): - """A list connecting switchbox faces and terminals. - - Global routes start off as a sequence of switchbox faces that the route - goes thru. Later, these faces are converted to terminals at fixed positions - on their respective faces. - - Args: - net (Net): The net associated with the wire. - *args: Positional args passed to list superclass __init__(). - **kwargs: Keyword args passed to list superclass __init__(). - """ - self.net = net - super().__init__(*args, **kwargs) - - def cvt_faces_to_terminals(self): - """Convert global face-to-face route to switchbox terminal-to-terminal route.""" - - if not self: - # Global route is empty so do nothing. - return - - # Non-empty global routes should always start from a face on a part. - assert self[0].part - - # All part faces already have terminals created from the part pins. Find all - # the route faces on part boundaries and convert them to pin terminals if - # one or more pins are attached to the same net as the route. - for i, face in enumerate(self[:]): - if face.part: - # This route face is on a part boundary, so find the terminal with the - # route's net. - for terminal in face.terminals: - if self.net is terminal.net: - # Replace the route face with the terminal on the part. - self[i] = terminal - break - else: - # Route should never touch a part face if there is no terminal with - # the route's net. - raise RuntimeError - - # Proceed through all the Faces/Terminals on the GlobalWire, converting - # all the Faces to Terminals. - for i in range(len(self) - 1): - # The current element on a GlobalWire should always be a Terminal. Use that - # terminal - # to convert the next Face on the wire to a Terminal (if it isn't one - # already). - if isinstance(self[i], Face): - # Logic error if the current element has not been converted to a - # Terminal. - raise RuntimeError - - if isinstance(self[i + 1], Face): - # Convert the next Face element into a Terminal on this net. This - # terminal will - # be the current element on the next iteration. - self[i + 1] = self[i].get_next_terminal(self[i + 1]) - - def draw(self, scr, tx, color=(0, 0, 0), thickness=1, dot_radius=10, **options): - """Draw a global wire from Face-to-Face in the drawing area. - - Args: - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - color (list): Three-element list of RGB integers with range [0, 255]. - thickness (int): Thickness of drawn wire in pixels. - dot_radius (int): Radius of drawn terminal in pixels. - options (dict, optional): Dictionary of options and values. Defaults to {}. - - Returns: - None. - """ - - # Draw pins on the net associated with the wire. - for pin in self.net.pins: - # Only draw pins in the current node being routed which have the route_pt - # attribute. - if hasattr(pin, "route_pt"): - pt = pin.route_pt * pin.part.tx - track = pin.face.track - pt = { - HORZ: Point(pt.x, track.coord), - VERT: Point(track.coord, pt.y), - }[track.orientation] - draw_endpoint(pt, scr, tx, color=color, dot_radius=10) - - # Draw global wire segment. - face_to_face = zip(self[:-1], self[1:]) - for terminal1, terminal2 in face_to_face: - p1 = terminal1.route_pt - p2 = terminal2.route_pt - draw_seg( - Segment(p1, p2), scr, tx, color=color, thickness=thickness, dot_radius=0 - ) - - -class GlobalRoute(list): - def __init__(self, *args, **kwargs): - """A list containing GlobalWires that form an entire routing of a net. - - Args: - net (Net): The net associated with the wire. - *args: Positional args passed to list superclass __init__(). - **kwargs: Keyword args passed to list superclass __init__(). - """ - super().__init__(*args, **kwargs) - - def cvt_faces_to_terminals(self): - """Convert GlobalWires in route to switchbox terminal-to-terminal route.""" - for wire in self: - wire.cvt_faces_to_terminals() - - def draw( - self, scr, tx, font, color=(0, 0, 0), thickness=1, dot_radius=10, **options - ): - """Draw the GlobalWires of this route in the drawing area. - - Args: - scr (PyGame screen): Screen object for PyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - font (PyGame font): Font for rendering text. - color (list): Three-element list of RGB integers with range [0, 255]. - thickness (int): Thickness of drawn wire in pixels. - dot_radius (int): Radius of drawn terminal in pixels. - options (dict, optional): Dictionary of options and values. Defaults to {}. - - Returns: - None. - """ - - for wire in self: - wire.draw(scr, tx, color, thickness, dot_radius, **options) - - -class GlobalTrack(list): - def __init__(self, orientation=HORZ, coord=0, idx=None, *args, **kwargs): - """A horizontal/vertical track holding zero or more faces all having the - same Y/X coordinate. - - These global tracks are made by extending the edges of part bounding boxes to - form a non-regular grid of rectangular switchboxes. These tracks are *NOT* - the same - as the tracks used within a switchbox for the detailed routing phase. - - Args: - orientation (Orientation): Orientation of track (horizontal or vertical). - coord (int): Coordinate of track on axis orthogonal to track direction. - idx (int): Index of track into a list of X or Y coords. - *args: Positional args passed to list superclass __init__(). - **kwargs: Keyword args passed to list superclass __init__(). - """ - - self.orientation = orientation - self.coord = coord - self.idx = idx - super().__init__(*args, **kwargs) - - # This stores the orthogonal tracks that intersect this one. - self.splits = set() - - def __eq__(self, track): - """Used for ordering tracks.""" - return self.coord == track.coord - - def __ne__(self, track): - """Used for ordering tracks.""" - return self.coord != track.coord - - def __lt__(self, track): - """Used for ordering tracks.""" - return self.coord < track.coord - - def __le__(self, track): - """Used for ordering tracks.""" - return self.coord <= track.coord - - def __gt__(self, track): - """Used for ordering tracks.""" - return self.coord > track.coord - - def __ge__(self, track): - """Used for ordering tracks.""" - return self.coord >= track.coord - - def __sub__(self, other): - """Subtract coords of two tracks.""" - return self.coord - other.coord - - def extend_faces(self, orthogonal_tracks): - """Extend the faces in a track. - - This is part of forming the irregular grid of switchboxes. - - Args: - orthogonal_tracks (list): List of tracks orthogonal to this one - (L/R vs. H/V). - """ - - for face in self[:]: - face.extend(orthogonal_tracks) - - def __hash__(self): - """This method lets a track be inserted into a set of splits.""" - return self.idx - - def add_split(self, orthogonal_track): - """Store the orthogonal track that intersects this one.""" - self.splits.add(orthogonal_track) - - def add_face(self, face): - """Add a face to a track. - - Args: - face (Face): Face to be added to track. - """ - - self.append(face) - - # The orthogonal tracks that bound the added face will split this track. - self.add_split(face.beg) - self.add_split(face.end) - - def split_faces(self): - """Split track faces by any intersecting orthogonal tracks.""" - - for split in self.splits: - for face in self[:]: - # Apply the split track to the face. The face will only be split - # if the split track intersects it. Any split faces will be added - # to the track this face is on. - face.split(split) - - def remove_duplicate_faces(self): - """Remove faces from the track having the same endpoints.""" - - # Create lists of faces having the same endpoints. - dup_faces_dict = defaultdict(list) - for face in self: - key = (face.beg, face.end) - dup_faces_dict[key].append(face) - - # Remove all but the first face from each list. - for dup_faces in dup_faces_dict.values(): - retained_face = dup_faces[0] - for dup_face in dup_faces[1:]: - # Add info from duplicate face to the retained face. - retained_face.combine(dup_face) - self.remove(dup_face) - - def remove_terminal(self, terminal): - """Remove a terminal from any non-part Faces in the track.""" - - coord = terminal.coord - # Look for the terminal in all non-part faces on the track. - for face in self: - if not face.part: - for term in face.terminals[:]: - if term.coord == coord: - face.terminals.remove(term) - - def add_adjacencies(self): - """Add adjacent switchbox faces to each face in a track.""" - - for top_face in self: - top_face.add_adjacencies() - - def audit(self): - """Raise exception if track is malformed.""" - - for i, first_face in enumerate(self): - first_face.audit() - for second_face in self[i + 1 :]: - if first_face.has_overlap(second_face): - raise AssertionError - - def draw(self, scr, tx, font, **options): - """Draw the Faces in a track. - - Args: - scr (_type_): _description - scr (PyGame screen): Screen object forPyGame drawing. - tx (Tx): Transformation matrix from real to screen coords. - font (PyGame font): Font for rendering text. - options (dict, optional): Dictionary of options and values. Defaults to {}. - """ - for face in self: - face.draw(scr, tx, font, **options) - - -class Target: - def __init__(self, net, row, col): - """A point on a switchbox face that switchbox router has not yet reached. - - Targets are used to direct the switchbox router towards terminals that - need to be connected to nets. So wiring will be nudged up/down to - get closer to terminals along the upper/lower faces. Wiring will also - be nudged toward the track rows where terminals on the right face reside - as the router works from the left to the right. - - Args: - net (Net): Target net. - row (int): Track row for the target, including top or bottom faces. - col (int): Switchbox column for the target. - """ - self.row = row - self.col = col - self.net = net - - def __lt__(self, other): - """Used for ordering Targets in terms of priority.""" - - # Targets in the left-most columns are given priority since they will be reached - # first as the switchbox router proceeds from left-to-right. - return (self.col, self.row, id(self.net)) < ( - other.col, - other.row, - id(other.net), - ) - - -class SwitchBox: - # Indices for faces of the switchbox. - TOP, LEFT, BOTTOM, RIGHT = 0, 1, 2, 3 - - def __init__(self, top_face, left_face=None, bottom_face=None, right_face=None): - """Routing switchbox. - - A switchbox is a rectangular region through which wires are routed. - It has top, bottom, left and right faces. - - Args: - top_face (Face): The top face of the switchbox (needed to find the other faces). - bottom_face (Face): The bottom face. Will be calculated if set to None. - left_face (Face): The left face. Will be calculated if set to None. - right_face (Face): The right face. Will be calculated if set to None. - - Raises: - NoSwitchBox: Exception raised if the switchbox is an - unroutable region inside a part bounding box. - """ - - # Find the left face in the left track that bounds the top face. - if left_face == None: - left_track = top_face.beg - for face in left_track: - # The left face will end at the track for the top face. - if face.end.coord == top_face.track.coord: - left_face = face - break - else: - raise NoSwitchBox("Unroutable switchbox (left)!") - - # Find the right face in the right track that bounds the top face. - if right_face == None: - right_track = top_face.end - for face in right_track: - # The right face will end at the track for the top face. - if face.end.coord == top_face.track.coord: - right_face = face - break - else: - raise NoSwitchBox("Unroutable switchbox (right)!") - - # For a routable switchbox, the left and right faces should each - # begin at the same point. - if left_face.beg != right_face.beg: - # Inequality only happens when two parts are butted up against each other - # to form a non-routable switchbox inside a part bounding box. - raise NoSwitchBox("Unroutable switchbox (left-right)!") - - # Find the bottom face in the track where the left/right faces begin. - if bottom_face == None: - bottom_track = left_face.beg - for face in bottom_track: - # The bottom face should begin/end in the same places as the top face. - if (face.beg.coord, face.end.coord) == ( - top_face.beg.coord, - top_face.end.coord, - ): - bottom_face = face - break - else: - raise NoSwitchBox("Unroutable switchbox (bottom)!") - - # If all four sides have a part in common, then the switchbox is inside - # a part bbox that wires cannot be routed through. - if top_face.part & bottom_face.part & left_face.part & right_face.part: - raise NoSwitchBox("Unroutable switchbox (part)!") - - # Store the faces. - self.top_face = top_face - self.bottom_face = bottom_face - self.left_face = left_face - self.right_face = right_face - - # Each face records which switchboxes it belongs to. - self.top_face.switchboxes.add(self) - self.bottom_face.switchboxes.add(self) - self.left_face.switchboxes.add(self) - self.right_face.switchboxes.add(self) - - def find_terminal_net(terminals, terminal_coords, coord): - """Return the net attached to a terminal at the given coordinate. - - Args: - terminals (list): List of Terminals to search. - terminal_coords (list): List of integer coordinates for Terminals. - coord (int): Terminal coordinate to search for. - - Returns: - Net/None: Net at given coordinate or None if no net exists. - """ - try: - return terminals[terminal_coords.index(coord)].net - except ValueError: - return None - - # Find the coordinates of all the horizontal routing tracks - left_coords = [terminal.coord for terminal in self.left_face.terminals] - right_coords = [terminal.coord for terminal in self.right_face.terminals] - tb_coords = [self.top_face.track.coord, self.bottom_face.track.coord] - # Remove duplicate coords. - self.track_coords = list(set(left_coords + right_coords + tb_coords)) - - if len(self.track_coords) == 2: - # This is a weird case. If the switchbox channel is too narrow to hold - # a routing track in the middle, then place two pseudo-tracks along the - # top and bottom faces to allow routing to proceed. The routed wires will - # end up in the top or bottom faces, but maybe that's OK. - # FIXME: Should this be extending with tb_coords? - # FIXME: Should we always extend with tb_coords? - self.track_coords.extend(self.track_coords) - - # Sort horiz. track coords from bottom to top. - self.track_coords = sorted(self.track_coords) - - # Create a list of nets for each of the left/right faces. - self.left_nets = [ - find_terminal_net(self.left_face.terminals, left_coords, coord) - for coord in self.track_coords - ] - self.right_nets = [ - find_terminal_net(self.right_face.terminals, right_coords, coord) - for coord in self.track_coords - ] - - # Find the coordinates of all the vertical columns and then create - # a list of nets for each of the top/bottom faces. - top_coords = [terminal.coord for terminal in self.top_face.terminals] - bottom_coords = [terminal.coord for terminal in self.bottom_face.terminals] - lr_coords = [self.left_face.track.coord, self.right_face.track.coord] - self.column_coords = sorted(set(top_coords + bottom_coords + lr_coords)) - self.top_nets = [ - find_terminal_net(self.top_face.terminals, top_coords, coord) - for coord in self.column_coords - ] - self.bottom_nets = [ - find_terminal_net(self.bottom_face.terminals, bottom_coords, coord) - for coord in self.column_coords - ] - - # Remove any nets that only have a single terminal in the switchbox. - all_nets = self.left_nets + self.right_nets + self.top_nets + self.bottom_nets - net_counts = Counter(all_nets) - single_terminal_nets = [net for net, count in net_counts.items() if count <= 1] - if single_terminal_nets: - for side_nets in ( - self.left_nets, - self.right_nets, - self.top_nets, - self.bottom_nets, - ): - for i, net in enumerate(side_nets): - if net in single_terminal_nets: - side_nets[i] = None - - # Handle special case when a terminal is right on the corner of the switchbox. - self.move_corner_nets() - - # Storage for detailed routing. - self.segments = defaultdict(list) - - def audit(self): - """Raise exception if switchbox is malformed.""" - - for face in self.face_list: - face.audit() - assert self.top_face.track.orientation == HORZ - assert self.bottom_face.track.orientation == HORZ - assert self.left_face.track.orientation == VERT - assert self.right_face.track.orientation == VERT - assert len(self.top_nets) == len(self.bottom_nets) - assert len(self.left_nets) == len(self.right_nets) - - @property - def face_list(self): - """Return list of switchbox faces in CCW order, starting from top face.""" - flst = [None] * 4 - flst[self.TOP] = self.top_face - flst[self.LEFT] = self.left_face - flst[self.BOTTOM] = self.bottom_face - flst[self.RIGHT] = self.right_face - return flst - - def move_corner_nets(self): - """ - Move any nets at the edges of the left/right faces - (i.e., the corners) to the edges of the top/bottom faces. - This will allow these nets to be routed within the switchbox columns - as the routing proceeds from left to right. - """ - - if self.left_nets[0]: - # Move bottommost net on left face to leftmost net on bottom face. - self.bottom_nets[0] = self.left_nets[0] - self.left_nets[0] = None - - if self.left_nets[-1]: - # Move topmost net on left face to leftmost net on top face. - self.top_nets[0] = self.left_nets[-1] - self.left_nets[-1] = None - - if self.right_nets[0]: - # Move bottommost net on right face to rightmost net on bottom face. - self.bottom_nets[-1] = self.right_nets[0] - self.right_nets[0] = None - - if self.right_nets[-1]: - # Move topmost net on right face to rightmost net on top face. - self.top_nets[-1] = self.right_nets[-1] - self.right_nets[-1] = None - - def flip_xy(self): - """ - Flip X-Y of switchbox to route from top-to-bottom instead of left-to-right. - """ - - # Flip coords of tracks and columns. - self.column_coords, self.track_coords = self.track_coords, self.column_coords - - # Flip top/right and bottom/left nets. - self.top_nets, self.right_nets = self.right_nets, self.top_nets - self.bottom_nets, self.left_nets = self.left_nets, self.bottom_nets - - # Flip top/right and bottom/left faces. - self.top_face, self.right_face = self.right_face, self.top_face - self.bottom_face, self.left_face = self.left_face, self.bottom_face - - # Move any corner nets from the new left/right faces to the new top/bottom - # faces. - self.move_corner_nets() - - # Flip X/Y coords of any routed segments. - for segments in self.segments.values(): - for seg in segments: - seg.flip_xy() - - def coalesce(self, switchboxes): - """Group switchboxes around a seed switchbox into a larger switchbox. - - Args: - switchboxes (list): List of seed switchboxes that have not yet been - coalesced into a larger switchbox. - - Returns: - A coalesced switchbox or None if the seed was no longer available for - coalescing. - """ - - # Abort if the switchbox is no longer a potential seed (it was already merged - # into a previous switchbox). - if self not in switchboxes: - return None - - # Remove the switchbox from the list of seeds. - switchboxes.remove(self) - - # List the switchboxes along the top, left, bottom and right borders of the - # coalesced switchbox. - box_lists = [[self], [self], [self], [self]] - - # Iteratively search to the top, left, bottom, and right for switchboxes to add. - active_directions = {self.TOP, self.LEFT, self.BOTTOM, self.RIGHT} - while active_directions: - # Grow in the shortest dimension so the coalesced switchbox - # stays "squarish". - bbox = BBox() - for box_list in box_lists: - bbox.add(box_list[0].bbox) - if bbox.w == bbox.h: - # Already square, so grow in any direction. - growth_directions = {self.TOP, self.LEFT, self.BOTTOM, self.RIGHT} - elif bbox.w < bbox.h: - # Taller than wide, so grow left or right. - growth_directions = {self.LEFT, self.RIGHT} - else: - # Wider than tall, so grow up or down. - growth_directions = {self.TOP, self.BOTTOM} - - # Only keep growth directions that are still active. - growth_directions = growth_directions & active_directions - - # If there is no active growth direction, then stop the growth iterations. - if not growth_directions: - break - - # Take a random choice of the active growth directions. - direction = random.choice(list(growth_directions)) - - # Check the switchboxes along the growth side to see if further expansion - # is possible. - box_list = box_lists[direction] - for box in box_list: - # Get the face of the box from which growth will occur. - box_face = box.face_list[direction] - if box_face.part: - # This box butts up against a part, so expansion in this direction - # is blocked. - active_directions.remove(direction) - break - # Get the box which will be added if expansion occurs. - # Every face borders two switchboxes, so the adjacent box is the other - # one. - adj_box = (box_face.switchboxes - {box}).pop() - if adj_box not in switchboxes: - # This box cannot be added, so expansion in this direction - # is blocked. - active_directions.remove(direction) - break - else: - # All the switchboxes along the growth side are available for expansion, - # so replace the current boxes in the growth side with these new ones. - for i, box in enumerate(box_list[:]): - # Get the adjacent box for the current box on the growth side. - box_face = box.face_list[direction] - adj_box = (box_face.switchboxes - {box}).pop() - # Replace the current box with the new box from the expansion. - box_list[i] = adj_box - # Remove the newly added box from the list of available boxes - # for growth. - switchboxes.remove(adj_box) - - # Add the first box on the growth side to the end of the list of - # boxes on the - # preceding direction: (top,left,bottom,right) if current - # direction is (left,bottom,right,top). - box_lists[direction - 1].append(box_list[0]) - - # Add the last box on the growth side to the start of the list of boxes on the - # next direction: (bottom,right,top,left) if current direction - # is (left,bottom,right,top). - box_lists[(direction + 1) % 4].insert(0, box_list[-1]) - - # Create new faces that bound the coalesced group of switchboxes. - total_faces = [None, None, None, None] - directions = (self.TOP, self.LEFT, self.BOTTOM, self.RIGHT) - for direction, box_list in zip(directions, box_lists): - # Create a face that spans all the faces of the boxes along one side. - face_list = [box.face_list[direction] for box in box_list] - beg = min([face.beg for face in face_list]) - end = max([face.end for face in face_list]) - total_face = Face(None, face_list[0].track, beg, end) - - # Add terminals from the box faces along one side. - total_face.create_nonpin_terminals() - for face in face_list: - for terminal in face.terminals: - if terminal.net: - total_face.add_terminal(terminal.net, terminal.coord) - - # Set the routing capacity of the new face. - total_face.set_capacity() - - # Store the new face for this side. - total_faces[direction] = total_face - - # Return the coalesced switchbox created from the new faces. - return SwitchBox(*total_faces) - - def trim_repeated_terminals(self): - """Trim terminals on each face.""" - for face in self.face_list: - face.trim_repeated_terminals() - - @property - def bbox(self): - """Return bounding box for a switchbox.""" - return BBox().add(self.top_face.bbox).add(self.left_face.bbox) - - def has_nets(self): - """Return True if switchbox has any terminals on any face with nets attached.""" - return ( - self.top_face.has_nets() - or self.bottom_face.has_nets() - or self.left_face.has_nets() - or self.right_face.has_nets() - ) - - def route(self, **options): - """Route wires between terminals on the switchbox faces. - - Args: - options (dict, optional): Dictionary of options and values. Defaults to {}. - - Raises: - RoutingFailure: Raised if routing could not be completed. - - Returns: - List of Segments: List of wiring segments for switchbox routes. - """ - - if not self.has_nets(): - # Return what should be an empty dict. - assert not self.segments.keys() - return self.segments - - def collect_targets(top_nets, bottom_nets, right_nets): - """Collect target nets along top, bottom, right faces of switchbox.""" - - min_row = 1 - max_row = len(right_nets) - 2 - max_col = len(top_nets) - targets = [] - - # Collect target nets along top and bottom faces of the switchbox. - for col, (t_net, b_net) in enumerate(zip(top_nets, bottom_nets)): - if t_net is not None: - targets.append(Target(t_net, max_row, col)) - if b_net is not None: - targets.append(Target(b_net, min_row, col)) - - # Collect target nets on the right face of the switchbox. - for row, r_net in enumerate(right_nets): - if r_net is not None: - targets.append(Target(r_net, row, max_col)) - - # Sort the targets by increasing column order so targets closer to - # the left-to-right routing have priority. - targets.sort() - - return targets - - def connect_top_btm(track_nets): - """Connect nets from top/bottom terminals in a column to nets in - horizontal tracks of the switchbox.""" - - def find_connection(net, tracks, direction): - """ - Searches for the closest track with the same net followed by the - closest empty track. The indices of these tracks are returned. - If the net cannot be connected to any track, return []. - If the net given to connect is None, then return a list of [None]. - - Args: - net (Net): Net to be connected. - tracks (list): Nets on tracks - direction (int): Search direction for connection (-1: down, +1:up). - - Returns: - list: Indices of tracks where the net can connect. - """ - - if net: - if direction < 0: - # Searching down so reverse tracks. - tracks = tracks[::-1] - - connections = [] - - try: - # Find closest track with the given net. - connections.append(tracks[1:-1].index(net) + 1) - except ValueError: - pass - - try: - # Find closest empty track. - connections.append(tracks[1:-1].index(None) + 1) - except ValueError: - pass - - if direction < 0: - # Reverse track indices if searching down. - l = len(tracks) - connections = [l - 1 - cnct for cnct in connections] - else: - # No net so return no connections. - connections = [None] - - return connections - - # Stores net intervals connecting top/bottom nets to horizontal tracks. - column_intvls = [] - - # Top/bottom nets for this switchbox column. Horizontal track nets are - # at indexes 1..-2. - b_net = track_nets[0] - t_net = track_nets[-1] - - if t_net and (t_net is b_net): - # If top & bottom nets are the same, just create a single net interval - # connecting them and that's it. - column_intvls.append(NetInterval(t_net, 0, len(track_nets) - 1)) - return column_intvls - - # Find which tracks the top/bottom nets can connect to. - t_cncts = find_connection(t_net, track_nets, -1) - b_cncts = find_connection(b_net, track_nets, 1) - - # Create all possible pairs of top/bottom connections. - tb_cncts = [(t, b) for t in t_cncts for b in b_cncts] - - if not tb_cncts: - # No possible connections for top and/or bottom. - if options.get("allow_routing_failure"): - return column_intvls # Return empty column. - else: - raise SwitchboxRoutingFailure - - # Test each possible pair of connections to find one that is free of - # interference. - for t_cnct, b_cnct in tb_cncts: - if t_cnct is None or b_cnct is None: - # No possible interference if at least one connection is None. - break - if t_cnct > b_cnct: - # Top & bottom connections don't interfere. - break - if t_cnct == b_cnct and t_net is b_net: - # Top & bottom connect to the same track but they're the same net - # so that's OK. - break - else: - if options.get("allow_routing_failure"): - return column_intvls - else: - raise SwitchboxRoutingFailure - - if t_cnct is not None: - # Connection from track to terminal on top of switchbox. - column_intvls.append(NetInterval(t_net, t_cnct, len(track_nets) - 1)) - if b_cnct is not None: - # Connection from terminal on bottom of switchbox to track. - column_intvls.append(NetInterval(b_net, 0, b_cnct)) - - # Return connection segments. - return column_intvls - - def prune_targets(targets, current_col): - """Remove targets in columns to the left of the current left-to-right routing column""" - return [target for target in targets if target.col > current_col] - - def insert_column_nets(track_nets, column_intvls): - """Return the active nets with the added nets of the column's vertical intervals.""" - - nets = track_nets[:] - for intvl in column_intvls: - nets[intvl.beg] = intvl.net - nets[intvl.end] = intvl.net - return nets - - def net_search(net, start, track_nets): - """Search for the closest points for the net before and after the start point.""" - - # illegal offset past the end of the list of track nets. - large_offset = 2 * len(track_nets) - - try: - # Find closest occurrence of net going up. - up = track_nets[start:].index(net) - except ValueError: - # Net not found, so use out-of-bounds index. - up = large_offset - - try: - # Find closest occurrence of net going down. - down = track_nets[start::-1].index(net) - except ValueError: - # Net not found, so use out-of-bounds index. - down = large_offset - - if up <= down: - return up - else: - return -down - - def insert_target_nets(track_nets, targets, right_nets): - """Return copy of active track nets with additional prioritized targets - from the top, bottom, right faces.""" - - # Allocate storage for potential target nets to be added to the list of - # active track nets. - placed_target_nets = [None] * len(track_nets) - - # Get a list of nets on the right face that are being actively routed - # right now - # so we can steer the routing as it proceeds rightward. - active_right_nets = [ - net if net in track_nets else None for net in right_nets - ] - - # Strip-off the top/bottom rows where terminals are and routing doesn't go. - search_nets = track_nets[1:-1] - - for target in targets: - target_net, target_row = target.net, target.row - - # Skip target nets that aren't currently active or have already been - # placed (prevents multiple insertions of the same target net). - # Also ignore targets on the far right face until the last step. - if ( - target_net not in track_nets # TODO: Use search_nets??? - or target_net in placed_target_nets - or target_net in active_right_nets - ): - continue - - # Assign the target net to the closest row to the target row that is - # either - # empty or has the same net. - net_row_offset = net_search(target_net, target_row, search_nets) - empty_row_offset = net_search(None, target_row, search_nets) - if abs(net_row_offset) <= abs(empty_row_offset): - row_offset = net_row_offset - else: - row_offset = empty_row_offset - try: - placed_target_nets[target_row + row_offset + 1] = target_net - search_nets[target_row + row_offset] = target_net - except IndexError: - # There was no place for this target net - pass - - return [ - active_net or target_net or right_net - # active_net or right_net or target_net - for (active_net, right_net, target_net) in zip( - track_nets, active_right_nets, placed_target_nets - ) - ] - - def connect_splits(track_nets, column): - """Vertically connect nets on multiple tracks.""" - - # Make a copy so the original isn't disturbed. - track_nets = track_nets[:] - - # Find nets that are running on multiple tracks. - multi_nets = set( - net for net in set(track_nets) if track_nets.count(net) > 1 - ) - multi_nets.discard(None) # Ignore empty tracks. - - # Find possible intervals for multi-track nets. - net_intervals = [] - for net in multi_nets: - net_trk_idxs = [idx for idx, nt in enumerate(track_nets) if nt is net] - for index, trk1 in enumerate(net_trk_idxs[:-1], 1): - for trk2 in net_trk_idxs[index:]: - net_intervals.append(NetInterval(net, trk1, trk2)) - - # Sort interval lengths from smallest to largest. - net_intervals.sort(key=lambda ni: len(ni)) - # Sort interval lengths from largest to smallest. - # net_intervals.sort(key=lambda ni: -len(ni)) - - # Connect tracks for each interval if it doesn't intersect an - # already existing connection. - for net_interval in net_intervals: - for col_interval in column: - if net_interval.obstructs(col_interval): - break - else: - # No conflicts found with existing connections. - column.append(net_interval) - - # Get the nets that have vertical wires in the column. - column_nets = set(intvl.net for intvl in column) - - # Merge segments of each net in the column. - for net in column_nets: - # Extract intervals if the current net has more than one interval. - intervals = [intvl for intvl in column if intvl.net is net] - if len(intervals) < 2: - # Skip if there's only a single interval for this net. - continue - - # Remove the intervals so they can be replaced with joined intervals. - for intvl in intervals: - column.remove(intvl) - - # Merge the extracted intervals as much as possible. - - # Sort intervals by their beginning coordinates. - intervals.sort(key=lambda intvl: intvl.beg) - - # Try merging consecutive pairs of intervals. - for i in range(len(intervals) - 1): - # Try to merge consecutive intervals. - merged_intvl = intervals[i].merge(intervals[i + 1]) - if merged_intvl: - # Keep only the merged interval and place it so it's compared - # to the next one. - intervals[i : i + 2] = None, merged_intvl - - # Remove the None entries that are inserted when segments get merged. - intervals = [intvl for intvl in intervals if intvl] - - # Place merged intervals back into column. - column.extend(intervals) - - return column - - def extend_tracks(track_nets, column, targets): - """Extend track nets into the next column.""" - - # These are nets to the right of the current column. - rightward_nets = set(target.net for target in targets) - - # Keep extending nets to next column if they do not intersect - # intervals in the - # current column with the same net. - flow_thru_nets = track_nets[:] - for intvl in column: - for trk_idx in range(intvl.beg, intvl.end + 1): - if flow_thru_nets[trk_idx] is intvl.net: - # Remove net from track since it intersects an interval with the - # same net. The net may be extended from the interval in the - # next phase, - # or it may terminate here. - flow_thru_nets[trk_idx] = None - - next_track_nets = flow_thru_nets[:] - - # Extend track net if net has multiple column intervals that need further - # interconnection - # or if there are terminals in rightward columns that need connections to - # this net. - first_track = 0 - last_track = len(track_nets) - 1 - column_nets = set([intvl.net for intvl in column]) - for net in column_nets: - # Get all the vertical intervals for this net in the current column. - net_intervals = [i for i in column if i.net is net] - - # No need to extend tracks for this net into next column if there - # aren't multiple - # intervals or further terminals to connect. - if net not in rightward_nets and len(net_intervals) < 2: - continue - - # Sort the net's intervals from bottom of the column to top. - net_intervals.sort(key=lambda e: e.beg) - - # Find the nearest target to the right matching the current net. - target_row = None - for target in targets: - if target.net is net: - target_row = target.row - break - - for i, intvl in enumerate(net_intervals): - # Sanity check: should never get here if interval goes from - # top-to-bottom of - # column (hence, only one interval) and there is no further - # terminal for this - # net to the right. - assert not ( - intvl.beg == first_track - and intvl.end == last_track - and not target_row - ) - - if intvl.beg == first_track and intvl.end < last_track: - # Interval starts on bottom of column, so extend net in the - # track where it ends. - assert i == 0 - assert track_nets[intvl.end] in (net, None) - exit_row = intvl.end - next_track_nets[exit_row] = net - continue - - if intvl.end == last_track and intvl.beg > first_track: - # Interval ends on top of column, so extend net in the track - # where it begins. - assert i == len(net_intervals) - 1 - assert track_nets[intvl.beg] in (net, None) - exit_row = intvl.beg - next_track_nets[exit_row] = net - continue - - if target_row is None: - # No target to the right, so we must be trying to connect - # multiple column intervals for this net. - if i == 0: - # First interval in column so extend from its top-most - # point. - exit_row = intvl.end - next_track_nets[exit_row] = net - elif i == len(net_intervals) - 1: - # Last interval in column so extend from its bottom-most - # point. - exit_row = intvl.beg - next_track_nets[exit_row] = net - else: - # This interval is between the top and bottom intervals. - beg_end = ( - bool(flow_thru_nets[intvl.beg]), - bool(flow_thru_nets[intvl.end]), - ) - if beg_end == (True, False): - # The net enters this interval at its bottom, so extend - # from the top (dogleg). - exit_row = intvl.end - next_track_nets[exit_row] = net - elif beg_end == (False, True): - # The net enters this interval at its top, so extend - # from the bottom (dogleg). - exit_row = intvl.beg - next_track_nets[exit_row] = net - else: - raise RuntimeError - continue - - else: - # Target to the right, so aim for it. - - if target_row > intvl.end: - # target track is above the interval's end, so bound it to - # the end. - target_row = intvl.end - elif target_row < intvl.beg: - # target track is below the interval's start, so bound it - # to the start. - target_row = intvl.beg - - # Search for the closest track to the target row that is either - # open - # or occupied by the same target net. - intvl_nets = track_nets[intvl.beg : intvl.end + 1] - net_row = ( - net_search(net, target_row - intvl.beg, intvl_nets) - + target_row - ) - open_row = ( - net_search(None, target_row - intvl.beg, intvl_nets) - + target_row - ) - net_dist = abs(net_row - target_row) - open_dist = abs(open_row - target_row) - if net_dist <= open_dist: - exit_row = net_row - else: - exit_row = open_row - assert intvl.beg <= exit_row <= intvl.end - next_track_nets[exit_row] = net - continue - - return next_track_nets - - def trim_column_intervals(column, track_nets, next_track_nets): - """Trim stubs from column intervals.""" - - # All nets entering and exiting the column. - trk_nets = list(enumerate(zip(track_nets, next_track_nets))) - - for intvl in column: - # Get all the entry/exit track positions having the same net as the - # interval - # and that are within the bounds of the interval. - net = intvl.net - beg = intvl.beg - end = intvl.end - trks = [i for (i, nets) in trk_nets if net in nets and beg <= i <= end] - - # Chop off any stubs of the interval that extend past where it could - # connect to an entry/exit point of its net. - intvl.beg = min(trks) - intvl.end = max(trks) - - ######################################## - # Main switchbox routing loop. - ######################################## - - # Get target nets as routing proceeds from left-to-right. - targets = collect_targets(self.top_nets, self.bottom_nets, self.right_nets) - - # Store the nets in each column that are in the process of being routed, - # starting with the nets in the left-hand face of the switchbox. - nets_in_column = [self.left_nets[:]] - - # Store routing intervals in each column. - all_column_intvls = [] - - # Route left-to-right across the columns connecting the top & bottom nets - # on each column to tracks within the switchbox. - for col, (t_net, b_net) in enumerate(zip(self.top_nets, self.bottom_nets)): - # Nets in the previous column become the currently active nets being routed - active_nets = nets_in_column[-1][:] - - if col == 0 and not t_net and not b_net: - # Nothing happens in the first column if there are no top & bottom nets. - # Just continue the active nets from the left-hand face to the next - # column. - column_intvls = [] - next_active_nets = active_nets[:] - - else: - # Bring any nets on the top & bottom of this column into the list of - # active nets. - active_nets[0] = b_net - active_nets[-1] = t_net - - # Generate the intervals that will vertically connect the top & bottom - # nets to - # horizontal tracks in the switchbox. - column_intvls = connect_top_btm(active_nets) - - # Add the nets from the new vertical connections to the active nets. - augmented_active_nets = insert_column_nets(active_nets, column_intvls) - - # Remove the nets processed in this column from the list of target nets. - targets = prune_targets(targets, col) - - # Insert target nets from rightward columns into this column to direct - # the placement of additional vertical intervals towards them. - augmented_active_nets = insert_target_nets( - augmented_active_nets, targets, self.right_nets - ) - - # Make vertical connections between tracks in the column having the - # same net. - column_intvls = connect_splits(augmented_active_nets, column_intvls) - - # Get the nets that will be active in the next column. - next_active_nets = extend_tracks(active_nets, column_intvls, targets) - - # Trim any hanging stubs from vertical routing intervals in the current - # column. - trim_column_intervals(column_intvls, active_nets, next_active_nets) - - # Store the active nets for the next column. - nets_in_column.append(next_active_nets) - - # Store the vertical routing intervals for this column. - all_column_intvls.append(column_intvls) - - ######################################## - # End of switchbox routing loop. - ######################################## - - # After routing from left-to-right, verify the active track nets coincide - # with the positions of the nets on the right-hand face of the switchbox. - for track_net, right_net in zip(nets_in_column[-1], self.right_nets): - if track_net is not right_net: - if not options.get("allow_routing_failure"): - raise SwitchboxRoutingFailure - - # Create wiring segments along each horizontal track. - # Add left and right faces to coordinates of the vertical columns. - column_coords = ( - [self.left_face.track.coord] - + self.column_coords - + [self.right_face.track.coord] - ) - # Proceed column-by-column from left-to-right creating horizontal wires. - for col_idx, nets in enumerate(nets_in_column): - beg_col_coord = column_coords[col_idx] - end_col_coord = column_coords[col_idx + 1] - # Create segments for each track (skipping bottom & top faces). - for trk_idx, net in enumerate(nets[1:-1], start=1): - if net: - # Create a wire segment for the net in this horizontal track of the - # column. - trk_coord = self.track_coords[trk_idx] - p1 = Point(beg_col_coord, trk_coord) - p2 = Point(end_col_coord, trk_coord) - seg = Segment(p1, p2) - self.segments[net].append(seg) - - # Create vertical wiring segments for each switchbox column. - for idx, column in enumerate(all_column_intvls): - # Get X coord of this column. - col_coord = self.column_coords[idx] - # Create vertical wire segments for wire interval in the column. - for intvl in column: - p1 = Point(col_coord, self.track_coords[intvl.beg]) - p2 = Point(col_coord, self.track_coords[intvl.end]) - self.segments[intvl.net].append(Segment(p1, p2)) - - return self.segments - - def draw( - self, scr=None, tx=None, font=None, color=(128, 0, 128), thickness=2, **options - ): - """Draw a switchbox and its routing for debugging purposes. - - Args: - scr (PyGame screen): Screen object for PyGame drawing. - Initialize PyGame ifNone. - tx (Tx): Transformation matrix from real to screen coords. - font (PyGame font): Font for rendering text. - color (tuple, optional): Switchbox boundary color. Defaults to (128, 0, 128). - thickness (int, optional): Switchbox boundary thickness. Defaults to 2. - options (dict, optional): Dictionary of options and values. Defaults to {}. - """ - - # If the screen object is not None, then PyGame drawing is enabled so set flag - # to initialize PyGame. - do_start_end = not bool(scr) - - if do_start_end: - # Initialize PyGame. - scr, tx, font = draw_start( - self.bbox.resize(Vector(DRAWING_BOX_RESIZE, DRAWING_BOX_RESIZE)) - ) - - if options.get("draw_switchbox_boundary"): - # Draw switchbox boundary. - self.top_face.draw(scr, tx, font, color, thickness, **options) - self.bottom_face.draw(scr, tx, font, color, thickness, **options) - self.left_face.draw(scr, tx, font, color, thickness, **options) - self.right_face.draw(scr, tx, font, color, thickness, **options) - - if options.get("draw_switchbox_routing"): - # Draw routed wire segments. - try: - for segments in self.segments.values(): - for segment in segments: - draw_seg(segment, scr, tx, dot_radius=0) - except AttributeError: - pass - - if options.get("draw_routing_channels"): - # Draw routing channels from midpoint of one switchbox face to midpoint of another. - - def draw_channel(face1, face2): - seg1 = face1.seg - seg2 = face2.seg - p1 = (seg1.p1 + seg1.p2) / 2 - p2 = (seg2.p1 + seg2.p2) / 2 - draw_seg(Segment(p1, p2), scr, tx, (128, 0, 128), 1, dot_radius=0) - - draw_channel(self.top_face, self.bottom_face) - draw_channel(self.top_face, self.left_face) - draw_channel(self.top_face, self.right_face) - draw_channel(self.bottom_face, self.left_face) - draw_channel(self.bottom_face, self.right_face) - draw_channel(self.left_face, self.right_face) - - if do_start_end: - # Terminate PyGame. - draw_end() - - -@export_to_all -class Router: - """Mixin to add routing function to Node class.""" - - def add_routing_points(node, nets): - """Add routing points by extending wires from pins out to the edge of the part - bounding box. - - Args: - nets (list): List of nets to be routed. - """ - - def add_routing_pt(pin): - """Add the point for a pin on the boundary of a part.""" - - bbox = pin.part.lbl_bbox - pin.route_pt = copy.copy(pin.pt) - if pin.orientation == "U": - # Pin points up, so extend downward to the bottom of the bounding box. - pin.route_pt.y = bbox.min.y - elif pin.orientation == "D": - # Pin points down, so extend upward to the top of the bounding box. - pin.route_pt.y = bbox.max.y - elif pin.orientation == "L": - # Pin points left, so extend rightward to the right-edge of the - # bounding box. - pin.route_pt.x = bbox.max.x - elif pin.orientation == "R": - # Pin points right, so extend leftward to the left-edge of the - # bounding box. - pin.route_pt.x = bbox.min.x - else: - raise RuntimeError("Unknown pin orientation.") - - # Global set of part pin (x,y) points may have stuff from processing previous - # nodes, so clear it. - del pin_pts[:] # Clear the list. Works for Python 2 and 3. - - for net in nets: - # Add routing points for all pins on the net that are inside this node. - for pin in node.get_internal_pins(net): - # Store the point where the pin is. (This is used after routing to - # trim wire stubs.) - pin_pts.append((pin.pt * pin.part.tx).round()) - - # Add the point to which the wiring should be extended. - add_routing_pt(pin) - - # Add a wire to connect the part pin to the routing point on the - # bounding box periphery. - if pin.route_pt != pin.pt: - seg = Segment(pin.pt, pin.route_pt) * pin.part.tx - node.wires[pin.net].append(seg) - - def create_routing_tracks(node, routing_bbox): - """Create horizontal & vertical global routing tracks.""" - - # Find the coords of the horiz/vert tracks that will hold the H/V faces of the - # routing switchboxes. - v_track_coord = [] - h_track_coord = [] - - # The top/bottom/left/right of each part's labeled bounding box define the H/V - # tracks. - for part in node.parts: - bbox = (part.lbl_bbox * part.tx).round() - v_track_coord.append(bbox.min.x) - v_track_coord.append(bbox.max.x) - h_track_coord.append(bbox.min.y) - h_track_coord.append(bbox.max.y) - - # Create delimiting tracks around the routing area. Just take the number of - # nets to be routed - # and create a channel that size around the periphery. That's guaranteed to be - # big enough. - # This is overkill but probably not worth optimizing since any excess boundary - # area is ignored. - v_track_coord.append(routing_bbox.min.x) - v_track_coord.append(routing_bbox.max.x) - h_track_coord.append(routing_bbox.min.y) - h_track_coord.append(routing_bbox.max.y) - - # Remove any duplicate track coords and then sort them. - v_track_coord = list(set(v_track_coord)) - h_track_coord = list(set(h_track_coord)) - v_track_coord.sort() - h_track_coord.sort() - - # Create an H/V track for each H/V coord containing a list for holding the - # faces in that track. - v_tracks = [ - GlobalTrack(orientation=VERT, idx=idx, coord=coord) - for idx, coord in enumerate(v_track_coord) - ] - h_tracks = [ - GlobalTrack(orientation=HORZ, idx=idx, coord=coord) - for idx, coord in enumerate(h_track_coord) - ] - - def bbox_to_faces(part, bbox): - left_track = v_tracks[v_track_coord.index(bbox.min.x)] - right_track = v_tracks[v_track_coord.index(bbox.max.x)] - bottom_track = h_tracks[h_track_coord.index(bbox.min.y)] - top_track = h_tracks[h_track_coord.index(bbox.max.y)] - Face(part, left_track, bottom_track, top_track) - Face(part, right_track, bottom_track, top_track) - Face(part, bottom_track, left_track, right_track) - Face(part, top_track, left_track, right_track) - if isinstance(part, Part): - part.left_track = left_track - part.right_track = right_track - part.top_track = top_track - part.bottom_track = bottom_track - - # Add routing box faces for each side of a part's labeled bounding box. - for part in node.parts: - part_bbox = (part.lbl_bbox * part.tx).round() - bbox_to_faces(part, part_bbox) - - # Add routing box faces for each side of the expanded bounding box surrounding - # all parts. - bbox_to_faces(boundary, routing_bbox) - - # Extend the part faces in each horizontal track and then each vertical track. - for track in h_tracks: - track.extend_faces(v_tracks) - for track in v_tracks: - track.extend_faces(h_tracks) - - # Apply splits to all faces and combine coincident faces. - for track in h_tracks + v_tracks: - track.split_faces() - track.remove_duplicate_faces() - - # Add adjacencies between faces that define global routing paths within - # switchboxes. - for h_track in h_tracks[1:]: - h_track.add_adjacencies() - - return h_tracks, v_tracks - - def create_terminals(node, internal_nets, h_tracks, v_tracks): - """Create terminals on the faces in the routing tracks.""" - - # Add terminals to all non-part/non-boundary faces. - for track in h_tracks + v_tracks: - for face in track: - face.create_nonpin_terminals() - - # Add terminals to switchbox faces for all part pins on internal nets. - for net in internal_nets: - for pin in node.get_internal_pins(net): - # Find the track (top/bottom/left/right) that the pin is on. - part = pin.part - pt = pin.route_pt * part.tx - closest_dist = abs(pt.y - part.top_track.coord) - pin_track = part.top_track - coord = pt.x # Pin coord within top track. - dist = abs(pt.y - part.bottom_track.coord) - if dist < closest_dist: - closest_dist = dist - pin_track = part.bottom_track - coord = pt.x # Pin coord within bottom track. - dist = abs(pt.x - part.left_track.coord) - if dist < closest_dist: - closest_dist = dist - pin_track = part.left_track - coord = pt.y # Pin coord within left track. - dist = abs(pt.x - part.right_track.coord) - if dist < closest_dist: - closest_dist = dist - pin_track = part.right_track - coord = pt.y # Pin coord within right track. - - # Now search for the face in the track that the pin is on. - for face in pin_track: - if part in face.part and face.beg.coord <= coord <= face.end.coord: - if not getattr(pin, "face", None): - # Only assign pin to face if it hasn't already been assigned - # to - # another face. This handles the case where a pin is exactly - # at the end coordinate and beginning coordinate of two - # successive faces in the same track. - pin.face = face - face.pins.append(pin) - terminal = Terminal(pin.net, face, coord) - face.terminals.append(terminal) - break - - # Set routing capacity of faces based on # of terminals on each face. - for track in h_tracks + v_tracks: - for face in track: - face.set_capacity() - - def global_router(node, nets): - """Globally route a list of nets from face to face. - - Args: - nets (list): List of Nets to be routed. - - Returns: - List: List of GlobalRoutes. - """ - - # This maze router assembles routes from each pin sequentially. - # - # 1. Find faces with net pins on them and place them on the - # start_faces list. - # 2. Randomly select one of the start faces. Add all the other - # faces to the stop_faces list. - # 3. Find a route from the start face to closest stop face. - # This concludes the initial phase of the routing. - # 4. Iterate through the remaining faces on the start_faces list. - # a. Randomly select a start face. - # b. Set stop faces to be all the faces currently on - # global routes. - # c. Find a route from the start face to any face on - # the global routes, thus enlarging the set of - # contiguous routes while reducing the number of - # unrouted start faces. - # d. Add the faces on the new route to the stop_faces list. - - # Core routing function. - def rt_srch(start_face, stop_faces): - """ - Return a minimal-distance path from the start face to one of the stop - faces. - - Args: - start_face (Face): Face from which path search begins - stop_faces (List): List of Faces at which search will end. - - Raises: - RoutingFailure: No path was found. - - Returns: - GlobalWire: List of Faces from start face to one of the stop faces. - """ - - # Return empty path if no stop faces or already starting from a stop face. - if start_face in stop_faces or not stop_faces: - return GlobalWire(net) - - # Record faces that have been visited and their distance from the start face - visited_faces = [start_face] - start_face.dist_from_start = 0 - - # Path searches are allowed to touch a Face on a Part if it - # has a Pin on the net being routed or if it is one of the stop faces. - # This is necessary to allow a search to terminate on a stop face or to - # pass through a face with a net pin on the way to finding a connection - # to one of the stop faces. - unconstrained_faces = stop_faces | net_pin_faces - - # Search through faces until a path is found & returned or a routing - # exception occurs. - while True: - # Set up for finding the closest unvisited face. - closest_dist = float("inf") - closest_face = None - - # Search for the closest face adjacent to the visited faces. - visited_faces.sort(key=lambda f: f.dist_from_start) - for visited_face in visited_faces: - if visited_face.dist_from_start > closest_dist: - # Visited face is already further than the current - # closest face, so no use continuing search since - # any remaining visited faces are even more distant. - break - - # Get the distances to the faces adjacent to this previously-visited - # face - # and update the closest face if appropriate. - for adj in visited_face.adjacent: - if adj.face in visited_faces: - # Don't re-visit faces that have already been visited. - continue - - if ( - adj.face not in unconstrained_faces - and adj.face.capacity <= 0 - ): - # Skip faces with insufficient routing capacity. - continue - - # Compute distance of this adjacent face to the start face. - dist = visited_face.dist_from_start + adj.dist - - if dist < closest_dist: - # Record the closest face seen so far. - closest_dist = dist - closest_face = adj.face - closest_face.prev_face = visited_face - - if not closest_face: - # Exception raised if couldn't find a path from start to stop faces. - raise GlobalRoutingFailure( - "Global routing failure: " f"{net.name} {net} {start_face.pins}" - ) - - # Add the closest adjacent face to the list of visited faces. - closest_face.dist_from_start = closest_dist - visited_faces.append(closest_face) - - if closest_face in stop_faces: - # The newest, closest face is actually on the list of stop faces, - # so the search is done. - # Now search back from this face to find the path back to the start - # face. - face_path = [closest_face] - while face_path[-1] is not start_face: - face_path.append(face_path[-1].prev_face) - - # Decrement the routing capacities of the path faces to account for - # this new routing. - # Don't decrement the stop face because any routing through it was - # accounted for - # during a previous routing. - for face in face_path[:-1]: - if face.capacity > 0: - face.capacity -= 1 - - # Reverse face path to go from start-to-stop face and return it. - return GlobalWire(net, reversed(face_path)) - - # Key function for setting the order in which nets will be globally routed. - def rank_net(net): - """Rank net based on W/H of bounding box of pins and the # of pins.""" - - # Nets with a small bounding box probably have fewer routing resources - # so they should be routed first. - - bbox = BBox() - for pin in node.get_internal_pins(net): - bbox.add(pin.route_pt) - return (bbox.w + bbox.h, len(net.pins)) - - # Set order in which nets will be routed. - nets.sort(key=rank_net) - - # Globally route each net. - global_routes = [] - - for net in nets: - # List for storing GlobalWires connecting pins on net. - global_route = GlobalRoute() - - # Faces with pins from which paths/routing originate. - net_pin_faces = {pin.face for pin in node.get_internal_pins(net)} - start_faces = set(net_pin_faces) - - # Select a random start face and look for a route to *any* of the other - # start faces. - start_face = random.choice(list(start_faces)) - start_faces.discard(start_face) - stop_faces = set(start_faces) - initial_route = rt_srch(start_face, stop_faces) - global_route.append(initial_route) - - # The faces on the route that was found now become the stopping faces for - # any further routing. - stop_faces = set(initial_route) - - # Go thru the other start faces looking for a connection to any existing - # route. - for start_face in start_faces: - next_route = rt_srch(start_face, stop_faces) - global_route.append(next_route) - - # Update the set of stopping faces with the faces on the newest route. - stop_faces |= set(next_route) - - # Add the complete global route for this net to the list of global routes. - global_routes.append(global_route) - - return global_routes - - def create_switchboxes(node, h_tracks, v_tracks, **options): - """Create routing switchboxes from the faces in the horz/vert tracks. - - Args: - h_tracks (list): List of horizontal Tracks. - v_tracks (list): List of vertical Tracks. - options (dict, optional): Dictionary of options and values. - - Returns: - list: List of Switchboxes. - """ - - # Clear any switchboxes associated with faces because we'll be making new ones. - for track in h_tracks + v_tracks: - for face in track: - face.switchboxes.clear() - - # For each horizontal Face, create a switchbox where it is the top face of the - # box. - switchboxes = [] - for h_track in h_tracks[1:]: - for face in h_track: - try: - # Create a Switchbox with the given Face on top and add it to the - # list. - switchboxes.append(SwitchBox(face)) - except NoSwitchBox: - continue - - # Check the switchboxes for problems. - for swbx in switchboxes: - swbx.audit() - - # Small switchboxes are more likely to fail routing so try to combine them into - # larger switchboxes. - # Use switchboxes containing nets for routing as seeds for coalescing into larger - # switchboxes. - seeds = [swbx for swbx in switchboxes if swbx.has_nets()] - - # Sort seeds by perimeter so smaller ones are coalesced before larger ones. - seeds.sort(key=lambda swbx: swbx.bbox.w + swbx.bbox.h) - - # Coalesce smaller switchboxes into larger ones having more routing area. - # The smaller switchboxes are removed from the list of switchboxes. - switchboxes = [seed.coalesce(switchboxes) for seed in seeds] - switchboxes = [swbx for swbx in switchboxes if swbx] # Remove None boxes. - - # A coalesced switchbox may have non-part faces containing multiple terminals - # on the same net. Remove all but one to prevent multi-path routes. - for switchbox in switchboxes: - switchbox.trim_repeated_terminals() - - return switchboxes - - def switchbox_router(node, switchboxes, **options): - """Create detailed wiring between the terminals along the sides of each switchbox. - - Args: - switchboxes (list): List of SwitchBox objects to be individually routed. - options (dict, optional): Dictionary of options and values. - - Returns: - None - """ - - # Do detailed routing inside each switchbox. - # TODO: Switchboxes are independent so could they be routed in parallel? - for swbx in switchboxes: - try: - # Try routing switchbox from left-to-right. - swbx.route(**options) - - except RoutingFailure: - # Routing failed, so try routing top-to-bottom instead. - swbx.flip_xy() - # If this fails, then a routing exception will terminate the whole - # routing process. - swbx.route(**options) - swbx.flip_xy() - - # Add switchbox routes to existing node wiring. - for net, segments in swbx.segments.items(): - node.wires[net].extend(segments) - - def cleanup_wires(node): - """Try to make wire segments look prettier.""" - - def order_seg_points(segments): - """Order endpoints in a horizontal or vertical segment.""" - for seg in segments: - if seg.p2 < seg.p1: - seg.p1, seg.p2 = seg.p2, seg.p1 - - def segments_bbox(segments): - """Return bounding box containing the given list of segments.""" - seg_pts = list(chain(*((s.p1, s.p2) for s in segments))) - return BBox(*seg_pts) - - def extract_horz_vert_segs(segments): - """Separate segments and return lists of horizontal & vertical segments.""" - horz_segs = [seg for seg in segments if seg.p1.y == seg.p2.y] - vert_segs = [seg for seg in segments if seg.p1.x == seg.p2.x] - assert len(horz_segs) + len(vert_segs) == len(segments) - return horz_segs, vert_segs - - def split_segments(segments, net_pin_pts): - """ - Return list of net segments split into the smallest intervals without - intersections with other segments. - """ - - # Check each horizontal segment against each vertical segment and split - # each one if they intersect. - # (This clunky iteration is used so the horz/vert lists can be updated - # within the loop.) - horz_segs, vert_segs = extract_horz_vert_segs(segments) - i = 0 - while i < len(horz_segs): - hseg = horz_segs[i] - hseg_y = hseg.p1.y - j = 0 - while j < len(vert_segs): - vseg = vert_segs[j] - vseg_x = vseg.p1.x - if ( - hseg.p1.x <= vseg_x <= hseg.p2.x - and vseg.p1.y <= hseg_y <= vseg.p2.y - ): - int_pt = Point(vseg_x, hseg_y) - if int_pt != hseg.p1 and int_pt != hseg.p2: - horz_segs.append( - Segment(copy.copy(int_pt), copy.copy(hseg.p2)) - ) - hseg.p2 = copy.copy(int_pt) - if int_pt != vseg.p1 and int_pt != vseg.p2: - vert_segs.append( - Segment(copy.copy(int_pt), copy.copy(vseg.p2)) - ) - vseg.p2 = copy.copy(int_pt) - j += 1 - i += 1 - - i = 0 - while i < len(horz_segs): - hseg = horz_segs[i] - hseg_y = hseg.p1.y - for pt in net_pin_pts: - if pt.y == hseg_y and hseg.p1.x < pt.x < hseg.p2.x: - horz_segs.append(Segment(copy.copy(pt), copy.copy(hseg.p2))) - hseg.p2 = copy.copy(pt) - i += 1 - - j = 0 - while j < len(vert_segs): - vseg = vert_segs[j] - vseg_x = vseg.p1.x - for pt in net_pin_pts: - if pt.x == vseg_x and vseg.p1.y < pt.y < vseg.p2.y: - vert_segs.append(Segment(copy.copy(pt), copy.copy(vseg.p2))) - vseg.p2 = copy.copy(pt) - j += 1 - - return horz_segs + vert_segs - - def merge_segments(segments): - """ - Return segments after merging those that run the same direction and overlap - """ - - # Preprocess the segments. - horz_segs, vert_segs = extract_horz_vert_segs(segments) - - merged_segs = [] - - # Separate horizontal segments having the same Y coord. - horz_segs_v = defaultdict(list) - for seg in horz_segs: - horz_segs_v[seg.p1.y].append(seg) - - # Merge overlapping segments having the same Y coord. - for segs in horz_segs_v.values(): - # Order segments by their starting X coord. - segs.sort(key=lambda s: s.p1.x) - # Append first segment to list of merged segments. - merged_segs.append(segs[0]) - # Go thru the remaining segments looking for overlaps with the last - # entry on the merge list. - for seg in segs[1:]: - if seg.p1.x <= merged_segs[-1].p2.x: - # Segments overlap, so update the extent of the last entry. - merged_segs[-1].p2.x = max(seg.p2.x, merged_segs[-1].p2.x) - else: - # No overlap, so append the current segment to the merge list - # and use it for - # further checks of intersection with remaining segments. - merged_segs.append(seg) - - # Separate vertical segments having the same X coord. - vert_segs_h = defaultdict(list) - for seg in vert_segs: - vert_segs_h[seg.p1.x].append(seg) - - # Merge overlapping segments having the same X coord. - for segs in vert_segs_h.values(): - # Order segments by their starting Y coord. - segs.sort(key=lambda s: s.p1.y) - # Append first segment to list of merged segments. - merged_segs.append(segs[0]) - # Go thru the remaining segments looking for overlaps with the last - # entry on the merge list. - for seg in segs[1:]: - if seg.p1.y <= merged_segs[-1].p2.y: - # Segments overlap, so update the extent of the last entry. - merged_segs[-1].p2.y = max(seg.p2.y, merged_segs[-1].p2.y) - else: - # No overlap, so append the current segment to the merge list - # and use it for - # further checks of intersection with remaining segments. - merged_segs.append(seg) - - return merged_segs - - def break_cycles(segments): - """Remove segments to break any cycles of a net's segments.""" - - # Create a dict storing set of segments adjacent to each endpoint. - adj_segs = defaultdict(set) - for seg in segments: - # Add segment to set for each endpoint. - adj_segs[seg.p1].add(seg) - adj_segs[seg.p2].add(seg) - - # Create a dict storing the list of endpoints adjacent to each endpoint. - adj_pts = dict() - for pt, segs in adj_segs.items(): - # Store endpoints of all segments adjacent to endpoint, then remove the - # endpoint. - adj_pts[pt] = list({p for seg in segs for p in (seg.p1, seg.p2)}) - adj_pts[pt].remove(pt) - - # Start at any endpoint and visit adjacent endpoints until all have been - # visited. - # If an endpoint is seen more than once, then a cycle exists. Remove the - # segment forming the cycle. - visited_pts = [] # List of visited endpoints. - frontier_pts = list(adj_pts.keys())[:1] # Arbitrary starting point. - while frontier_pts: - # Visit a point on the frontier. - frontier_pt = frontier_pts.pop() - visited_pts.append(frontier_pt) - - # Check each adjacent endpoint for cycles. - for adj_pt in adj_pts[frontier_pt][:]: - if adj_pt in visited_pts + frontier_pts: - # This point was already reached by another path so there is a - # cycle. - # Break it by removing segment between frontier_pt and adj_pt. - loop_seg = (adj_segs[frontier_pt] & adj_segs[adj_pt]).pop() - segments.remove(loop_seg) - adj_segs[frontier_pt].remove(loop_seg) - adj_segs[adj_pt].remove(loop_seg) - adj_pts[frontier_pt].remove(adj_pt) - adj_pts[adj_pt].remove(frontier_pt) - else: - # First time adjacent point has been reached, so add it to - # frontier. - frontier_pts.append(adj_pt) - # Keep this new frontier point from backtracking to the current - # frontier point later. - adj_pts[adj_pt].remove(frontier_pt) - - return segments - - def is_pin_pt(pt): - """Return True if the point is on one of the part pins.""" - return pt in pin_pts - - def contains_pt(seg, pt): - """Return True if the point is contained within the horz/vert segment.""" - return seg.p1.x <= pt.x <= seg.p2.x and seg.p1.y <= pt.y <= seg.p2.y - - def trim_stubs(segments): - """ - Return segments after removing stubs that have an unconnected endpoint. - """ - - def get_stubs(segments): - """Return set of stub segments.""" - - # For end point, the dict entry contains a list of the segments that - # meet there. - stubs = defaultdict(list) - - # Process the segments looking for points that are on only a single - # segment. - for seg in segments: - # Add the segment to the segment list of each end point. - stubs[seg.p1].append(seg) - stubs[seg.p2].append(seg) - - # Keep only the segments with an unconnected endpoint that is not on a - # part pin. - stubs = { - segs[0] - for endpt, segs in stubs.items() - if len(segs) == 1 and not is_pin_pt(endpt) - } - return stubs - - trimmed_segments = set(segments[:]) - stubs = get_stubs(trimmed_segments) - while stubs: - trimmed_segments -= stubs - stubs = get_stubs(trimmed_segments) - return list(trimmed_segments) - - def remove_jogs_old(net, segments, wires, net_bboxes, part_bboxes): - """Remove jogs in wiring segments. - - Args: - net (Net): Net whose wire segments will be modified. - segments (list): List of wire segments for the given net. - wires (dict): Dict of lists of wire segments indexed by nets. - net_bboxes (dict): Dict of BBoxes for wire segments indexed by nets. - part_bboxes (list): List of BBoxes for the placed parts. - """ - - def get_touching_segs(seg, ortho_segs): - """Return list of orthogonal segments that touch the given segment.""" - touch_segs = set() - for oseg in ortho_segs: - # oseg horz, seg vert. Do they intersect? - if contains_pt(oseg, Point(seg.p2.x, oseg.p1.y)): - touch_segs.add(oseg) - # oseg vert, seg horz. Do they intersect? - elif contains_pt(oseg, Point(oseg.p2.x, seg.p1.y)): - touch_segs.add(oseg) - return list(touch_segs) # Convert to list with no dups. - - def get_overlap(*segs): - """ - Find extent of overlap of parallel horz/vert - segments and return as (min, max) tuple. - """ - ov1 = float("-inf") - ov2 = float("inf") - for seg in segs: - if seg.p1.y == seg.p2.y: - # Horizontal segment. - p1, p2 = seg.p1.x, seg.p2.x - else: - # Vertical segment. - p1, p2 = seg.p1.y, seg.p2.y - ov1 = max(ov1, p1) # Max of extent minimums. - ov2 = min(ov2, p2) # Min of extent maximums. - # assert ov1 <= ov2 - return ov1, ov2 - - def obstructed(segment): - """Return true if segment obstructed by parts or segments of other nets.""" - - # Obstructed if segment bbox intersects one of the part bboxes. - segment_bbox = BBox(segment.p1, segment.p2) - for part_bbox in part_bboxes: - if part_bbox.intersects(segment_bbox): - return True - - # BBoxes don't intersect if they line up exactly edge-to-edge. - # So expand the segment bbox slightly so intersections with bboxes of - # other segments will be detected. - segment_bbox = segment_bbox.resize(Vector(1, 1)) - - # Look for an overlay intersection with a segment of another net. - for nt, nt_bbox in net_bboxes.items(): - if nt is net: - # Don't check this segment with other segments of its own net. - continue - - if not segment_bbox.intersects(nt_bbox): - # Don't check this segment against segments of another net whose - # bbox doesn't even intersect this segment. - continue - - # Check for overlay intersectionss between this segment and the - # parallel segments of the other net. - for seg in wires[nt]: - if segment.p1.x == segment.p2.x == seg.p1.x == seg.p2.x: - # Segments are both aligned vertically on the same track - # X coord. - if segment.p1.y <= seg.p2.y and segment.p2.y >= seg.p1.y: - # Segments overlap so segment is obstructed. - return True - elif segment.p1.y == segment.p2.y == seg.p1.y == seg.p2.y: - # Segments are both aligned horizontally on the same track - # Y coord. - if segment.p1.x <= seg.p2.x and segment.p2.x >= seg.p1.x: - # Segments overlap so segment is obstructed. - return True - - # No obstructions found, so return False. - return False - - # Make sure p1 <= p2 for segment endpoints. - order_seg_points(segments) - - # Split segments into horizontal/vertical groups. - horz_segs, vert_segs = extract_horz_vert_segs(segments) - - # Look for a segment touched by ends of orthogonal segments all pointing in - # the same direction. - # Then slide this segment to the other end of the interval by which the - # touching segments - # overlap. This will reduce or eliminate the jog. - stop = True - for segs, ortho_segs in ((horz_segs, vert_segs), (vert_segs, horz_segs)): - for seg in segs: - # Don't move a segment if one of its endpoints connects to a part - # pin. - if is_pin_pt(seg.p1) or is_pin_pt(seg.p2): - continue - - # Find all orthogonal segments that touch this one. - touching_segs = get_touching_segs(seg, ortho_segs) - - # Find extent of overlap of all orthogonal segments. - ov1, ov2 = get_overlap(*touching_segs) - - if ov1 >= ov2: - # No overlap, so this segment can't be moved one way or - # the other. - continue - - if seg in horz_segs: - # Move horz segment vertically to other end of overlap to remove - # jog. - test_seg = Segment(seg.p1, seg.p2) - seg_y = test_seg.p1.y - if seg_y == ov1: - # Segment is at one end of the overlay, so move it to the - # other end. - test_seg.p1.y = ov2 - test_seg.p2.y = ov2 - if not obstructed(test_seg): - # Segment move is not obstructed, so accept it. - seg.p1 = test_seg.p1 - seg.p2 = test_seg.p2 - # If one segment is moved, maybe more can be moved so - # don't stop. - stop = False - elif seg_y == ov2: - # Segment is at one end of the overlay, so move it to the - # other end. - test_seg.p1.y = ov1 - test_seg.p2.y = ov1 - if not obstructed(test_seg): - # Segment move is not obstructed, so accept it. - seg.p1 = test_seg.p1 - seg.p2 = test_seg.p2 - # If one segment is moved, maybe more can be moved so - # don't stop. - stop = False - else: - # Segment in interior of overlay, so it's not a jog. Don't - # move it. - pass - else: - # Move vert segment horizontally to other end of overlap to - # remove jog. - test_seg = Segment(seg.p1, seg.p2) - seg_x = seg.p1.x - if seg_x == ov1: - # Segment is at one end of the overlay, so move it to the - # other end. - test_seg.p1.x = ov2 - test_seg.p2.x = ov2 - if not obstructed(test_seg): - # Segment move is not obstructed, so accept it. - seg.p1 = test_seg.p1 - seg.p2 = test_seg.p2 - # If one segment is moved, maybe more can be moved so - # don't stop. - stop = False - elif seg_x == ov2: - # Segment is at one end of the overlay, so move it to the - # other end. - test_seg.p1.x = ov1 - test_seg.p2.x = ov1 - if not obstructed(test_seg): - # Segment move is not obstructed, so accept it. - seg.p1 = test_seg.p1 - seg.p2 = test_seg.p2 - # If one segment is moved, maybe more can be moved so - # don't stop. - stop = False - else: - # Segment in interior of overlay, so it's not a jog. Don't - # move it. - pass - - # Return updated segments. If no segments for this net were updated, then - # stop is True. - return segments, stop - - def remove_jogs(net, segments, wires, net_bboxes, part_bboxes): - """Remove jogs and staircases in wiring segments. - - Args: - net (Net): Net whose wire segments will be modified. - segments (list): List of wire segments for the given net. - wires (dict): Dict of lists of wire segments indexed by nets. - net_bboxes (dict): Dict of BBoxes for wire segments indexed by nets. - part_bboxes (list): List of BBoxes for the placed parts. - """ - - def obstructed(segment): - """ - Return true if segment obstructed by parts or segments of other nets. - """ - - # Obstructed if segment bbox intersects one of the part bboxes. - segment_bbox = BBox(segment.p1, segment.p2) - for part_bbox in part_bboxes: - if part_bbox.intersects(segment_bbox): - return True - - # BBoxes don't intersect if they line up exactly edge-to-edge. - # So expand the segment bbox slightly so intersections with bboxes of - # other segments will be detected. - segment_bbox = segment_bbox.resize(Vector(2, 2)) - - # Look for an overlay intersection with a segment of another net. - for nt, nt_bbox in net_bboxes.items(): - if nt is net: - # Don't check this segment with other segments of its own net. - continue - - if not segment_bbox.intersects(nt_bbox): - # Don't check this segment against segments of another net whose - # bbox doesn't even intersect this segment. - continue - - # Check for overlay intersectionss between this segment and the - # parallel segments of the other net. - for seg in wires[nt]: - if segment.p1.x == segment.p2.x == seg.p1.x == seg.p2.x: - # Segments are both aligned vertically on the same track - # X coord. - if segment.p1.y <= seg.p2.y and segment.p2.y >= seg.p1.y: - # Segments overlap so segment is obstructed. - return True - elif segment.p1.y == segment.p2.y == seg.p1.y == seg.p2.y: - # Segments are both aligned horizontally on the same track - # Y coord. - if segment.p1.x <= seg.p2.x and segment.p2.x >= seg.p1.x: - # Segments overlap so segment is obstructed. - return True - - # No obstructions found, so return False. - return False - - def get_corners(segments): - """ - Return dictionary of right-angle corner - points and lists of associated segments. - """ - - # For each corner point, the dict entry contains a list of the segments - # that meet there. - corners = defaultdict(list) - - # Process the segments so that any potential right-angle corner has the - # horizontal - # segment followed by the vertical segment. - horz_segs, vert_segs = extract_horz_vert_segs(segments) - for seg in horz_segs + vert_segs: - # Add the segment to the segment list of each end point. - corners[seg.p1].append(seg) - corners[seg.p2].append(seg) - - # Keep only the corner points where two segments meet at right angles at - # a point not on a part pin. - corners = { - corner: segs - for corner, segs in corners.items() - if len(segs) == 2 - and not is_pin_pt(corner) - and segs[0] in horz_segs - and segs[1] in vert_segs - } - return corners - - def get_jogs(segments): - """ - Yield the three segments and starting and end points of a staircase or - tophat jog. - """ - - # Get dict of right-angle corners formed by segments. - corners = get_corners(segments) - - # Look for segments with both endpoints on right-angle corners, - # indicating this segment - # is in the middle of a three-segment staircase or tophat jog. - for segment in segments: - if segment.p1 in corners and segment.p2 in corners: - # Get the three segments in the jog. - jog_segs = set() - jog_segs.add(corners[segment.p1][0]) - jog_segs.add(corners[segment.p1][1]) - jog_segs.add(corners[segment.p2][0]) - jog_segs.add(corners[segment.p2][1]) - - # Get the points where the three-segment jog starts and stops. - start_stop_pts = set() - for seg in jog_segs: - start_stop_pts.add(seg.p1) - start_stop_pts.add(seg.p2) - start_stop_pts.discard(segment.p1) - start_stop_pts.discard(segment.p2) - - # Send the jog that was found. - yield list(jog_segs), list(start_stop_pts) - - # Shuffle segments to vary the order of detected jogs. - random.shuffle(segments) - - # Get iterator for jogs. - jogs = get_jogs(segments) - - # Search for jogs and break from the loop if a correctable jog is found or - # we run out of jogs. - while True: - # Get the segments and start-stop points for the next jog. - try: - jog_segs, start_stop_pts = next(jogs) - except StopIteration: - # No more jogs and no corrections made, so return segments and stop - # flag is true. - return segments, True - - # Get the start-stop points and order them so p1 < p3. - p1, p3 = sorted(start_stop_pts) - - # These are the potential routing points for correcting the jog. - # Either start at p1 and move vertically and then horizontally to p3, or - # move horizontally from p1 and then vertically to p3. - p2s = [Point(p1.x, p3.y), Point(p3.x, p1.y)] - - # Shuffle the routing points so the applied correction isn't always the - # same orientation. - random.shuffle(p2s) - - # Check each routing point to see if it leads to a valid routing. - for p2 in p2s: - # Replace the three-segment jog with these two right-angle segments. - new_segs = [ - Segment(copy.copy(pa), copy.copy(pb)) - for pa, pb in ((p1, p2), (p2, p3)) - if pa != pb - ] - order_seg_points(new_segs) - - # Check the new segments to see if they run into parts or segmentsof - # other nets. - if not any((obstructed(new_seg) for new_seg in new_segs)): - # OK, segments are good so replace the old segments in the jog - # with them. - for seg in jog_segs: - segments.remove(seg) - segments.extend(new_segs) - - # Return updated segments and set stop flag to false because - # segments were modified. - return segments, False - - # Get part bounding boxes so parts can be avoided when modifying net segments. - part_bboxes = [p.bbox * p.tx for p in node.parts] - - # Get dict of bounding boxes for the nets in this node. - net_bboxes = {net: segments_bbox(segs) for net, segs in node.wires.items()} - - # Get locations for part pins of each net. - # (For use when splitting net segments.) - net_pin_pts = dict() - for net in node.wires.keys(): - net_pin_pts[net] = [ - (pin.pt * pin.part.tx).round() for pin in node.get_internal_pins(net) - ] - - # Do a generalized cleanup of the wire segments of each net. - for net, segments in node.wires.items(): - # Round the wire segment endpoints to integers. - segments = [seg.round() for seg in segments] - - # Keep only non zero-length segments. - segments = [seg for seg in segments if seg.p1 != seg.p2] - - # Make sure the segment endpoints are in the right order. - order_seg_points(segments) - - # Merge colinear, overlapping segments. - # Also removes any duplicated segments. - segments = merge_segments(segments) - - # Split intersecting segments. - segments = split_segments(segments, net_pin_pts[net]) - - # Break loops of segments. - segments = break_cycles(segments) - - # Keep only non zero-length segments. - segments = [seg for seg in segments if seg.p1 != seg.p2] - - # Trim wire stubs. - segments = trim_stubs(segments) - - node.wires[net] = segments - - # Remove jogs in the wire segments of each net. - keep_cleaning = True - while keep_cleaning: - keep_cleaning = False - - for net, segments in node.wires.items(): - while True: - # Split intersecting segments. - segments = split_segments(segments, net_pin_pts[net]) - - # Remove unnecessary wire jogs. - segments, stop = remove_jogs( - net, segments, node.wires, net_bboxes, part_bboxes - ) - - # Keep only non zero-length segments. - segments = [seg for seg in segments if seg.p1 != seg.p2] - - # Merge segments made colinear by removing jogs. - segments = merge_segments(segments) - - # Split intersecting segments. - segments = split_segments(segments, net_pin_pts[net]) - - # Keep only non zero-length segments. - segments = [seg for seg in segments if seg.p1 != seg.p2] - - # Trim wire stubs caused by removing jogs. - segments = trim_stubs(segments) - - if stop: - # Break from loop once net segments can no longer be improved. - break - - # Recalculate the net bounding box after modifying its segments. - net_bboxes[net] = segments_bbox(segments) - - keep_cleaning = True - - # Merge segments made colinear by removing jogs. - segments = merge_segments(segments) - - # Update the node net's wire with the cleaned version. - node.wires[net] = segments - - def add_junctions(node): - """Add X & T-junctions where wire segments in the same net meet.""" - - def find_junctions(route): - """Find junctions where segments of a net intersect. - - Args: - route (List): List of Segment objects. - - Returns: - List: List of Points, one for each junction. - - Notes: - You must run merge_segments() before finding junctions - or else the segment endpoints might not be ordered - correctly with p1 < p2. - """ - - # Separate route into vertical and horizontal segments. - horz_segs = [seg for seg in route if seg.p1.y == seg.p2.y] - vert_segs = [seg for seg in route if seg.p1.x == seg.p2.x] - - junctions = [] - - # Check each pair of horz/vert segments for an intersection, except - # where they form a right-angle turn. - for hseg in horz_segs: - hseg_y = hseg.p1.y # Horz seg Y coord. - for vseg in vert_segs: - vseg_x = vseg.p1.x # Vert seg X coord. - if (hseg.p1.x < vseg_x < hseg.p2.x) and ( - vseg.p1.y <= hseg_y <= vseg.p2.y - ): - # The vert segment intersects the interior of the horz seg. - junctions.append(Point(vseg_x, hseg_y)) - elif (vseg.p1.y < hseg_y < vseg.p2.y) and ( - hseg.p1.x <= vseg_x <= hseg.p2.x - ): - # The horz segment intersects the interior of the vert seg. - junctions.append(Point(vseg_x, hseg_y)) - - return junctions - - for net, segments in node.wires.items(): - # Add X & T-junctions between segments in the same net. - junctions = find_junctions(segments) - node.junctions[net].extend(junctions) - - def rmv_routing_stuff(node): - """Remove attributes added to parts/pins during routing.""" - - rmv_attr(node.parts, ("left_track", "right_track", "top_track", "bottom_track")) - for part in node.parts: - rmv_attr(part.pins, ("route_pt", "face")) - - def route(node, tool=None, **options): - """Route the wires between part pins in this node and its children. - - Steps: - 1. Divide the bounding box surrounding the parts into switchboxes. - 2. Do global routing of nets through sequences of switchboxes. - 3. Do detailed routing within each switchbox. - - Args: - node (Node): Hierarchical node containing the parts to be connected. - tool (str): Backend tool for schematics. - options (dict, optional): Dictionary of options and values: - "allow_routing_failure", "draw", "draw_all_terminals", - "show_capacities", "draw_switchbox", "draw_routing", - "draw_channels" - """ - - # Inject the constants for the backend tool into this module. - import skidl - from skidl.tools import tool_modules - - tool = tool or skidl.config.tool - this_module = sys.modules[__name__] - this_module.__dict__.update(tool_modules[tool].constants.__dict__) - - random.seed(options.get("seed")) - - # Remove any stuff leftover from a previous place & route run. - node.rmv_routing_stuff() - - # First, recursively route any children of this node. - # TODO: Child nodes are independent so could they be processed in parallel? - for child in node.children.values(): - child.route(tool=tool, **options) - - # Exit if no parts to route in this node. - if not node.parts: - return - - # Get all the nets that have one or more pins within this node. - internal_nets = node.get_internal_nets() - - # Exit if no nets to route. - if not internal_nets: - return - - try: - # Extend routing points of part pins to the edges of their bounding boxes. - node.add_routing_points(internal_nets) - - # Create the surrounding box that contains the entire routing area. - channel_sz = (len(internal_nets) + 1) * GRID - routing_bbox = ( - node.internal_bbox().resize(Vector(channel_sz, channel_sz)) - ).round() - - # Create horizontal & vertical global routing tracks and faces. - h_tracks, v_tracks = node.create_routing_tracks(routing_bbox) - - # Create terminals on the faces in the routing tracks. - node.create_terminals(internal_nets, h_tracks, v_tracks) - - # Draw part outlines, routing tracks and terminals. - if options.get("draw_routing_channels"): - draw_routing( - node, routing_bbox, node.parts, h_tracks, v_tracks, **options - ) - - # Do global routing of nets internal to the node. - global_routes = node.global_router(internal_nets) - - # Convert the global face-to-face routes into terminals on the switchboxes. - for route in global_routes: - route.cvt_faces_to_terminals() - - # If enabled, draw the global routing for debug purposes. - if options.get("draw_global_routing"): - draw_routing( - node, - routing_bbox, - node.parts, - h_tracks, - v_tracks, - global_routes, - **options, - ) - - # Create detailed wiring using switchbox routing for the global routes. - switchboxes = node.create_switchboxes(h_tracks, v_tracks) - - # Draw switchboxes and routing channels. - if options.get("draw_assigned_terminals"): - draw_routing( - node, - routing_bbox, - node.parts, - switchboxes, - global_routes, - **options, - ) - - node.switchbox_router(switchboxes, **options) - - # If enabled, draw the global and detailed routing for debug purposes. - if options.get("draw_switchbox_routing"): - draw_routing( - node, - routing_bbox, - node.parts, - global_routes, - switchboxes, - **options, - ) - - # Now clean-up the wires and add junctions. - node.cleanup_wires() - node.add_junctions() - - # If enabled, draw the global and detailed routing for debug purposes. - if options.get("draw_switchbox_routing"): - draw_routing(node, routing_bbox, node.parts, **options) - - # Remove any stuff leftover from this place & route run. - node.rmv_routing_stuff() - - except RoutingFailure: - # Remove any stuff leftover from this place & route run. - node.rmv_routing_stuff() - raise RoutingFailure diff --git a/src/faebryk/exporters/schematic/kicad/schematic.py b/src/faebryk/exporters/schematic/kicad/schematic.py deleted file mode 100644 index a231f4aa..00000000 --- a/src/faebryk/exporters/schematic/kicad/schematic.py +++ /dev/null @@ -1,822 +0,0 @@ -# -*- coding: utf-8 -*- - -# The MIT License (MIT) - Copyright (c) Dave Vandenbout. - - -from dataclasses import dataclass -import datetime -import os.path -import re -import time -from collections import Counter, OrderedDict - -import faebryk.library._F as F -from faebryk.core.graphinterface import Graph -from faebryk.core.module import Module -from faebryk.core.node import Node -from faebryk.core.trait import Trait -from faebryk.exporters.schematic.kicad.transformer import SchTransformer -from faebryk.libs.util import cast_assert - -from .bboxes import calc_hier_label_bbox, calc_symbol_bbox -from .constants import BLK_INT_PAD, BOX_LABEL_FONT_SIZE, GRID, PIN_LABEL_FONT_SIZE - -# from skidl.scriptinfo import get_script_name -from .geometry import BBox, Point, Tx, Vector -from .net_terminal import NetTerminal - -# from skidl.utilities import rmv_attr - -""" -Functions for generating a KiCad EESCHEMA schematic. -""" - - -@dataclass -class Options: - allow_routing_failure: bool = False - compress_before_place: bool = False - dont_rotate_pin_count: int = 10000 - draw_assigned_terminals: bool = False - draw_font: str = "Arial" - draw_global_routing: bool = False - draw_placement: bool = False - draw_routing_channels: bool = False - draw_routing: bool = False - draw_scr: bool = True - draw_switchbox_boundary: bool = False - draw_switchbox_routing: bool = False - draw_tx: Tx = Tx() - expansion_factor: float = 1.0 - graphics_only: bool = False - net_normalize: bool = False - pin_normalize: bool = False - pt_to_pt_mult: float = 1.0 - rotate_parts: bool = False - seed: int = 0 - show_capacities: bool = False - terminal_evolution: str = "all_at_once" - use_push_pull: bool = True - - -def bbox_to_eeschema(bbox, tx, name=None): - """Create a bounding box using EESCHEMA graphic lines.""" - - # Make sure the box corners are integers. - bbox = (bbox * tx).round() - - graphic_box = [] - - if name: - # Place name at the lower-left corner of the box. - name_pt = bbox.ul - graphic_box.append( - "Text Notes {} {} 0 {} ~ 20\n{}".format( - name_pt.x, name_pt.y, BOX_LABEL_FONT_SIZE, name - ) - ) - - graphic_box.append("Wire Notes Line") - graphic_box.append( - " {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.lr.x, bbox.lr.y) - ) - graphic_box.append("Wire Notes Line") - graphic_box.append( - " {} {} {} {}".format(bbox.lr.x, bbox.lr.y, bbox.ur.x, bbox.ur.y) - ) - graphic_box.append("Wire Notes Line") - graphic_box.append( - " {} {} {} {}".format(bbox.ur.x, bbox.ur.y, bbox.ul.x, bbox.ul.y) - ) - graphic_box.append("Wire Notes Line") - graphic_box.append( - " {} {} {} {}".format(bbox.ul.x, bbox.ul.y, bbox.ll.x, bbox.ll.y) - ) - graphic_box.append("") # For blank line at end. - - return "\n".join(graphic_box) - - -def net_to_eeschema(self, tx): - """Generate the EESCHEMA code for the net terminal. - - Args: - tx (Tx): Transformation matrix for the node containing this net terminal. - - Returns: - str: EESCHEMA code string. - """ - self.pins[0].stub = True - self.pins[0].orientation = "R" - return pin_label_to_eeschema(self.pins[0], tx) - # return pin_label_to_eeschema(self.pins[0], tx) + bbox_to_eeschema(self.bbox, self.tx * tx) - - -def part_to_eeschema(part, tx): - """Create EESCHEMA code for a part. - - Args: - part (Part): SKiDL part. - tx (Tx): Transformation matrix. - - Returns: - string: EESCHEMA code for the part. - - Notes: - https://en.wikibooks.org/wiki/Kicad/file_formats#Schematic_Files_Format - """ - - tx = part.tx * tx - origin = tx.origin.round() - time_hex = hex(int(time.time()))[2:] - unit_num = getattr(part, "num", 1) - - eeschema = [] - eeschema.append("$Comp") - lib = os.path.splitext(part.lib.filename)[0] - eeschema.append("L {}:{} {}".format(lib, part.name, part.ref)) - eeschema.append("U {} 1 {}".format(unit_num, time_hex)) - eeschema.append("P {} {}".format(str(origin.x), str(origin.y))) - - # Add part symbols. For now we are only adding the designator - n_F0 = 1 - for i in range(len(part.draw)): - if re.search("^DrawF0", str(part.draw[i])): - n_F0 = i - break - eeschema.append( - 'F 0 "{}" {} {} {} {} {} {} {}'.format( - part.ref, - part.draw[n_F0].orientation, - str(origin.x + part.draw[n_F0].x), - str(origin.y + part.draw[n_F0].y), - part.draw[n_F0].size, - "000", # TODO: Refine this to match part def. - part.draw[n_F0].halign, - part.draw[n_F0].valign, - ) - ) - - # Part value. - n_F1 = 1 - for i in range(len(part.draw)): - if re.search("^DrawF1", str(part.draw[i])): - n_F1 = i - break - eeschema.append( - 'F 1 "{}" {} {} {} {} {} {} {}'.format( - str(part.value), - part.draw[n_F1].orientation, - str(origin.x + part.draw[n_F1].x), - str(origin.y + part.draw[n_F1].y), - part.draw[n_F1].size, - "000", # TODO: Refine this to match part def. - part.draw[n_F1].halign, - part.draw[n_F1].valign, - ) - ) - - # Part footprint. - n_F2 = 2 - for i in range(len(part.draw)): - if re.search("^DrawF2", str(part.draw[i])): - n_F2 = i - break - eeschema.append( - 'F 2 "{}" {} {} {} {} {} {} {}'.format( - part.footprint, - part.draw[n_F2].orientation, - str(origin.x + part.draw[n_F2].x), - str(origin.y + part.draw[n_F2].y), - part.draw[n_F2].size, - "001", # TODO: Refine this to match part def. - part.draw[n_F2].halign, - part.draw[n_F2].valign, - ) - ) - eeschema.append(" 1 {} {}".format(str(origin.x), str(origin.y))) - eeschema.append(" {} {} {} {}".format(tx.a, tx.b, tx.c, tx.d)) - eeschema.append("$EndComp") - eeschema.append("") # For blank line at end. - - # For debugging: draws a bounding box around a part. - # eeschema.append(bbox_to_eeschema(part.bbox, tx)) - # eeschema.append(bbox_to_eeschema(part.place_bbox, tx)) - - return "\n".join(eeschema) - - -def wire_to_eeschema(net, wire, tx): - """Create EESCHEMA code for a multi-segment wire. - - Args: - net (Net): Net associated with the wire. - wire (list): List of Segments for a wire. - tx (Tx): transformation matrix for each point in the wire. - - Returns: - string: Text to be placed into EESCHEMA file. - """ - - eeschema = [] - for segment in wire: - eeschema.append("Wire Wire Line") - w = (segment * tx).round() - eeschema.append(" {} {} {} {}".format(w.p1.x, w.p1.y, w.p2.x, w.p2.y)) - eeschema.append("") # For blank line at end. - return "\n".join(eeschema) - - -def junction_to_eeschema(net, junctions, tx): - eeschema = [] - for junction in junctions: - pt = (junction * tx).round() - eeschema.append("Connection ~ {} {}".format(pt.x, pt.y)) - eeschema.append("") # For blank line at end. - return "\n".join(eeschema) - - -def power_part_to_eeschema(part, tx=Tx()): - return "" # REMOVE: Remove this. - out = [] - for pin in part.pins: - try: - if not (pin.net is None): - if pin.net.netclass == "Power": - # strip out the '_...' section from power nets - t = pin.net.name - u = t.split("_") - symbol_name = u[0] - # find the stub in the part - time_hex = hex(int(time.time()))[2:] - pin_pt = (part.origin + offset + Point(pin.x, pin.y)).round() - x, y = pin_pt.x, pin_pt.y - out.append("$Comp\n") - out.append("L power:{} #PWR?\n".format(symbol_name)) - out.append("U 1 1 {}\n".format(time_hex)) - out.append("P {} {}\n".format(str(x), str(y))) - # Add part symbols. For now we are only adding the designator - n_F0 = 1 - for i in range(len(part.draw)): - if re.search("^DrawF0", str(part.draw[i])): - n_F0 = i - break - part_orientation = part.draw[n_F0].orientation - part_horizontal_align = part.draw[n_F0].halign - part_vertical_align = part.draw[n_F0].valign - - # check if the pin orientation will clash with the power part - if "+" in symbol_name: - # voltage sources face up, so check if the pin is facing down (opposite logic y-axis) - if pin.orientation == "U": - orientation = [-1, 0, 0, 1] - elif "gnd" in symbol_name.lower(): - # gnd points down so check if the pin is facing up (opposite logic y-axis) - if pin.orientation == "D": - orientation = [-1, 0, 0, 1] - out.append( - 'F 0 "{}" {} {} {} {} {} {} {}\n'.format( - "#PWR?", - part_orientation, - str(x + 25), - str(y + 25), - str(40), - "001", - part_horizontal_align, - part_vertical_align, - ) - ) - out.append( - 'F 1 "{}" {} {} {} {} {} {} {}\n'.format( - symbol_name, - part_orientation, - str(x + 25), - str(y + 25), - str(40), - "000", - part_horizontal_align, - part_vertical_align, - ) - ) - out.append(" 1 {} {}\n".format(str(x), str(y))) - out.append( - " {} {} {} {}\n".format( - orientation[0], - orientation[1], - orientation[2], - orientation[3], - ) - ) - out.append("$EndComp\n") - except Exception as inst: - print(type(inst)) - print(inst.args) - print(inst) - return "\n" + "".join(out) - - -# Sizes of EESCHEMA schematic pages from smallest to largest. Dimensions in mils. -A_sizes_list = [ - ("A4", BBox(Point(0, 0), Point(11693, 8268))), - ("A3", BBox(Point(0, 0), Point(16535, 11693))), - ("A2", BBox(Point(0, 0), Point(23386, 16535))), - ("A1", BBox(Point(0, 0), Point(33110, 23386))), - ("A0", BBox(Point(0, 0), Point(46811, 33110))), -] - -# Create bounding box for each A size sheet. -A_sizes = OrderedDict(A_sizes_list) - - -def get_A_size(bbox): - """Return the A-size page needed to fit the given bounding box.""" - - width = bbox.w - height = bbox.h * 1.25 # HACK: why 1.25? - for A_size, page in A_sizes.items(): - if width < page.w and height < page.h: - return A_size - return "A0" # Nothing fits, so use the largest available. - - -def calc_sheet_tx(bbox): - """Compute the page size and positioning for this sheet.""" - A_size = get_A_size(bbox) - page_bbox = bbox * Tx(d=-1) - move_to_ctr = A_sizes[A_size].ctr.snap(GRID) - page_bbox.ctr.snap(GRID) - move_tx = Tx(d=-1).move(move_to_ctr) - return move_tx - - -def calc_pin_dir(pin): - """Calculate pin direction accounting for part transformation matrix.""" - - # Copy the part trans. matrix, but remove the translation vector, leaving only scaling/rotation stuff. - tx = pin.part.tx - tx = Tx(a=tx.a, b=tx.b, c=tx.c, d=tx.d) - - # Use the pin orientation to compute the pin direction vector. - pin_vector = { - "U": Point(0, 1), - "D": Point(0, -1), - "L": Point(-1, 0), - "R": Point(1, 0), - }[pin.orientation] - - # Rotate the direction vector using the part rotation matrix. - pin_vector = pin_vector * tx - - # Create an integer tuple from the rotated direction vector. - pin_vector = (int(round(pin_vector.x)), int(round(pin_vector.y))) - - # Return the pin orientation based on its rotated direction vector. - return { - (0, 1): "U", - (0, -1): "D", - (-1, 0): "L", - (1, 0): "R", - }[pin_vector] - - -def pin_label_to_eeschema(pin, tx): - """Create EESCHEMA text of net label attached to a pin.""" - - if pin.stub is False or not pin.is_connected(): - # No label if pin is not connected or is connected to an explicit wire. - return "" - - label_type = "HLabel" - for pn in pin.net.pins: - if pin.part.hierarchy.startswith(pn.part.hierarchy): - continue - if pn.part.hierarchy.startswith(pin.part.hierarchy): - continue - label_type = "GLabel" - break - - part_tx = pin.part.tx * tx - pt = pin.pt * part_tx - - pin_dir = calc_pin_dir(pin) - orientation = { - "R": 0, - "D": 1, - "L": 2, - "U": 3, - }[pin_dir] - - return "Text {} {} {} {} {} UnSpc ~ 0\n{}\n".format( - label_type, - int(round(pt.x)), - int(round(pt.y)), - orientation, - PIN_LABEL_FONT_SIZE, - pin.net.name, - ) - - -def create_eeschema_file( - filename, - contents, - cur_sheet_num=1, - total_sheet_num=1, - title="Default", - rev_major=0, - rev_minor=1, - year=datetime.date.today().year, - month=datetime.date.today().month, - day=datetime.date.today().day, - A_size="A2", -): - """Write EESCHEMA header, contents, and footer to a file.""" - - with open(filename, "w") as f: - f.write( - "\n".join( - ( - "EESchema Schematic File Version 4", - "EELAYER 30 0", - "EELAYER END", - "$Descr {} {} {}".format( - A_size, A_sizes[A_size].max.x, A_sizes[A_size].max.y - ), - "encoding utf-8", - "Sheet {} {}".format(cur_sheet_num, total_sheet_num), - 'Title "{}"'.format(title), - 'Date "{}-{}-{}"'.format(year, month, day), - 'Rev "v{}.{}"'.format(rev_major, rev_minor), - 'Comp ""', - 'Comment1 ""', - 'Comment2 ""', - 'Comment3 ""', - 'Comment4 ""', - "$EndDescr", - "", - contents, - "$EndSCHEMATC", - ) - ) - ) - - -def node_to_eeschema(node, sheet_tx=Tx()): - """Convert node circuitry to an EESCHEMA sheet. - - Args: - sheet_tx (Tx, optional): Scaling/translation matrix for sheet. Defaults to Tx(). - - Returns: - str: EESCHEMA text for the node circuitry. - """ - - from skidl import HIER_SEP - - # List to hold all the EESCHEMA code for this node. - eeschema_code = [] - - if node.flattened: - # Create the transformation matrix for the placement of the parts in the node. - tx = node.tx * sheet_tx - else: - # Unflattened nodes are placed in their own sheet, so compute - # their bounding box as if they *were* flattened and use that to - # find the transformation matrix for an appropriately-sized sheet. - flattened_bbox = node.internal_bbox() - tx = calc_sheet_tx(flattened_bbox) - - # Generate EESCHEMA code for each child of this node. - for child in node.children.values(): - eeschema_code.append(node_to_eeschema(child, tx)) - - # Generate EESCHEMA code for each part in the node. - for part in node.parts: - if isinstance(part, NetTerminal): - eeschema_code.append(net_to_eeschema(part, tx=tx)) - else: - eeschema_code.append(part_to_eeschema(part, tx=tx)) - - # Generate EESCHEMA wiring code between the parts in the node. - for net, wire in node.wires.items(): - wire_code = wire_to_eeschema(net, wire, tx=tx) - eeschema_code.append(wire_code) - for net, junctions in node.junctions.items(): - junction_code = junction_to_eeschema(net, junctions, tx=tx) - eeschema_code.append(junction_code) - - # Generate power connections for the each part in the node. - for part in node.parts: - stub_code = power_part_to_eeschema(part, tx=tx) - if len(stub_code) != 0: - eeschema_code.append(stub_code) - - # Generate pin labels for stubbed nets on each part in the node. - for part in node.parts: - for pin in part: - pin_label_code = pin_label_to_eeschema(pin, tx=tx) - eeschema_code.append(pin_label_code) - - # Join EESCHEMA code into one big string. - eeschema_code = "\n".join(eeschema_code) - - # If this node was flattened, then return the EESCHEMA code and surrounding box - # for inclusion in the parent node. - if node.flattened: - # Generate the graphic box that surrounds the flattened hierarchical block of this node. - block_name = node.name.split(HIER_SEP)[-1] - pad = Vector(BLK_INT_PAD, BLK_INT_PAD) - bbox_code = bbox_to_eeschema(node.bbox.resize(pad), tx, block_name) - - return "\n".join((eeschema_code, bbox_code)) - - # Create a hierarchical sheet file for storing this unflattened node. - A_size = get_A_size(flattened_bbox) - filepath = os.path.join(node.filepath, node.sheet_filename) - create_eeschema_file(filepath, eeschema_code, title=node.title, A_size=A_size) - - # Create the hierarchical sheet for insertion into the calling node sheet. - bbox = (node.bbox * node.tx * sheet_tx).round() - time_hex = hex(int(time.time()))[2:] - return "\n".join( - ( - "$Sheet", - "S {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.w, bbox.h), - "U {}".format(time_hex), - 'F0 "{}" {}'.format(node.name, node.name_sz), - 'F1 "{}" {}'.format(node.sheet_filename, node.filename_sz), - "$EndSheet", - "", - ) - ) - - -""" -Generate a KiCad EESCHEMA schematic from a Circuit object. -""" - -class has_symbol_layout_data(Trait): - tx: Tx - orientation_locked: bool - - -class has_pin_layout_data(Trait): - pt: Point - routed: bool - - -def _add_data_trait[T: Trait](node: Node, trait: type[T]) -> T: - """Helper to shave down boilerplate for adding data-traits to nodes""" - class Impl(trait.impl()): - pass - - return node.add(Impl()) - - -def preprocess_circuit(circuit: Graph, **options): - """Add stuff to parts & nets for doing placement and routing of schematics.""" - - def units(part: Module): - return [part] - # TODO: handle units within parts - # if len(part.unit) == 0: - # return [part] - # else: - # return part.unit.values() - - def initialize(part: Module): - """Initialize part or its part units.""" - - # Initialize the units of the part, or the part itself if it has no units. - pin_limit = options.get("orientation_pin_limit", 44) - for part_unit in units(part): - cmp_data_trait = _add_data_trait(part, has_symbol_layout_data) - - # Initialize transform matrix. - layout_data = part_unit.get_trait(F.has_symbol_layout) or part_unit.add(F.has_symbol_layout_defined()) - cmp_data_trait.tx = Tx.from_symtx(layout_data.translations) - - # Lock part orientation if symtx was specified. Also lock parts with a lot of pins - # since they're typically drawn the way they're supposed to be oriented. - # And also lock single-pin parts because these are usually power/ground and - # they shouldn't be flipped around. - num_pins = len(part_unit.get_children(direct_only=True, types=F.Symbol.Pin)) - cmp_data_trait.orientation_locked = bool(layout_data.translations) or not ( - 1 < num_pins <= pin_limit - ) - - # Initialize pin attributes used for generating schematics. - for pin in part_unit.get_children(direct_only=True, types=F.Symbol.Pin): - pin_data_trait = _add_data_trait(pin, has_pin_layout_data) - lib_pin = SchTransformer.get_lib_pin(pin) - # TODO: what to do with pin rotation? - pin_data_trait.pt = Point(lib_pin.at.x, lib_pin.at.y) - pin_data_trait.routed = False - - def rotate_power_pins(part: Module): - """Rotate a part based on the direction of its power pins. - - This function is to make sure that voltage sources face up and gnd pins - face down. - """ - - # Don't rotate parts that are already explicitly rotated/flipped. - if part.get_trait(F.has_symbol_layout).translations: - return - - def is_pwr(net: F.Electrical): - F.ElectricPower - - def is_gnd(net): - return "gnd" in net_name.lower() - - dont_rotate_pin_cnt = options.get("dont_rotate_pin_count", 10000) - - for part_unit in units(part): - # Don't rotate parts with too many pins. - if len(part_unit) > dont_rotate_pin_cnt: - return - - # Tally what rotation would make each pwr/gnd pin point up or down. - rotation_tally = Counter() - for pin in part_unit: - net_name = getattr(pin.net, "name", "").lower() - if is_gnd(net_name): - if pin.orientation == "U": - rotation_tally[0] += 1 - if pin.orientation == "D": - rotation_tally[180] += 1 - if pin.orientation == "L": - rotation_tally[90] += 1 - if pin.orientation == "R": - rotation_tally[270] += 1 - elif is_pwr(net_name): - if pin.orientation == "D": - rotation_tally[0] += 1 - if pin.orientation == "U": - rotation_tally[180] += 1 - if pin.orientation == "L": - rotation_tally[270] += 1 - if pin.orientation == "R": - rotation_tally[90] += 1 - - # Rotate the part unit in the direction with the most tallies. - try: - rotation = rotation_tally.most_common()[0][0] - except IndexError: - pass - else: - # Rotate part unit 90-degrees clockwise until the desired rotation is reached. - tx_cw_90 = Tx(a=0, b=-1, c=1, d=0) # 90-degree trans. matrix. - for _ in range(int(round(rotation / 90))): - part_unit.tx = part_unit.tx * tx_cw_90 - - def calc_part_bbox(part: Module): - """Calculate the labeled bounding boxes and store it in the part.""" - - # Find part/unit bounding boxes excluding any net labels on pins. - # TODO: part.lbl_bbox could be substituted for part.bbox. - # TODO: Part ref and value should be updated before calculating bounding box. - bare_bboxes = calc_symbol_bbox(part)[1:] - - for part_unit, bare_bbox in zip(units(part), bare_bboxes): - # Expand the bounding box if it's too small in either dimension. - resize_wh = Vector(0, 0) - if bare_bbox.w < 100: - resize_wh.x = (100 - bare_bbox.w) / 2 - if bare_bbox.h < 100: - resize_wh.y = (100 - bare_bbox.h) / 2 - bare_bbox = bare_bbox.resize(resize_wh) - - # Find expanded bounding box that includes any hier labels attached to pins. - part_unit.lbl_bbox = BBox() - part_unit.lbl_bbox.add(bare_bbox) - for pin in part_unit: - if pin.stub: - # Find bounding box for net stub label attached to pin. - hlbl_bbox = calc_hier_label_bbox(pin.net.name, pin.orientation) - # Move the label bbox to the pin location. - hlbl_bbox *= Tx().move(pin.pt) - # Update the bbox for the labelled part with this pin label. - part_unit.lbl_bbox.add(hlbl_bbox) - - # Set the active bounding box to the labeled version. - part_unit.bbox = part_unit.lbl_bbox - - # Pre-process parts - # TODO: complete criteria on what schematic symbols we can handle - for part, has_symbol_trait in circuit.nodes_with_trait(F.has_symbol): - symbol = has_symbol_trait.get_symbol() - # Initialize part attributes used for generating schematics. - initialize(symbol) - - # Rotate parts. Power pins should face up. GND pins should face down. - rotate_power_pins(symbol) - - # Compute bounding boxes around parts - calc_part_bbox(symbol) - - -def finalize_parts_and_nets(circuit, **options): - """Restore parts and nets after place & route is done.""" - - # Remove any NetTerminals that were added. - net_terminals = (p for p in circuit.parts if isinstance(p, NetTerminal)) - circuit.rmv_parts(*net_terminals) - - # Return pins from the part units to their parent part. - for part in circuit.parts: - part.grab_pins() - - # Remove some stuff added to parts during schematic generation process. - rmv_attr(circuit.parts, ("force", "bbox", "lbl_bbox", "tx")) - - -def gen_schematic( - circuit, - filepath=".", - top_name=get_script_name(), - title="SKiDL-Generated Schematic", - flatness=0.0, - retries=2, - **options, -): - """Create a schematic file from a Circuit object. - - Args: - circuit (Circuit): The Circuit object that will have a schematic generated for it. - filepath (str, optional): The directory where the schematic files are placed. Defaults to ".". - top_name (str, optional): The name for the top of the circuit hierarchy. Defaults to get_script_name(). - title (str, optional): The title of the schematic. Defaults to "SKiDL-Generated Schematic". - flatness (float, optional): Determines how much the hierarchy is flattened in the schematic. Defaults to 0.0 (completely hierarchical). - retries (int, optional): Number of times to re-try if routing fails. Defaults to 2. - options (dict, optional): Dict of options and values, usually for drawing/debugging. - """ - - from skidl import KICAD - from skidl.schematics.node import Node - from skidl.tools import tool_modules - - from .place import PlacementFailure - from .route import RoutingFailure - - # Part placement options that should always be turned on. - options["use_push_pull"] = True - options["rotate_parts"] = True - options["pt_to_pt_mult"] = 5 # HACK: Ad-hoc value. - options["pin_normalize"] = True - - # Start with default routing area. - expansion_factor = 1.0 - - # Try to place & route one or more times. - for _ in range(retries): - preprocess_circuit(circuit, **options) - - node = Node(circuit, tool_modules[KICAD], filepath, top_name, title, flatness) - - try: - # Place parts. - node.place(expansion_factor=expansion_factor, **options) - - # Route parts. - node.route(**options) - - except PlacementFailure: - # Placement failed, so clean up ... - finalize_parts_and_nets(circuit, **options) - # ... and try again. - continue - - except RoutingFailure: - # Routing failed, so clean up ... - finalize_parts_and_nets(circuit, **options) - # ... and expand routing area ... - expansion_factor *= 1.5 # HACK: Ad-hoc increase of expansion factor. - # ... and try again. - continue - - # Generate EESCHEMA code for the schematic. - node_to_eeschema(node) - - # Append place & route statistics for the schematic to a file. - if options.get("collect_stats"): - stats = node.collect_stats(**options) - with open(options["stats_file"], "a") as f: - f.write(stats) - - # Clean up. - finalize_parts_and_nets(circuit, **options) - - # Place & route was successful if we got here, so exit. - return - - # Append failed place & route statistics for the schematic to a file. - if options.get("collect_stats"): - stats = "-1\n" - with open(options["stats_file"], "a") as f: - f.write(stats) - - # Clean-up after failure. - finalize_parts_and_nets(circuit, **options) - - # Exited the loop without successful routing. - raise (RoutingFailure) From a327ace0e15b027d71858a3368a23f5481f6f30e Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 15:43:36 +0200 Subject: [PATCH 09/85] Re-add fresh sch layout lib --- .../schematic/kicad/skidl/__init__.py | 0 .../exporters/schematic/kicad/skidl/bboxes.py | 328 ++ .../schematic/kicad/skidl/constants.py | 16 + .../schematic/kicad/skidl/debug_draw.py | 352 ++ .../schematic/kicad/skidl/draw_objs.py | 41 + .../schematic/kicad/skidl/gen_schematic.py | 757 ++++ .../schematic/kicad/skidl/geometry.py | 485 +++ .../schematic/kicad/skidl/net_terminal.py | 65 + .../exporters/schematic/kicad/skidl/node.py | 354 ++ .../exporters/schematic/kicad/skidl/place.py | 1534 ++++++++ .../exporters/schematic/kicad/skidl/route.py | 3238 +++++++++++++++++ 11 files changed, 7170 insertions(+) create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/__init__.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/bboxes.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/constants.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/geometry.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/node.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/place.py create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/route.py diff --git a/src/faebryk/exporters/schematic/kicad/skidl/__init__.py b/src/faebryk/exporters/schematic/kicad/skidl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py new file mode 100644 index 00000000..0c36d79b --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Calculate bounding boxes for part symbols and hierarchical sheets. +""" + +from collections import namedtuple + +from skidl.logger import active_logger +from skidl.schematics.geometry import ( + Tx, + BBox, + Point, + Vector, + tx_rot_0, + tx_rot_90, + tx_rot_180, + tx_rot_270, +) +from skidl.utilities import export_to_all +from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE +from skidl.schematics.geometry import BBox, Point, Tx, Vector +from .draw_objs import * + + +@export_to_all +def calc_symbol_bbox(part, **options): + """ + Return the bounding box of the part symbol. + + Args: + part: Part object for which an SVG symbol will be created. + options (dict): Various options to control bounding box calculation: + graphics_only (boolean): If true, compute bbox of graphics (no text). + + Returns: List of BBoxes for all units in the part symbol. + + Note: V5 library format: https://www.compuphase.com/electronics/LibraryFileFormats.pdf + """ + + # Named tuples for part KiCad V5 DRAW primitives. + + def make_pin_dir_tbl(abs_xoff=20): + + # abs_xoff is the absolute distance of name/num from the end of the pin. + rel_yoff_num = -0.15 # Relative distance of number above pin line. + rel_yoff_name = ( + 0.2 # Relative distance that places name midline even with pin line. + ) + + # Tuple for storing information about pins in each of four directions: + # direction: The direction the pin line is drawn from start to end. + # side: The side of the symbol the pin is on. (Opposite of the direction.) + # angle: The angle of the name/number text for the pin (usually 0, -90.). + # num_justify: Text justification of the pin number. + # name_justify: Text justification of the pin name. + # num_offset: (x,y) offset of the pin number w.r.t. the end of the pin. + # name_offset: (x,y) offset of the pin name w.r.t. the end of the pin. + PinDir = namedtuple( + "PinDir", + "direction side angle num_justify name_justify num_offset name_offset net_offset", + ) + + return { + "U": PinDir( + Point(0, 1), + "bottom", + -90, + "end", + "start", + Point(-abs_xoff, rel_yoff_num), + Point(abs_xoff, rel_yoff_name), + Point(abs_xoff, rel_yoff_num), + ), + "D": PinDir( + Point(0, -1), + "top", + -90, + "start", + "end", + Point(abs_xoff, rel_yoff_num), + Point(-abs_xoff, rel_yoff_name), + Point(-abs_xoff, rel_yoff_num), + ), + "L": PinDir( + Point(-1, 0), + "right", + 0, + "start", + "end", + Point(abs_xoff, rel_yoff_num), + Point(-abs_xoff, rel_yoff_name), + Point(-abs_xoff, rel_yoff_num), + ), + "R": PinDir( + Point(1, 0), + "left", + 0, + "end", + "start", + Point(-abs_xoff, rel_yoff_num), + Point(abs_xoff, rel_yoff_name), + Point(abs_xoff, rel_yoff_num), + ), + } + + default_pin_name_offset = 20 + + # Go through each graphic object that makes up the component symbol. + for obj in part.draw: + + obj_bbox = BBox() # Bounding box of all the component objects. + thickness = 0 + + if isinstance(obj, DrawDef): + def_ = obj + # Make pin direction table with symbol-specific name offset. + pin_dir_tbl = make_pin_dir_tbl(def_.name_offset or default_pin_name_offset) + # Make structures for holding info on each part unit. + num_units = def_.num_units + unit_bboxes = [BBox() for _ in range(num_units + 1)] + + elif isinstance(obj, DrawF0) and not options.get("graphics_only", False): + # obj attributes: x y size orientation visibility halign valign + # Skip if the object is invisible. + if obj.visibility.upper() == "I": + continue + + # Calculate length and height of part reference. + # Use ref from the SKiDL part since the ref in the KiCAD part + # hasn't been updated from its generic value. + length = len(part.ref) * obj.size + height = obj.size + + # Create bbox with lower-left point at (0, 0). + bbox = BBox(Point(0, 0), Point(length, height)) + + # Rotate bbox around origin. + rot_tx = {"H": Tx(), "V": tx_rot_90}[obj.orientation.upper()] + bbox *= rot_tx + + # Horizontally align bbox. + halign = obj.halign.upper() + if halign == "L": + pass + elif halign == "R": + bbox *= Tx().move(Point(-bbox.w, 0)) + elif halign == "C": + bbox *= Tx().move(Point(-bbox.w / 2, 0)) + else: + raise Exception("Inconsistent horizontal alignment: {}".format(halign)) + + # Vertically align bbox. + valign = obj.valign[:1].upper() # valign is first letter. + if valign == "B": + pass + elif valign == "T": + bbox *= Tx().move(Point(0, -bbox.h)) + elif valign == "C": + bbox *= Tx().move(Point(0, -bbox.h / 2)) + else: + raise Exception("Inconsistent vertical alignment: {}".format(valign)) + + bbox *= Tx().move(Point(obj.x, obj.y)) + obj_bbox.add(bbox) + + elif isinstance(obj, DrawF1) and not options.get("graphics_only", False): + # Skip if the object is invisible. + if obj.visibility.upper() == "I": + continue + + # Calculate length and height of part value. + # Use value from the SKiDL part since the value in the KiCAD part + # hasn't been updated from its generic value. + length = len(str(part.value)) * obj.size + height = obj.size + + # Create bbox with lower-left point at (0, 0). + bbox = BBox(Point(0, 0), Point(length, height)) + + # Rotate bbox around origin. + rot_tx = {"H": Tx(), "V": tx_rot_90}[obj.orientation.upper()] + bbox *= rot_tx + + # Horizontally align bbox. + halign = obj.halign.upper() + if halign == "L": + pass + elif halign == "R": + bbox *= Tx().move(Point(-bbox.w, 0)) + elif halign == "C": + bbox *= Tx().move(Point(-bbox.w / 2, 0)) + else: + raise Exception("Inconsistent horizontal alignment: {}".format(halign)) + + # Vertically align bbox. + valign = obj.valign[:1].upper() # valign is first letter. + if valign == "B": + pass + elif valign == "T": + bbox *= Tx().move(Point(0, -bbox.h)) + elif valign == "C": + bbox *= Tx().move(Point(0, -bbox.h / 2)) + else: + raise Exception("Inconsistent vertical alignment: {}".format(valign)) + + bbox *= Tx().move(Point(obj.x, obj.y)) + obj_bbox.add(bbox) + + elif isinstance(obj, DrawArc): + arc = obj + center = Point(arc.cx, arc.cy) + thickness = arc.thickness + radius = arc.radius + start = Point(arc.startx, arc.starty) + end = Point(arc.endx, arc.endy) + start_angle = arc.start_angle / 10 + end_angle = arc.end_angle / 10 + clock_wise = int(end_angle < start_angle) + large_arc = int(abs(end_angle - start_angle) > 180) + radius_pt = Point(radius, radius) + obj_bbox.add(center - radius_pt) + obj_bbox.add(center + radius_pt) + + elif isinstance(obj, DrawCircle): + circle = obj + center = Point(circle.cx, circle.cy) + thickness = circle.thickness + radius = circle.radius + radius_pt = Point(radius, radius) + obj_bbox.add(center - radius_pt) + obj_bbox.add(center + radius_pt) + + elif isinstance(obj, DrawPoly): + poly = obj + thickness = obj.thickness + pts = [Point(x, y) for x, y in zip(poly.points[0::2], poly.points[1::2])] + path = [] + for pt in pts: + obj_bbox.add(pt) + + elif isinstance(obj, DrawRect): + rect = obj + thickness = obj.thickness + start = Point(rect.x1, rect.y1) + end = Point(rect.x2, rect.y2) + obj_bbox.add(start) + obj_bbox.add(end) + + elif isinstance(obj, DrawText) and not options.get("graphics_only", False): + pass + + elif isinstance(obj, DrawPin): + pin = obj + + try: + visible = pin.shape[0] != "N" + except IndexError: + visible = True # No pin shape given, so it is visible by default. + + if visible: + # Draw pin if it's not invisible. + + # Create line for pin lead. + dir = pin_dir_tbl[pin.orientation].direction + start = Point(pin.x, pin.y) + l = dir * pin.length + end = start + l + obj_bbox.add(start) + obj_bbox.add(end) + + else: + active_logger.error( + "Unknown graphical object {} in part symbol {}.".format( + type(obj), part.name + ) + ) + + # REMOVE: Maybe we shouldn't do this? + # Expand bounding box to account for object line thickness. + # obj_bbox.resize(Vector(round(thickness / 2), round(thickness / 2))) + + # Enter the current object into the SVG for this part. + unit = getattr(obj, "unit", 0) + if unit == 0: + for bbox in unit_bboxes: + bbox.add(obj_bbox) + else: + unit_bboxes[unit].add(obj_bbox) + + # End of loop through all the component objects. + + return unit_bboxes + + +@export_to_all +def calc_hier_label_bbox(label, dir): + """Calculate the bounding box for a hierarchical label. + + Args: + label (str): String for the label. + dir (str): Orientation ("U", "D", "L", "R"). + + Returns: + BBox: Bounding box for the label and hierarchical terminal. + """ + + # Rotation matrices for each direction. + lbl_tx = { + "U": tx_rot_90, # Pin on bottom pointing upwards. + "D": tx_rot_270, # Pin on top pointing down. + "L": tx_rot_180, # Pin on right pointing left. + "R": tx_rot_0, # Pin on left pointing right. + } + + # Calculate length and height of label + hierarchical marker. + lbl_len = len(label) * PIN_LABEL_FONT_SIZE + HIER_TERM_SIZE + lbl_hgt = max(PIN_LABEL_FONT_SIZE, HIER_TERM_SIZE) + + # Create bbox for label on left followed by marker on right. + bbox = BBox(Point(0, lbl_hgt / 2), Point(-lbl_len, -lbl_hgt / 2)) + + # Rotate the bbox in the given direction. + bbox *= lbl_tx[dir] + + return bbox diff --git a/src/faebryk/exporters/schematic/kicad/skidl/constants.py b/src/faebryk/exporters/schematic/kicad/skidl/constants.py new file mode 100644 index 00000000..a7de8044 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/constants.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Constants used when generating schematics. +""" + +# Constants for KiCad. +GRID = 50 +PIN_LABEL_FONT_SIZE = 50 +BOX_LABEL_FONT_SIZE = 50 +BLK_INT_PAD = 2 * GRID +BLK_EXT_PAD = 2 * GRID +DRAWING_BOX_RESIZE = 100 +HIER_TERM_SIZE = 50 diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py new file mode 100644 index 00000000..e84274a1 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Drawing routines used for debugging place & route. +""" + +from collections import defaultdict +from random import randint + +from skidl.utilities import export_to_all +from .geometry import BBox, Point, Segment, Tx, Vector + + +# Dictionary for storing colors to visually distinguish routed nets. +net_colors = defaultdict(lambda: (randint(0, 200), randint(0, 200), randint(0, 200))) + + +@export_to_all +def draw_box(bbox, scr, tx, color=(192, 255, 192), thickness=0): + """Draw a box in the drawing area. + + Args: + bbox (BBox): Bounding box for the box. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + color (tuple, optional): Box color. Defaults to (192, 255, 192). + + Returns: + None. + """ + + bbox = bbox * tx + corners = ( + (bbox.min.x, bbox.min.y), + (bbox.min.x, bbox.max.y), + (bbox.max.x, bbox.max.y), + (bbox.max.x, bbox.min.y), + ) + pygame.draw.polygon(scr, color, corners, thickness) + + +@export_to_all +def draw_endpoint(pt, scr, tx, color=(100, 100, 100), dot_radius=10): + """Draw a line segment endpoint in the drawing area. + + Args: + pt (Point): A point with (x,y) coords. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + color (tuple, optional): Segment color. Defaults to (192, 255, 192). + dot_Radius (int, optional): Endpoint dot radius. Defaults to 3. + """ + + pt = pt * tx # Convert to drawing coords. + + # Draw diamond for terminal. + sz = dot_radius / 2 * tx.a # Scale for drawing coords. + corners = ( + (pt.x, pt.y + sz), + (pt.x + sz, pt.y), + (pt.x, pt.y - sz), + (pt.x - sz, pt.y), + ) + pygame.draw.polygon(scr, color, corners, 0) + + # Draw dot for terminal. + radius = dot_radius * tx.a + pygame.draw.circle(scr, color, (pt.x, pt.y), radius) + + +@export_to_all +def draw_seg(seg, scr, tx, color=(100, 100, 100), thickness=5, dot_radius=10): + """Draw a line segment in the drawing area. + + Args: + seg (Segment, Interval, NetInterval): An object with two endpoints. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + color (tuple, optional): Segment color. Defaults to (192, 255, 192). + seg_thickness (int, optional): Segment line thickness. Defaults to 5. + dot_Radius (int, optional): Endpoint dot radius. Defaults to 3. + """ + + # Use net color if object has a net. Otherwise use input color. + try: + color = net_colors[seg.net] + except AttributeError: + pass + + # draw endpoints. + draw_endpoint(seg.p1, scr, tx, color=color, dot_radius=dot_radius) + draw_endpoint(seg.p2, scr, tx, color=color, dot_radius=dot_radius) + + # Transform segment coords to screen coords. + seg = seg * tx + + # Draw segment. + pygame.draw.line( + scr, color, (seg.p1.x, seg.p1.y), (seg.p2.x, seg.p2.y), width=thickness + ) + + +@export_to_all +def draw_text(txt, pt, scr, tx, font, color=(100, 100, 100), real=True): + """Render text in drawing area. + + Args: + txt (str): Text string to be rendered. + pt (Point): Real or screen coord for start of rendered text. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (tuple, optional): Segment color. Defaults to (100,100,100). + real (Boolean): If true, transform real pt to screen coords. Otherwise, pt is screen coords. + """ + + # Transform real text starting point to screen coords. + if real: + pt = pt * tx + + # Render text. + font.render_to(scr, (pt.x, pt.y), txt, color) + + +@export_to_all +def draw_part(part, scr, tx, font): + """Draw part bounding box. + + Args: + part (Part): Part to draw. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + """ + tx_bbox = ( + getattr(part, "lbl_bbox", getattr(part, "place_bbox", Vector(0, 0))) * part.tx + ) + draw_box(tx_bbox, scr, tx, color=(180, 255, 180), thickness=0) + draw_box(tx_bbox, scr, tx, color=(90, 128, 90), thickness=5) + draw_text(part.ref, tx_bbox.ctr, scr, tx, font) + try: + for pin in part: + if hasattr(pin, "place_pt"): + pt = pin.place_pt * part.tx + draw_endpoint(pt, scr, tx, color=(200, 0, 200), dot_radius=10) + except TypeError: + # Probably trying to draw a block of parts which has no pins and can't iterate thru them. + pass + + +@export_to_all +def draw_net(net, parts, scr, tx, font, color=(0, 0, 0), thickness=2, dot_radius=5): + """Draw net and connected terminals. + + Args: + net (Net): Net of conmnected terminals. + parts (list): List of parts to which net will be drawn. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (tuple, optional): Segment color. Defaults to (0,0,0). + thickness (int, optional): Thickness of net line. Defaults to 2. + dot_radius (int, optional): Radius of terminals on net. Defaults to 5. + """ + pts = [] + for pin in net.pins: + part = pin.part + if part in parts: + pt = pin.route_pt * part.tx + pts.append(pt) + for pt1, pt2 in zip(pts[:-1], pts[1:]): + draw_seg( + Segment(pt1, pt2), + scr, + tx, + color=color, + thickness=thickness, + dot_radius=dot_radius, + ) + + +@export_to_all +def draw_force(part, force, scr, tx, font, color=(128, 0, 0)): + """Draw force vector affecting a part. + + Args: + part (Part): The part affected by the force. + force (Vector): The force vector. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (tuple, optional): Segment color. Defaults to (0,0,0). + """ + force *= 1 + anchor = part.place_bbox.ctr * part.tx + draw_seg( + Segment(anchor, anchor + force), scr, tx, color=color, thickness=5, dot_radius=5 + ) + + +@export_to_all +def draw_placement(parts, nets, scr, tx, font): + """Draw placement of parts and interconnecting nets. + + Args: + parts (list): List of Part objects. + nets (list): List of Net objects. + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + """ + draw_clear(scr) + for part in parts: + draw_part(part, scr, tx, font) + draw_force(part, getattr(part, "force", Vector(0, 0)), scr, tx, font) + for net in nets: + draw_net(net, parts, scr, tx, font) + draw_redraw() + + +@export_to_all +def draw_routing(node, bbox, parts, *other_stuff, **options): + """Draw routing for debugging purposes. + + Args: + node (Node): Hierarchical node. + bbox (BBox): Bounding box of drawing area. + node (Node): The Node being routed. + parts (list): List of Parts. + other_stuff (list): Other stuff with a draw() method. + options (dict, optional): Dictionary of options and values. Defaults to {}. + """ + + # Initialize drawing area. + draw_scr, draw_tx, draw_font = draw_start(bbox) + + # Draw parts. + for part in parts: + draw_part(part, draw_scr, draw_tx, draw_font) + + # Draw wiring. + for wires in node.wires.values(): + for wire in wires: + draw_seg(wire, draw_scr, draw_tx, (255, 0, 255), 3, dot_radius=10) + + # Draw other stuff (global routes, switchbox routes, etc.) that has a draw() method. + for stuff in other_stuff: + for obj in stuff: + obj.draw(draw_scr, draw_tx, draw_font, **options) + + draw_end() + + +@export_to_all +def draw_clear(scr, color=(255, 255, 255)): + """Clear drawing area. + + Args: + scr (PyGame screen): Screen object to be cleared. + color (tuple, optional): Background color. Defaults to (255, 255, 255). + """ + scr.fill(color) + + +@export_to_all +def draw_start(bbox): + """ + Initialize PyGame drawing area. + + Args: + bbox: Bounding box of object to be drawn. + + Returns: + scr: PyGame screen that is drawn upon. + tx: Matrix to transform from real coords to screen coords. + font: PyGame font for rendering text. + """ + + # Only import pygame if drawing is being done to avoid the startup message. + import pygame + import pygame.freetype + + # Make pygame module available to other functions. + globals()["pygame"] = pygame + + # Screen drawing area. + scr_bbox = BBox(Point(0, 0), Point(2000, 1500)) + + # Place a blank region around the object by expanding it's bounding box. + border = max(bbox.w, bbox.h) / 20 + bbox = bbox.resize(Vector(border, border)) + bbox = bbox.round() + + # Compute the scaling from real to screen coords. + scale = min(scr_bbox.w / bbox.w, scr_bbox.h / bbox.h) + scale_tx = Tx(a=scale, d=scale) + + # Flip the Y coord. + flip_tx = Tx(d=-1) + + # Compute the translation of the object center to the drawing area center + new_bbox = bbox * scale_tx * flip_tx # Object bbox transformed to screen coords. + move = scr_bbox.ctr - new_bbox.ctr # Vector to move object ctr to drawing ctr. + move_tx = Tx(dx=move.x, dy=move.y) + + # The final transformation matrix will scale the object's real coords, + # flip the Y coord, and then move the object to the center of the drawing area. + tx = scale_tx * flip_tx * move_tx + + # Initialize drawing area. + pygame.init() + scr = pygame.display.set_mode((scr_bbox.w, scr_bbox.h)) + + # Set font for text rendering. + font = pygame.freetype.SysFont("consolas", 24) + + # Clear drawing area. + draw_clear(scr) + + # Return drawing screen, transformation matrix, and font. + return scr, tx, font + + +@export_to_all +def draw_redraw(): + """Redraw the PyGame display.""" + pygame.display.flip() + + +@export_to_all +def draw_pause(): + """Pause drawing and then resume after button press.""" + + # Display drawing. + draw_redraw() + + # Wait for user to press a key or close PyGame window. + running = True + while running: + for event in pygame.event.get(): + if event.type in (pygame.KEYDOWN, pygame.QUIT): + running = False + + +@export_to_all +def draw_end(): + """Display drawing and wait for user to close PyGame window.""" + + draw_pause() + pygame.quit() diff --git a/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py b/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py new file mode 100644 index 00000000..f4703981 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +KiCad 5 drawing objects. +""" + +from collections import namedtuple + + +DrawDef = namedtuple( + "DrawDef", + "name ref zero name_offset show_nums show_names num_units lock_units power_symbol", +) + +DrawF0 = namedtuple("DrawF0", "ref x y size orientation visibility halign valign") + +DrawF1 = namedtuple( + "DrawF1", "name x y size orientation visibility halign valign fieldname" +) + +DrawArc = namedtuple( + "DrawArc", + "cx cy radius start_angle end_angle unit dmg thickness fill startx starty endx endy", +) + +DrawCircle = namedtuple("DrawCircle", "cx cy radius unit dmg thickness fill") + +DrawPoly = namedtuple("DrawPoly", "point_count unit dmg thickness points fill") + +DrawRect = namedtuple("DrawRect", "x1 y1 x2 y2 unit dmg thickness fill") + +DrawText = namedtuple( + "DrawText", "angle x y size hidden unit dmg text italic bold halign valign" +) + +DrawPin = namedtuple( + "DrawPin", + "name num x y length orientation num_size name_size unit dmg electrical_type shape", +) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py new file mode 100644 index 00000000..8f3b36c7 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -0,0 +1,757 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + + +import datetime +import os.path +import re +import time +from collections import Counter, OrderedDict + +from skidl.scriptinfo import get_script_name +from skidl.schematics.geometry import BBox, Point, Tx, Vector +from skidl.schematics.net_terminal import NetTerminal +from skidl.utilities import export_to_all +from .constants import BLK_INT_PAD, BOX_LABEL_FONT_SIZE, GRID, PIN_LABEL_FONT_SIZE +from .bboxes import calc_symbol_bbox, calc_hier_label_bbox +from skidl.utilities import rmv_attr + + +__all__ = [] + +""" +Functions for generating a KiCad EESCHEMA schematic. +""" + + +def bbox_to_eeschema(bbox, tx, name=None): + """Create a bounding box using EESCHEMA graphic lines.""" + + # Make sure the box corners are integers. + bbox = (bbox * tx).round() + + graphic_box = [] + + if name: + # Place name at the lower-left corner of the box. + name_pt = bbox.ul + graphic_box.append( + "Text Notes {} {} 0 {} ~ 20\n{}".format( + name_pt.x, name_pt.y, BOX_LABEL_FONT_SIZE, name + ) + ) + + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.lr.x, bbox.lr.y) + ) + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.lr.x, bbox.lr.y, bbox.ur.x, bbox.ur.y) + ) + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.ur.x, bbox.ur.y, bbox.ul.x, bbox.ul.y) + ) + graphic_box.append("Wire Notes Line") + graphic_box.append( + " {} {} {} {}".format(bbox.ul.x, bbox.ul.y, bbox.ll.x, bbox.ll.y) + ) + graphic_box.append("") # For blank line at end. + + return "\n".join(graphic_box) + + +def net_to_eeschema(self, tx): + """Generate the EESCHEMA code for the net terminal. + + Args: + tx (Tx): Transformation matrix for the node containing this net terminal. + + Returns: + str: EESCHEMA code string. + """ + self.pins[0].stub = True + self.pins[0].orientation = "R" + return pin_label_to_eeschema(self.pins[0], tx) + # return pin_label_to_eeschema(self.pins[0], tx) + bbox_to_eeschema(self.bbox, self.tx * tx) + + +def part_to_eeschema(part, tx): + """Create EESCHEMA code for a part. + + Args: + part (Part): SKiDL part. + tx (Tx): Transformation matrix. + + Returns: + string: EESCHEMA code for the part. + + Notes: + https://en.wikibooks.org/wiki/Kicad/file_formats#Schematic_Files_Format + """ + + tx = part.tx * tx + origin = tx.origin.round() + time_hex = hex(int(time.time()))[2:] + unit_num = getattr(part, "num", 1) + + eeschema = [] + eeschema.append("$Comp") + lib = os.path.splitext(part.lib.filename)[0] + eeschema.append("L {}:{} {}".format(lib, part.name, part.ref)) + eeschema.append("U {} 1 {}".format(unit_num, time_hex)) + eeschema.append("P {} {}".format(str(origin.x), str(origin.y))) + + # Add part symbols. For now we are only adding the designator + n_F0 = 1 + for i in range(len(part.draw)): + if re.search("^DrawF0", str(part.draw[i])): + n_F0 = i + break + eeschema.append( + 'F 0 "{}" {} {} {} {} {} {} {}'.format( + part.ref, + part.draw[n_F0].orientation, + str(origin.x + part.draw[n_F0].x), + str(origin.y + part.draw[n_F0].y), + part.draw[n_F0].size, + "000", # TODO: Refine this to match part def. + part.draw[n_F0].halign, + part.draw[n_F0].valign, + ) + ) + + # Part value. + n_F1 = 1 + for i in range(len(part.draw)): + if re.search("^DrawF1", str(part.draw[i])): + n_F1 = i + break + eeschema.append( + 'F 1 "{}" {} {} {} {} {} {} {}'.format( + str(part.value), + part.draw[n_F1].orientation, + str(origin.x + part.draw[n_F1].x), + str(origin.y + part.draw[n_F1].y), + part.draw[n_F1].size, + "000", # TODO: Refine this to match part def. + part.draw[n_F1].halign, + part.draw[n_F1].valign, + ) + ) + + # Part footprint. + n_F2 = 2 + for i in range(len(part.draw)): + if re.search("^DrawF2", str(part.draw[i])): + n_F2 = i + break + eeschema.append( + 'F 2 "{}" {} {} {} {} {} {} {}'.format( + part.footprint, + part.draw[n_F2].orientation, + str(origin.x + part.draw[n_F2].x), + str(origin.y + part.draw[n_F2].y), + part.draw[n_F2].size, + "001", # TODO: Refine this to match part def. + part.draw[n_F2].halign, + part.draw[n_F2].valign, + ) + ) + eeschema.append(" 1 {} {}".format(str(origin.x), str(origin.y))) + eeschema.append(" {} {} {} {}".format(tx.a, tx.b, tx.c, tx.d)) + eeschema.append("$EndComp") + eeschema.append("") # For blank line at end. + + # For debugging: draws a bounding box around a part. + # eeschema.append(bbox_to_eeschema(part.bbox, tx)) + # eeschema.append(bbox_to_eeschema(part.place_bbox, tx)) + + return "\n".join(eeschema) + + +def wire_to_eeschema(net, wire, tx): + """Create EESCHEMA code for a multi-segment wire. + + Args: + net (Net): Net associated with the wire. + wire (list): List of Segments for a wire. + tx (Tx): transformation matrix for each point in the wire. + + Returns: + string: Text to be placed into EESCHEMA file. + """ + + eeschema = [] + for segment in wire: + eeschema.append("Wire Wire Line") + w = (segment * tx).round() + eeschema.append(" {} {} {} {}".format(w.p1.x, w.p1.y, w.p2.x, w.p2.y)) + eeschema.append("") # For blank line at end. + return "\n".join(eeschema) + + +def junction_to_eeschema(net, junctions, tx): + eeschema = [] + for junction in junctions: + pt = (junction * tx).round() + eeschema.append("Connection ~ {} {}".format(pt.x, pt.y)) + eeschema.append("") # For blank line at end. + return "\n".join(eeschema) + + +def power_part_to_eeschema(part, tx=Tx()): + return "" # REMOVE: Remove this. + out = [] + for pin in part.pins: + try: + if not (pin.net is None): + if pin.net.netclass == "Power": + # strip out the '_...' section from power nets + t = pin.net.name + u = t.split("_") + symbol_name = u[0] + # find the stub in the part + time_hex = hex(int(time.time()))[2:] + pin_pt = (part.origin + offset + Point(pin.x, pin.y)).round() + x, y = pin_pt.x, pin_pt.y + out.append("$Comp\n") + out.append("L power:{} #PWR?\n".format(symbol_name)) + out.append("U 1 1 {}\n".format(time_hex)) + out.append("P {} {}\n".format(str(x), str(y))) + # Add part symbols. For now we are only adding the designator + n_F0 = 1 + for i in range(len(part.draw)): + if re.search("^DrawF0", str(part.draw[i])): + n_F0 = i + break + part_orientation = part.draw[n_F0].orientation + part_horizontal_align = part.draw[n_F0].halign + part_vertical_align = part.draw[n_F0].valign + + # check if the pin orientation will clash with the power part + if "+" in symbol_name: + # voltage sources face up, so check if the pin is facing down (opposite logic y-axis) + if pin.orientation == "U": + orientation = [-1, 0, 0, 1] + elif "gnd" in symbol_name.lower(): + # gnd points down so check if the pin is facing up (opposite logic y-axis) + if pin.orientation == "D": + orientation = [-1, 0, 0, 1] + out.append( + 'F 0 "{}" {} {} {} {} {} {} {}\n'.format( + "#PWR?", + part_orientation, + str(x + 25), + str(y + 25), + str(40), + "001", + part_horizontal_align, + part_vertical_align, + ) + ) + out.append( + 'F 1 "{}" {} {} {} {} {} {} {}\n'.format( + symbol_name, + part_orientation, + str(x + 25), + str(y + 25), + str(40), + "000", + part_horizontal_align, + part_vertical_align, + ) + ) + out.append(" 1 {} {}\n".format(str(x), str(y))) + out.append( + " {} {} {} {}\n".format( + orientation[0], + orientation[1], + orientation[2], + orientation[3], + ) + ) + out.append("$EndComp\n") + except Exception as inst: + print(type(inst)) + print(inst.args) + print(inst) + return "\n" + "".join(out) + + +# Sizes of EESCHEMA schematic pages from smallest to largest. Dimensions in mils. +A_sizes_list = [ + ("A4", BBox(Point(0, 0), Point(11693, 8268))), + ("A3", BBox(Point(0, 0), Point(16535, 11693))), + ("A2", BBox(Point(0, 0), Point(23386, 16535))), + ("A1", BBox(Point(0, 0), Point(33110, 23386))), + ("A0", BBox(Point(0, 0), Point(46811, 33110))), +] + +# Create bounding box for each A size sheet. +A_sizes = OrderedDict(A_sizes_list) + + +def get_A_size(bbox): + """Return the A-size page needed to fit the given bounding box.""" + + width = bbox.w + height = bbox.h * 1.25 # HACK: why 1.25? + for A_size, page in A_sizes.items(): + if width < page.w and height < page.h: + return A_size + return "A0" # Nothing fits, so use the largest available. + + +def calc_sheet_tx(bbox): + """Compute the page size and positioning for this sheet.""" + A_size = get_A_size(bbox) + page_bbox = bbox * Tx(d=-1) + move_to_ctr = A_sizes[A_size].ctr.snap(GRID) - page_bbox.ctr.snap(GRID) + move_tx = Tx(d=-1).move(move_to_ctr) + return move_tx + + +def calc_pin_dir(pin): + """Calculate pin direction accounting for part transformation matrix.""" + + # Copy the part trans. matrix, but remove the translation vector, leaving only scaling/rotation stuff. + tx = pin.part.tx + tx = Tx(a=tx.a, b=tx.b, c=tx.c, d=tx.d) + + # Use the pin orientation to compute the pin direction vector. + pin_vector = { + "U": Point(0, 1), + "D": Point(0, -1), + "L": Point(-1, 0), + "R": Point(1, 0), + }[pin.orientation] + + # Rotate the direction vector using the part rotation matrix. + pin_vector = pin_vector * tx + + # Create an integer tuple from the rotated direction vector. + pin_vector = (int(round(pin_vector.x)), int(round(pin_vector.y))) + + # Return the pin orientation based on its rotated direction vector. + return { + (0, 1): "U", + (0, -1): "D", + (-1, 0): "L", + (1, 0): "R", + }[pin_vector] + + +@export_to_all +def pin_label_to_eeschema(pin, tx): + """Create EESCHEMA text of net label attached to a pin.""" + + if pin.stub is False or not pin.is_connected(): + # No label if pin is not connected or is connected to an explicit wire. + return "" + + label_type = "HLabel" + for pn in pin.net.pins: + if pin.part.hierarchy.startswith(pn.part.hierarchy): + continue + if pn.part.hierarchy.startswith(pin.part.hierarchy): + continue + label_type = "GLabel" + break + + part_tx = pin.part.tx * tx + pt = pin.pt * part_tx + + pin_dir = calc_pin_dir(pin) + orientation = { + "R": 0, + "D": 1, + "L": 2, + "U": 3, + }[pin_dir] + + return "Text {} {} {} {} {} UnSpc ~ 0\n{}\n".format( + label_type, + int(round(pt.x)), + int(round(pt.y)), + orientation, + PIN_LABEL_FONT_SIZE, + pin.net.name, + ) + + +def create_eeschema_file( + filename, + contents, + cur_sheet_num=1, + total_sheet_num=1, + title="Default", + rev_major=0, + rev_minor=1, + year=datetime.date.today().year, + month=datetime.date.today().month, + day=datetime.date.today().day, + A_size="A2", +): + """Write EESCHEMA header, contents, and footer to a file.""" + + with open(filename, "w") as f: + f.write( + "\n".join( + ( + "EESchema Schematic File Version 4", + "EELAYER 30 0", + "EELAYER END", + "$Descr {} {} {}".format( + A_size, A_sizes[A_size].max.x, A_sizes[A_size].max.y + ), + "encoding utf-8", + "Sheet {} {}".format(cur_sheet_num, total_sheet_num), + 'Title "{}"'.format(title), + 'Date "{}-{}-{}"'.format(year, month, day), + 'Rev "v{}.{}"'.format(rev_major, rev_minor), + 'Comp ""', + 'Comment1 ""', + 'Comment2 ""', + 'Comment3 ""', + 'Comment4 ""', + "$EndDescr", + "", + contents, + "$EndSCHEMATC", + ) + ) + ) + + +@export_to_all +def node_to_eeschema(node, sheet_tx=Tx()): + """Convert node circuitry to an EESCHEMA sheet. + + Args: + sheet_tx (Tx, optional): Scaling/translation matrix for sheet. Defaults to Tx(). + + Returns: + str: EESCHEMA text for the node circuitry. + """ + + from skidl import HIER_SEP + + # List to hold all the EESCHEMA code for this node. + eeschema_code = [] + + if node.flattened: + # Create the transformation matrix for the placement of the parts in the node. + tx = node.tx * sheet_tx + else: + # Unflattened nodes are placed in their own sheet, so compute + # their bounding box as if they *were* flattened and use that to + # find the transformation matrix for an appropriately-sized sheet. + flattened_bbox = node.internal_bbox() + tx = calc_sheet_tx(flattened_bbox) + + # Generate EESCHEMA code for each child of this node. + for child in node.children.values(): + eeschema_code.append(node_to_eeschema(child, tx)) + + # Generate EESCHEMA code for each part in the node. + for part in node.parts: + if isinstance(part, NetTerminal): + eeschema_code.append(net_to_eeschema(part, tx=tx)) + else: + eeschema_code.append(part_to_eeschema(part, tx=tx)) + + # Generate EESCHEMA wiring code between the parts in the node. + for net, wire in node.wires.items(): + wire_code = wire_to_eeschema(net, wire, tx=tx) + eeschema_code.append(wire_code) + for net, junctions in node.junctions.items(): + junction_code = junction_to_eeschema(net, junctions, tx=tx) + eeschema_code.append(junction_code) + + # Generate power connections for the each part in the node. + for part in node.parts: + stub_code = power_part_to_eeschema(part, tx=tx) + if len(stub_code) != 0: + eeschema_code.append(stub_code) + + # Generate pin labels for stubbed nets on each part in the node. + for part in node.parts: + for pin in part: + pin_label_code = pin_label_to_eeschema(pin, tx=tx) + eeschema_code.append(pin_label_code) + + # Join EESCHEMA code into one big string. + eeschema_code = "\n".join(eeschema_code) + + # If this node was flattened, then return the EESCHEMA code and surrounding box + # for inclusion in the parent node. + if node.flattened: + + # Generate the graphic box that surrounds the flattened hierarchical block of this node. + block_name = node.name.split(HIER_SEP)[-1] + pad = Vector(BLK_INT_PAD, BLK_INT_PAD) + bbox_code = bbox_to_eeschema(node.bbox.resize(pad), tx, block_name) + + return "\n".join((eeschema_code, bbox_code)) + + # Create a hierarchical sheet file for storing this unflattened node. + A_size = get_A_size(flattened_bbox) + filepath = os.path.join(node.filepath, node.sheet_filename) + create_eeschema_file(filepath, eeschema_code, title=node.title, A_size=A_size) + + # Create the hierarchical sheet for insertion into the calling node sheet. + bbox = (node.bbox * node.tx * sheet_tx).round() + time_hex = hex(int(time.time()))[2:] + return "\n".join( + ( + "$Sheet", + "S {} {} {} {}".format(bbox.ll.x, bbox.ll.y, bbox.w, bbox.h), + "U {}".format(time_hex), + 'F0 "{}" {}'.format(node.name, node.name_sz), + 'F1 "{}" {}'.format(node.sheet_filename, node.filename_sz), + "$EndSheet", + "", + ) + ) + + +""" +Generate a KiCad EESCHEMA schematic from a Circuit object. +""" + +# TODO: Handle symio attribute. + + +def preprocess_circuit(circuit, **options): + """Add stuff to parts & nets for doing placement and routing of schematics.""" + + def units(part): + if len(part.unit) == 0: + return [part] + else: + return part.unit.values() + + def initialize(part): + """Initialize part or its part units.""" + + # Initialize the units of the part, or the part itself if it has no units. + pin_limit = options.get("orientation_pin_limit", 44) + for part_unit in units(part): + # Initialize transform matrix. + part_unit.tx = Tx.from_symtx(getattr(part_unit, "symtx", "")) + + # Lock part orientation if symtx was specified. Also lock parts with a lot of pins + # since they're typically drawn the way they're supposed to be oriented. + # And also lock single-pin parts because these are usually power/ground and + # they shouldn't be flipped around. + num_pins = len(part_unit.pins) + part_unit.orientation_locked = getattr(part_unit, "symtx", False) or not ( + 1 < num_pins <= pin_limit + ) + + # Assign pins from the parent part to the part unit. + part_unit.grab_pins() + + # Initialize pin attributes used for generating schematics. + for pin in part_unit: + pin.pt = Point(pin.x, pin.y) + pin.routed = False + + def rotate_power_pins(part): + """Rotate a part based on the direction of its power pins. + + This function is to make sure that voltage sources face up and gnd pins + face down. + """ + + # Don't rotate parts that are already explicitly rotated/flipped. + if not getattr(part, "symtx", ""): + return + + def is_pwr(net): + return net_name.startswith("+") + + def is_gnd(net): + return "gnd" in net_name.lower() + + dont_rotate_pin_cnt = options.get("dont_rotate_pin_count", 10000) + + for part_unit in units(part): + # Don't rotate parts with too many pins. + if len(part_unit) > dont_rotate_pin_cnt: + return + + # Tally what rotation would make each pwr/gnd pin point up or down. + rotation_tally = Counter() + for pin in part_unit: + net_name = getattr(pin.net, "name", "").lower() + if is_gnd(net_name): + if pin.orientation == "U": + rotation_tally[0] += 1 + if pin.orientation == "D": + rotation_tally[180] += 1 + if pin.orientation == "L": + rotation_tally[90] += 1 + if pin.orientation == "R": + rotation_tally[270] += 1 + elif is_pwr(net_name): + if pin.orientation == "D": + rotation_tally[0] += 1 + if pin.orientation == "U": + rotation_tally[180] += 1 + if pin.orientation == "L": + rotation_tally[270] += 1 + if pin.orientation == "R": + rotation_tally[90] += 1 + + # Rotate the part unit in the direction with the most tallies. + try: + rotation = rotation_tally.most_common()[0][0] + except IndexError: + pass + else: + # Rotate part unit 90-degrees clockwise until the desired rotation is reached. + tx_cw_90 = Tx(a=0, b=-1, c=1, d=0) # 90-degree trans. matrix. + for _ in range(int(round(rotation / 90))): + part_unit.tx = part_unit.tx * tx_cw_90 + + def calc_part_bbox(part): + """Calculate the labeled bounding boxes and store it in the part.""" + + # Find part/unit bounding boxes excluding any net labels on pins. + # TODO: part.lbl_bbox could be substituted for part.bbox. + # TODO: Part ref and value should be updated before calculating bounding box. + bare_bboxes = calc_symbol_bbox(part)[1:] + + for part_unit, bare_bbox in zip(units(part), bare_bboxes): + # Expand the bounding box if it's too small in either dimension. + resize_wh = Vector(0, 0) + if bare_bbox.w < 100: + resize_wh.x = (100 - bare_bbox.w) / 2 + if bare_bbox.h < 100: + resize_wh.y = (100 - bare_bbox.h) / 2 + bare_bbox = bare_bbox.resize(resize_wh) + + # Find expanded bounding box that includes any hier labels attached to pins. + part_unit.lbl_bbox = BBox() + part_unit.lbl_bbox.add(bare_bbox) + for pin in part_unit: + if pin.stub: + # Find bounding box for net stub label attached to pin. + hlbl_bbox = calc_hier_label_bbox(pin.net.name, pin.orientation) + # Move the label bbox to the pin location. + hlbl_bbox *= Tx().move(pin.pt) + # Update the bbox for the labelled part with this pin label. + part_unit.lbl_bbox.add(hlbl_bbox) + + # Set the active bounding box to the labeled version. + part_unit.bbox = part_unit.lbl_bbox + + # Pre-process parts + for part in circuit.parts: + # Initialize part attributes used for generating schematics. + initialize(part) + + # Rotate parts. Power pins should face up. GND pins should face down. + rotate_power_pins(part) + + # Compute bounding boxes around parts + calc_part_bbox(part) + + +def finalize_parts_and_nets(circuit, **options): + """Restore parts and nets after place & route is done.""" + + # Remove any NetTerminals that were added. + net_terminals = (p for p in circuit.parts if isinstance(p, NetTerminal)) + circuit.rmv_parts(*net_terminals) + + # Return pins from the part units to their parent part. + for part in circuit.parts: + part.grab_pins() + + # Remove some stuff added to parts during schematic generation process. + rmv_attr(circuit.parts, ("force", "bbox", "lbl_bbox", "tx")) + + +@export_to_all +def gen_schematic( + circuit, + filepath=".", + top_name=get_script_name(), + title="SKiDL-Generated Schematic", + flatness=0.0, + retries=2, + **options +): + """Create a schematic file from a Circuit object. + + Args: + circuit (Circuit): The Circuit object that will have a schematic generated for it. + filepath (str, optional): The directory where the schematic files are placed. Defaults to ".". + top_name (str, optional): The name for the top of the circuit hierarchy. Defaults to get_script_name(). + title (str, optional): The title of the schematic. Defaults to "SKiDL-Generated Schematic". + flatness (float, optional): Determines how much the hierarchy is flattened in the schematic. Defaults to 0.0 (completely hierarchical). + retries (int, optional): Number of times to re-try if routing fails. Defaults to 2. + options (dict, optional): Dict of options and values, usually for drawing/debugging. + """ + + from skidl import KICAD + from skidl.schematics.place import PlacementFailure + from skidl.schematics.route import RoutingFailure + from skidl.tools import tool_modules + from skidl.schematics.node import Node + + # Part placement options that should always be turned on. + options["use_push_pull"] = True + options["rotate_parts"] = True + options["pt_to_pt_mult"] = 5 # HACK: Ad-hoc value. + options["pin_normalize"] = True + + # Start with default routing area. + expansion_factor = 1.0 + + # Try to place & route one or more times. + for _ in range(retries): + preprocess_circuit(circuit, **options) + + node = Node(circuit, tool_modules[KICAD], filepath, top_name, title, flatness) + + try: + # Place parts. + node.place(expansion_factor=expansion_factor, **options) + + # Route parts. + node.route(**options) + + except PlacementFailure: + # Placement failed, so clean up ... + finalize_parts_and_nets(circuit, **options) + # ... and try again. + continue + + except RoutingFailure: + # Routing failed, so clean up ... + finalize_parts_and_nets(circuit, **options) + # ... and expand routing area ... + expansion_factor *= 1.5 # HACK: Ad-hoc increase of expansion factor. + # ... and try again. + continue + + # Generate EESCHEMA code for the schematic. + node_to_eeschema(node) + + # Clean up. + finalize_parts_and_nets(circuit, **options) + + # Place & route was successful if we got here, so exit. + return + + # Clean-up after failure. + finalize_parts_and_nets(circuit, **options) + + # Exited the loop without successful routing. + raise (RoutingFailure) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py new file mode 100644 index 00000000..ba98d4f6 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -0,0 +1,485 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +from math import sqrt, sin, cos, radians +from copy import copy + +from ..utilities import export_to_all + +__all__ = [ + "mms_per_mil", + "mils_per_mm", + "Vector", + "tx_rot_0", + "tx_rot_90", + "tx_rot_180", + "tx_rot_270", + "tx_flip_x", + "tx_flip_y", +] + + +""" +Stuff for handling geometry: + transformation matrices, + points, + bounding boxes, + line segments. +""" + +# Millimeters/thousandths-of-inch conversion factor. +mils_per_mm = 39.37008 +mms_per_mil = 0.0254 + + +@export_to_all +def to_mils(mm): + """Convert millimeters to thousandths-of-inch and return.""" + return mm * mils_per_mm + + +@export_to_all +def to_mms(mils): + """Convert thousandths-of-inch to millimeters and return.""" + return mils * mms_per_mil + + +@export_to_all +class Tx: + def __init__(self, a=1, b=0, c=0, d=1, dx=0, dy=0): + """Create a transformation matrix. + tx = [ + a b 0 + c d 0 + dx dy 1 + ] + x' = a*x + c*y + dx + y' = b*x + d*y + dy + """ + self.a = a + self.b = b + self.c = c + self.d = d + self.dx = dx + self.dy = dy + + @classmethod + def from_symtx(cls, symtx): + """Return a Tx() object that implements the "HVLR" geometric operation sequence. + + Args: + symtx (str): A string of H, V, L, R operations that are applied in sequence left-to-right. + + Returns: + Tx: A transformation matrix that implements the sequence of geometric operations. + """ + op_dict = { + "H": Tx(a=-1, c=0, b=0, d=1), # Horizontal flip. + "V": Tx(a=1, c=0, b=0, d=-1), # Vertical flip. + "L": Tx(a=0, c=-1, b=1, d=0), # Rotate 90 degrees left (counter-clockwise). + "R": Tx(a=0, c=1, b=-1, d=0), # Rotate 90 degrees right (clockwise). + } + + tx = Tx() + for op in symtx.upper(): + tx *= op_dict[op] + return tx + + def __repr__(self): + return "{self.__class__}({self.a}, {self.b}, {self.c}, {self.d}, {self.dx}, {self.dy})".format( + self=self + ) + + def __str__(self): + return "[{self.a}, {self.b}, {self.c}, {self.d}, {self.dx}, {self.dy}]".format( + self=self + ) + + def __mul__(self, m): + """Return the product of two transformation matrices.""" + if isinstance(m, Tx): + tx = m + else: + # Assume m is a scalar, so convert it to a scaling Tx matrix. + tx = Tx(a=m, d=m) + return Tx( + a=self.a * tx.a + self.b * tx.c, + b=self.a * tx.b + self.b * tx.d, + c=self.c * tx.a + self.d * tx.c, + d=self.c * tx.b + self.d * tx.d, + dx=self.dx * tx.a + self.dy * tx.c + tx.dx, + dy=self.dx * tx.b + self.dy * tx.d + tx.dy, + ) + + @property + def origin(self): + """Return the (dx, dy) translation as a Point.""" + return Point(self.dx, self.dy) + + # This setter doesn't work in Python 2.7.18. + # @origin.setter + # def origin(self, pt): + # """Set the (dx, dy) translation from an (x,y) Point.""" + # self.dx, self.dy = pt.x, pt.y + + @property + def scale(self): + """Return the scaling factor.""" + return (Point(1, 0) * self - Point(0, 0) * self).magnitude + + def move(self, vec): + """Return Tx with movement vector applied.""" + return self * Tx(dx=vec.x, dy=vec.y) + + def rot_90cw(self): + """Return Tx with 90-deg clock-wise rotation around (0, 0).""" + return self * Tx(a=0, b=1, c=-1, d=0) + + def rot(self, degs): + """Return Tx rotated by the given angle (in degrees).""" + rads = radians(degs) + return self * Tx(a=cos(rads), b=sin(rads), c=-sin(rads), d=cos(rads)) + + def flip_x(self): + """Return Tx with X coords flipped around (0, 0).""" + return self * Tx(a=-1) + + def flip_y(self): + """Return Tx with Y coords flipped around (0, 0).""" + return self * Tx(d=-1) + + def no_translate(self): + """Return Tx with translation set to (0,0).""" + return Tx(a=self.a, b=self.b, c=self.c, d=self.d) + + +# Some common rotations. +tx_rot_0 = Tx(a=1, b=0, c=0, d=1) +tx_rot_90 = Tx(a=0, b=1, c=-1, d=0) +tx_rot_180 = Tx(a=-1, b=0, c=0, d=-1) +tx_rot_270 = Tx(a=0, b=-1, c=1, d=0) + +# Some common flips. +tx_flip_x = Tx(a=-1, b=0, c=0, d=1) +tx_flip_y = Tx(a=1, b=0, c=0, d=-1) + + +@export_to_all +class Point: + def __init__(self, x, y): + """Create a Point with coords x,y.""" + self.x = x + self.y = y + + def __hash__(self): + """Return hash of X,Y tuple.""" + return hash((self.x, self.y)) + + def __eq__(self, other): + """Return true if (x,y) tuples of self and other are the same.""" + return (self.x, self.y) == (other.x, other.y) + + def __lt__(self, other): + """Return true if (x,y) tuple of self compares as less than (x,y) tuple of other.""" + return (self.x, self.y) < (other.x, other.y) + + def __ne__(self, other): + """Return true if (x,y) tuples of self and other differ.""" + return not (self == other) + + def __add__(self, pt): + """Add the x,y coords of pt to self and return the resulting Point.""" + if not isinstance(pt, Point): + pt = Point(pt, pt) + return Point(self.x + pt.x, self.y + pt.y) + + def __sub__(self, pt): + """Subtract the x,y coords of pt from self and return the resulting Point.""" + if not isinstance(pt, Point): + pt = Point(pt, pt) + return Point(self.x - pt.x, self.y - pt.y) + + def __mul__(self, m): + """Apply transformation matrix or scale factor to a point and return a point.""" + if isinstance(m, Tx): + return Point( + self.x * m.a + self.y * m.c + m.dx, self.x * m.b + self.y * m.d + m.dy + ) + elif isinstance(m, Point): + return Point(self.x * m.x, self.y * m.y) + else: + return Point(m * self.x, m * self.y) + + def __rmul__(self, m): + if isinstance(m, Tx): + raise ValueError + else: + return self * m + + def xprod(self, pt): + """Cross-product of two 2D vectors returns scalar in Z coord.""" + return self.x * pt.y - self.y * pt.x + + def mask(self, msk): + """Multiply the X & Y coords by the elements of msk.""" + return Point(self.x * msk[0], self.y * msk[1]) + + def __neg__(self): + """Negate both coords.""" + return Point(-self.x, -self.y) + + def __truediv__(self, d): + """Divide the x,y coords by d.""" + return Point(self.x / d, self.y / d) + + def __div__(self, d): + """Divide the x,y coords by d.""" + return Point(self.x / d, self.y / d) + + def round(self): + return Point(int(round(self.x)), int(round(self.y))) + + def __str__(self): + return "{} {}".format(self.x, self.y) + + def snap(self, grid_spacing): + """Snap point x,y coords to the given grid spacing.""" + snap_func = lambda x: int(grid_spacing * round(x / grid_spacing)) + return Point(snap_func(self.x), snap_func(self.y)) + + def min(self, pt): + """Return a Point with coords that are the min x,y of both points.""" + return Point(min(self.x, pt.x), min(self.y, pt.y)) + + def max(self, pt): + """Return a Point with coords that are the max x,y of both points.""" + return Point(max(self.x, pt.x), max(self.y, pt.y)) + + @property + def magnitude(self): + """Get the distance of the point from the origin.""" + return sqrt(self.x**2 + self.y**2) + + @property + def norm(self): + """Return a unit vector pointing from the origin to the point.""" + try: + return self / self.magnitude + except ZeroDivisionError: + return Point(0, 0) + + def flip_xy(self): + """Flip X-Y coordinates of point.""" + self.x, self.y = self.y, self.x + + def __repr__(self): + return "{self.__class__}({self.x}, {self.y})".format(self=self) + + def __str__(self): + return "({}, {})".format(self.x, self.y) + + +Vector = Point + + +@export_to_all +class BBox: + def __init__(self, *pts): + """Create a bounding box surrounding the given points.""" + inf = float("inf") + self.min = Point(inf, inf) + self.max = Point(-inf, -inf) + self.add(*pts) + + def __add__(self, obj): + """Return the merged BBox of two BBoxes or a BBox and a Point.""" + sum_ = BBox() + if isinstance(obj, Point): + sum_.min = self.min.min(obj) + sum_.max = self.max.max(obj) + elif isinstance(obj, BBox): + sum_.min = self.min.min(obj.min) + sum_.max = self.max.max(obj.max) + else: + raise NotImplementedError + return sum_ + + def __iadd__(self, obj): + """Update BBox bt adding another Point or BBox""" + sum_ = self + obj + self.min = sum_.min + self.max = sum_.max + return self + + def add(self, *objs): + """Update the bounding box size by adding Point/BBox objects.""" + for obj in objs: + self += obj + return self + + def __mul__(self, m): + return BBox(self.min * m, self.max * m) + + def round(self): + return BBox(self.min.round(), self.max.round()) + + def is_inside(self, pt): + """Return True if point is inside bounding box.""" + return (self.min.x <= pt.x <= self.max.x) and (self.min.y <= pt.y <= self.max.y) + + def intersects(self, bbox): + """Return True if the two bounding boxes intersect.""" + return ( + (self.min.x < bbox.max.x) + and (self.max.x > bbox.min.x) + and (self.min.y < bbox.max.y) + and (self.max.y > bbox.min.y) + ) + + def intersection(self, bbox): + """Return the bounding box of the intersection between the two bounding boxes.""" + if not self.intersects(bbox): + return None + corner1 = self.min.max(bbox.min) + corner2 = self.max.min(bbox.max) + return BBox(corner1, corner2) + + def resize(self, vector): + """Expand/contract the bounding box by applying vector to its corner points.""" + return BBox(self.min - vector, self.max + vector) + + def snap_resize(self, grid_spacing): + """Resize bbox so max and min points are on grid. + + Args: + grid_spacing (float): Grid spacing. + """ + bbox = self.resize(Point(grid_spacing - 1, grid_spacing - 1)) + bbox.min = bbox.min.snap(grid_spacing) + bbox.max = bbox.max.snap(grid_spacing) + return bbox + + @property + def area(self): + """Return area of bounding box.""" + return self.w * self.h + + @property + def w(self): + """Return the bounding box width.""" + return abs(self.max.x - self.min.x) + + @property + def h(self): + """Return the bounding box height.""" + return abs(self.max.y - self.min.y) + + @property + def ctr(self): + """Return center point of bounding box.""" + return (self.max + self.min) / 2 + + @property + def ll(self): + """Return lower-left point of bounding box.""" + return Point(self.min.x, self.min.y) + + @property + def lr(self): + """Return lower-right point of bounding box.""" + return Point(self.max.x, self.min.y) + + @property + def ul(self): + """Return upper-left point of bounding box.""" + return Point(self.min.x, self.max.y) + + @property + def ur(self): + """Return upper-right point of bounding box.""" + return Point(self.max.x, self.max.y) + + def __repr__(self): + return "{self.__class__}(Point({self.min}), Point({self.max}))".format( + self=self + ) + + def __str__(self): + return "[{}, {}]".format(self.min, self.max) + + +@export_to_all +class Segment: + def __init__(self, p1, p2): + "Create a line segment between two points." + self.p1 = copy(p1) + self.p2 = copy(p2) + + def __mul__(self, m): + """Apply transformation matrix to a segment and return a segment.""" + return Segment(self.p1 * m, self.p2 * m) + + def round(self): + return Segment(self.p1.round(), self.p2.round()) + + def __str__(self): + return "{} {}".format(str(self.p1), str(self.p2)) + + def flip_xy(self): + """Flip the X-Y coordinates of the segment.""" + self.p1.flip_xy() + self.p2.flip_xy() + + def intersects(self, other): + """Return true if the segments intersect.""" + + # FIXME: This fails if the segments are parallel! + raise NotImplementedError + + # Given two segments: + # self: p1 + (p2-p1) * t1 + # other: p3 + (p4-p3) * t2 + # Look for a solution t1, t2 that solves: + # p1x + (p2x-p1x)*t1 = p3x + (p4x-p3x)*t2 + # p1y + (p2y-p1y)*t1 = p3y + (p4y-p3y)*t2 + # If t1 and t2 are both in range [0,1], then the two segments intersect. + + p1x, p1y, p2x, p2y = self.p1.x, self.p1.y, self.p2.x, self.p2.y + p3x, p3y, p4x, p4y = other.p1.x, other.p1.y, other.p2.x, other.p2.y + + # denom = p1x*p3y - p1x*p4y - p1y*p3x + p1y*p4x - p2x*p3y + p2x*p4y + p2y*p3x - p2y*p4x + # denom = p1x * (p3y - p4y) + p1y * (p4x - p3x) + p2x * (p4y - p3y) + p2y * (p3x - p4x) + denom = (p1x - p2x) * (p3y - p4y) + (p1y - p2y) * (p4x - p3x) + + try: + # t1 = (p1x*p3y - p1x*p4y - p1y*p3x + p1y*p4x + p3x*p4y - p3y*p4x) / denom + # t2 = (-p1x*p2y + p1x*p3y + p1y*p2x - p1y*p3x - p2x*p3y + p2y*p3x) / denom + t1 = ((p1y - p3y) * (p4x - p3x) - (p1x - p3x) * (p4y - p3y)) / denom + t2 = ((p1y - p3y) * (p2x - p3x) - (p1x - p3x) * (p2y - p3y)) / denom + except ZeroDivisionError: + return False + + return (0 <= t1 <= 1) and (0 <= t2 <= 1) + + def shadows(self, other): + """Return true if two segments overlap each other even if they aren't on the same horiz or vertical track.""" + + if self.p1.x == self.p2.x and other.p1.x == other.p2.x: + # Horizontal segments. See if their vertical extents overlap. + self_min = min(self.p1.y, self.p2.y) + self_max = max(self.p1.y, self.p2.y) + other_min = min(other.p1.y, other.p2.y) + other_max = max(other.p1.y, other.p2.y) + elif self.p1.y == self.p2.y and other.p1.y == other.p2.y: + # Verttical segments. See if their horizontal extents overlap. + self_min = min(self.p1.x, self.p2.x) + self_max = max(self.p1.x, self.p2.x) + other_min = min(other.p1.x, other.p2.x) + other_max = max(other.p1.x, other.p2.x) + else: + # Segments aren't horizontal or vertical, so neither can shadow the other. + return False + + # Overlap conditions based on segment endpoints. + return other_min < self_max and other_max > self_min diff --git a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py new file mode 100644 index 00000000..e1d9e869 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +from skidl import Part, Pin +from skidl.utilities import export_to_all +from .geometry import Point, Tx, Vector + + +""" +Net_Terminal class for handling net labels. +""" + + +@export_to_all +class NetTerminal(Part): + def __init__(self, net, tool_module): + """Specialized Part with a single pin attached to a net. + + This is intended for attaching to nets to label them, typically when + the net spans across levels of hierarchical nodes. + """ + + # Create a Part. + from skidl import SKIDL + + super().__init__(name="NT", ref_prefix="NT", tool=SKIDL) + + # Set a default transformation matrix for this part. + self.tx = Tx() + + # Add a single pin to the part. + pin = Pin(num="1", name="~") + self.add_pins(pin) + + # Connect the pin to the net. + pin += net + + # Set the pin at point (0,0) and pointing leftward toward the part body + # (consisting of just the net label for this type of part) so any attached routing + # will go to the right. + pin.x, pin.y = 0, 0 + pin.pt = Point(pin.x, pin.y) + pin.orientation = "L" + + # Calculate the bounding box, but as if the pin were pointed right so + # the associated label text would go to the left. + self.bbox = tool_module.calc_hier_label_bbox(net.name, "R") + + # Resize bbox so it's an integer number of GRIDs. + self.bbox = self.bbox.snap_resize(tool_module.constants.GRID) + + # Extend the bounding box a bit so any attached routing will come straight in. + self.bbox.max += Vector(tool_module.constants.GRID, 0) + self.lbl_bbox = self.bbox + + # Flip the NetTerminal horizontally if it is an output net (label on the right). + netio = getattr(net, "netio", "").lower() + self.orientation_locked = bool(netio in ("i", "o")) + if getattr(net, "netio", "").lower() == "o": + origin = Point(0, 0) + term_origin = self.tx.origin + self.tx = ( + self.tx.move(origin - term_origin).flip_x().move(term_origin - origin) + ) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py new file mode 100644 index 00000000..9c658051 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +import re +from collections import defaultdict +from itertools import chain + +from skidl.utilities import export_to_all, rmv_attr +from .geometry import BBox, Point, Tx, Vector +from .place import Placer +from .route import Router + + +""" +Node class for storing circuit hierarchy. +""" + + +@export_to_all +class Node(Placer, Router): + """Data structure for holding information about a node in the circuit hierarchy.""" + + filename_sz = 20 + name_sz = 40 + + def __init__( + self, + circuit=None, + tool_module=None, + filepath=".", + top_name="", + title="", + flatness=0.0, + ): + self.parent = None + self.children = defaultdict( + lambda: Node(None, tool_module, filepath, top_name, title, flatness) + ) + self.filepath = filepath + self.top_name = top_name + self.sheet_name = None + self.sheet_filename = None + self.title = title + self.flatness = flatness + self.flattened = False + self.tool_module = tool_module # Backend tool. + self.parts = [] + self.wires = defaultdict(list) + self.junctions = defaultdict(list) + self.tx = Tx() + self.bbox = BBox() + + if circuit: + self.add_circuit(circuit) + + def find_node_with_part(self, part): + """Find the node that contains the part based on its hierarchy. + + Args: + part (Part): The part being searched for in the node hierarchy. + + Returns: + Node: The Node object containing the part. + """ + + from skidl.circuit import HIER_SEP + + level_names = part.hierarchy.split(HIER_SEP) + node = self + for lvl_nm in level_names[1:]: + node = node.children[lvl_nm] + assert part in node.parts + return node + + def add_circuit(self, circuit): + """Add parts in circuit to node and its children. + + Args: + circuit (Circuit): Circuit object. + """ + + # Build the circuit node hierarchy by adding the parts. + for part in circuit.parts: + self.add_part(part) + + # Add terminals to nodes in the hierarchy for nets that span across nodes. + for net in circuit.nets: + # Skip nets that are stubbed since there will be no wire to attach to the NetTerminal. + if getattr(net, "stub", False): + continue + + # Search for pins in different nodes. + for pin1, pin2 in zip(net.pins[:-1], net.pins[1:]): + if pin1.part.hierarchy != pin2.part.hierarchy: + # Found pins in different nodes, so break and add terminals to nodes below. + break + else: + if len(net.pins) == 1: + # Single pin on net and not stubbed, so add a terminal to it below. + pass + elif not net.is_implicit(): + # The net has a user-assigned name, so add a terminal to it below. + pass + else: + # No need for net terminal because there are multiple pins + # and they are all in the same node. + continue + + # Add a single terminal to each node that contains one or more pins of the net. + visited = [] + for pin in net.pins: + # A stubbed pin can't be used to add NetTerminal since there is no explicit wire. + if pin.stub: + continue + + part = pin.part + + if part.hierarchy in visited: + # Already added a terminal to this node, so don't add another. + continue + + # Add NetTerminal to the node with this part/pin. + self.find_node_with_part(part).add_terminal(net) + + # Record that this hierarchical node was visited. + visited.append(part.hierarchy) + + # Flatten the hierarchy as specified by the flatness parameter. + self.flatten(self.flatness) + + def add_part(self, part, level=0): + """Add a part to the node at the appropriate level of the hierarchy. + + Args: + part (Part): Part to be added to this node or one of its children. + level (int, optional): The current level (depth) of the node in the hierarchy. Defaults to 0. + """ + + from skidl.circuit import HIER_SEP + + # Get list of names of hierarchical levels (in order) leading to this part. + level_names = part.hierarchy.split(HIER_SEP) + + # Get depth in hierarchy for this part. + part_level = len(level_names) - 1 + assert part_level >= level + + # Node name is the name assigned to this level of the hierarchy. + self.name = level_names[level] + + # File name for storing the schematic for this node. + base_filename = "_".join([self.top_name] + level_names[0 : level + 1]) + ".sch" + self.sheet_filename = base_filename + + if part_level == level: + # Add part to node at this level in the hierarchy. + if not part.unit: + # Monolithic part so just add it to the node. + self.parts.append(part) + else: + # Multi-unit part so add each unit to the node. + # FIXME: Some part units might be split into other nodes. + for p in part.unit.values(): + self.parts.append(p) + else: + # Part is at a level below the current node. Get the child node using + # the name of the next level in the hierarchy for this part. + child_node = self.children[level_names[level + 1]] + + # Attach the child node to this node. (It may have just been created.) + child_node.parent = self + + # Add part to the child node (or one of its children). + child_node.add_part(part, level + 1) + + def add_terminal(self, net): + """Add a terminal for this net to the node. + + Args: + net (Net): The net to be added to this node. + """ + + from skidl.circuit import HIER_SEP + from .net_terminal import NetTerminal + + nt = NetTerminal(net, self.tool_module) + self.parts.append(nt) + + def external_bbox(self): + """Return the bounding box of a hierarchical sheet as seen by its parent node.""" + bbox = BBox(Point(0, 0), Point(500, 500)) + bbox.add(Point(len("File: " + self.sheet_filename) * self.filename_sz, 0)) + bbox.add(Point(len("Sheet: " + self.name) * self.name_sz, 0)) + + # Pad the bounding box for extra spacing when placed. + bbox = bbox.resize(Vector(100, 100)) + + return bbox + + def internal_bbox(self): + """Return the bounding box for the circuitry contained within this node.""" + + # The bounding box is determined by the arrangement of the node's parts and child nodes. + bbox = BBox() + for obj in chain(self.parts, self.children.values()): + tx_bbox = obj.bbox * obj.tx + bbox.add(tx_bbox) + + # Pad the bounding box for extra spacing when placed. + bbox = bbox.resize(Vector(100, 100)) + + return bbox + + def calc_bbox(self): + """Compute the bounding box for the node in the circuit hierarchy.""" + + if self.flattened: + self.bbox = self.internal_bbox() + else: + # Use hierarchical bounding box if node has not been flattened. + self.bbox = self.external_bbox() + + return self.bbox + + def flatten(self, flatness=0.0): + """Flatten node hierarchy according to flatness parameter. + + Args: + flatness (float, optional): Degree of hierarchical flattening (0=completely hierarchical, 1=totally flat). Defaults to 0.0. + + Create hierarchical sheets for the node and its child nodes. Complexity (or size) of a node + and its children is the total number of part pins they contain. The sum of all the child sizes + multiplied by the flatness is the number of part pins that can be shown on the schematic + page before hierarchy is used. The instances of each type of child are flattened and placed + directly in the sheet as long as the sum of their sizes is below the slack. Otherwise, the + children are included using hierarchical sheets. The children are handled in order of + increasing size so small children are more likely to be flattened while large, complicated + children are included using hierarchical sheets. + """ + + # Create sheets and compute complexity for any circuitry in hierarchical child nodes. + for child in self.children.values(): + child.flatten(flatness) + + # Complexity of the parts directly instantiated at this hierarchical level. + self.complexity = sum((len(part) for part in self.parts)) + + # Sum the child complexities and use it to compute the number of pins that can be + # shown before hierarchical sheets are used. + child_complexity = sum((child.complexity for child in self.children.values())) + slack = child_complexity * flatness + + # Group the children according to what types of modules they are by removing trailing instance ids. + child_types = defaultdict(list) + for child_id, child in self.children.items(): + child_types[re.sub(r"\d+$", "", child_id)].append(child) + + # Compute the total size of each type of children. + child_type_sizes = dict() + for child_type, children in child_types.items(): + child_type_sizes[child_type] = sum((child.complexity for child in children)) + + # Sort the groups from smallest total size to largest. + sorted_child_type_sizes = sorted( + child_type_sizes.items(), key=lambda item: item[1] + ) + + # Flatten each instance in a group until the slack is used up. + for child_type, child_type_size in sorted_child_type_sizes: + if child_type_size <= slack: + # Include the circuitry of each child instance directly in the sheet. + for child in child_types[child_type]: + child.flattened = True + # Reduce the slack by the sum of the child sizes. + slack -= child_type_size + else: + # Not enough slack left. Add these children as hierarchical sheets. + for child in child_types[child_type]: + child.flattened = False + + def get_internal_nets(self): + """Return a list of nets that have at least one pin on a part in this node.""" + + processed_nets = [] + internal_nets = [] + for part in self.parts: + for part_pin in part: + # No explicit wire for pins connected to labeled stub nets. + if part_pin.stub: + continue + + # No explicit wires if the pin is not connected to anything. + if not part_pin.is_connected(): + continue + + net = part_pin.net + + # Skip nets that have already been processed. + if net in processed_nets: + continue + + processed_nets.append(net) + + # Skip stubbed nets. + if getattr(net, "stub", False) is True: + continue + + # Add net to collection if at least one pin is on one of the parts of the node. + for net_pin in net.pins: + if net_pin.part in self.parts: + internal_nets.append(net) + break + + return internal_nets + + def get_internal_pins(self, net): + """Return the pins on the net that are on parts in the node. + + Args: + net (Net): The net whose pins are being examined. + + Returns: + list: List of pins on the net that are on parts in this node. + """ + + # Skip pins on stubbed nets. + if getattr(net, "stub", False) is True: + return [] + + return [pin for pin in net.pins if pin.stub is False and pin.part in self.parts] + + def collect_stats(self, **options): + """Return comma-separated string with place & route statistics of a schematic.""" + + def get_wire_length(node): + """Return the sum of the wire segment lengths between parts in a routed node.""" + + wire_length = 0 + + # Sum wire lengths for child nodes. + for child in node.children.values(): + wire_length += get_wire_length(child) + + # Add the wire lengths between parts in the top node. + for wire_segs in node.wires.values(): + for seg in wire_segs: + len_x = abs(seg.p1.x - seg.p2.x) + len_y = abs(seg.p1.y - seg.p2.y) + wire_length += len_x + len_y + + return wire_length + + return "{}\n".format(get_wire_length(self)) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py new file mode 100644 index 00000000..bbe9869c --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -0,0 +1,1534 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Autoplacer for arranging symbols in a schematic. +""" + +import functools +import itertools +import math +import random +import sys +from collections import defaultdict +from copy import copy + +from skidl import Pin +from skidl.utilities import export_to_all, rmv_attr, sgn +from .debug_draw import ( + draw_end, + draw_pause, + draw_placement, + draw_redraw, + draw_start, + draw_text, +) +from .geometry import BBox, Point, Segment, Tx, Vector + + +__all__ = [ + "PlacementFailure", +] + + +################################################################### +# +# OVERVIEW OF AUTOPLACER +# +# The input is a Node containing child nodes and parts. The parts in +# each child node are placed, and then the blocks for each child are +# placed along with the parts in this node. +# +# The individual parts in a node are separated into groups: +# 1) multiple groups of parts that are all interconnected by one or +# more nets, and 2) a single group of parts that are not connected +# by any explicit nets (i.e., floating parts). +# +# Each group of connected parts are placed using force-directed placement. +# Each net exerts an attractive force pulling parts together, and +# any overlap of parts exerts a repulsive force pushing them apart. +# Initially, the attractive force is dominant but, over time, it is +# decreased while the repulsive force is increased using a weighting +# factor. After that, any part overlaps are cleared and the parts +# are aligned to the routing grid. +# +# Force-directed placement is also used with the floating parts except +# the non-existent net forces are replaced by a measure of part similarity. +# This collects similar parts (such as bypass capacitors) together. +# +# The child-node blocks are then arranged with the blocks of connected +# and floating parts to arrive at a total placement for this node. +# +################################################################### + + +class PlacementFailure(Exception): + """Exception raised when parts or blocks could not be placed.""" + + pass + + +# Small functions for summing Points and Vectors. +pt_sum = lambda pts: sum(pts, Point(0, 0)) +force_sum = lambda forces: sum(forces, Vector(0, 0)) + + +def is_net_terminal(part): + from skidl.schematics.net_terminal import NetTerminal + + return isinstance(part, NetTerminal) + + +def get_snap_pt(part_or_blk): + """Get the point for snapping the Part or PartBlock to the grid. + + Args: + part_or_blk (Part | PartBlock): Object with snap point. + + Returns: + Point: Point for snapping to grid or None if no point found. + """ + try: + return part_or_blk.pins[0].pt + except AttributeError: + try: + return part_or_blk.snap_pt + except AttributeError: + return None + + +def snap_to_grid(part_or_blk): + """Snap Part or PartBlock to grid. + + Args: + part (Part | PartBlk): Object to snap to grid. + """ + + # Get the position of the current snap point. + pt = get_snap_pt(part_or_blk) * part_or_blk.tx + + # This is where the snap point should be on the grid. + snap_pt = pt.snap(GRID) + + # This is the required movement to get on-grid. + mv = snap_pt - pt + + # Update the object's transformation matrix. + snap_tx = Tx(dx=mv.x, dy=mv.y) + part_or_blk.tx *= snap_tx + + +def add_placement_bboxes(parts, **options): + """Expand part bounding boxes to include space for subsequent routing.""" + from skidl.schematics.net_terminal import NetTerminal + + for part in parts: + # Placement bbox starts off with the part bbox (including any net labels). + part.place_bbox = BBox() + part.place_bbox.add(part.lbl_bbox) + + # Compute the routing area for each side based on the number of pins on each side. + padding = {"U": 1, "D": 1, "L": 1, "R": 1} # Min padding of 1 channel per side. + for pin in part: + if pin.stub is False and pin.is_connected(): + padding[pin.orientation] += 1 + + # expansion_factor > 1 is used to expand the area for routing around each part, + # usually in response to a failed routing phase. But don't expand the routing + # around NetTerminals since those are just used to label wires. + if isinstance(part, NetTerminal): + expansion_factor = 1 + else: + expansion_factor = options.get("expansion_factor", 1.0) + + # Add padding for routing to the right and upper sides. + part.place_bbox.add( + part.place_bbox.max + + (Point(padding["L"], padding["D"]) * GRID * expansion_factor) + ) + + # Add padding for routing to the left and lower sides. + part.place_bbox.add( + part.place_bbox.min + - (Point(padding["R"], padding["U"]) * GRID * expansion_factor) + ) + + +def get_enclosing_bbox(parts): + """Return bounding box that encloses all the parts.""" + return BBox().add(*(part.place_bbox * part.tx for part in parts)) + + +def add_anchor_pull_pins(parts, nets, **options): + """Add positions of anchor and pull pins for attractive net forces between parts. + + Args: + part (list): List of movable parts. + nets (list): List of attractive nets between parts. + options (dict): Dict of options and values that enable/disable functions. + """ + + def add_place_pt(part, pin): + """Add the point for a pin on the placement boundary of a part.""" + + pin.route_pt = pin.pt # For drawing of nets during debugging. + pin.place_pt = Point(pin.pt.x, pin.pt.y) + if pin.orientation == "U": + pin.place_pt.y = part.place_bbox.min.y + elif pin.orientation == "D": + pin.place_pt.y = part.place_bbox.max.y + elif pin.orientation == "L": + pin.place_pt.x = part.place_bbox.max.x + elif pin.orientation == "R": + pin.place_pt.x = part.place_bbox.min.x + else: + raise RuntimeError("Unknown pin orientation.") + + # Remove any existing anchor and pull pins before making new ones. + rmv_attr(parts, ("anchor_pins", "pull_pins")) + + # Add dicts for anchor/pull pins and pin centroids to each movable part. + for part in parts: + part.anchor_pins = defaultdict(list) + part.pull_pins = defaultdict(list) + part.pin_ctrs = dict() + + if nets: + # If nets exist, then these parts are interconnected so + # assign pins on each net to part anchor and pull pin lists. + for net in nets: + # Get net pins that are on movable parts. + pins = {pin for pin in net.pins if pin.part in parts} + + # Get the set of parts with pins on the net. + net.parts = {pin.part for pin in pins} + + # Add each pin as an anchor on the part that contains it and + # as a pull pin on all the other parts that will be pulled by this part. + for pin in pins: + pin.part.anchor_pins[net].append(pin) + add_place_pt(pin.part, pin) + for part in net.parts - {pin.part}: + # NetTerminals are pulled towards connected parts, but + # those parts are not attracted towards NetTerminals. + if not is_net_terminal(pin.part): + part.pull_pins[net].append(pin) + + # For each net, assign the centroid of the part's anchor pins for that net. + for net in nets: + for part in net.parts: + if part.anchor_pins[net]: + part.pin_ctrs[net] = pt_sum( + pin.place_pt for pin in part.anchor_pins[net] + ) / len(part.anchor_pins[net]) + + else: + # There are no nets so these parts are floating freely. + # Floating parts are all pulled by each other. + all_pull_pins = [] + for part in parts: + try: + # Set anchor at top-most pin so floating part tops will align. + anchor_pull_pin = max(part.pins, key=lambda pin: pin.pt.y) + add_place_pt(part, anchor_pull_pin) + except ValueError: + # Set anchor for part with no pins at all. + anchor_pull_pin = Pin() + anchor_pull_pin.place_pt = part.place_bbox.max + part.anchor_pins["similarity"] = [anchor_pull_pin] + part.pull_pins["similarity"] = all_pull_pins + all_pull_pins.append(anchor_pull_pin) + + +def save_anchor_pull_pins(parts): + """Save anchor/pull pins for each part before they are changed.""" + for part in parts: + part.saved_anchor_pins = copy(part.anchor_pins) + part.saved_pull_pins = copy(part.pull_pins) + + +def restore_anchor_pull_pins(parts): + """Restore the original anchor/pull pin lists for each Part.""" + + for part in parts: + if hasattr(part, "saved_anchor_pins"): + # Saved pin lists exist, so restore them to the original anchor/pull pin lists. + part.anchor_pins = part.saved_anchor_pins + part.pull_pins = part.saved_pull_pins + + # Remove the attributes where the original lists were saved. + rmv_attr(parts, ("saved_anchor_pins", "saved_pull_pins")) + + +def adjust_orientations(parts, **options): + """Adjust orientation of parts. + + Args: + parts (list): List of Parts to adjust. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + bool: True if one or more part orientations were changed. Otherwise, False. + """ + + def find_best_orientation(part): + """Each part has 8 possible orientations. Find the best of the 7 alternatives from the starting one.""" + + # Store starting orientation. + part.prev_tx = copy(part.tx) + + # Get centerpoint of part for use when doing rotations/flips. + part_ctr = (part.place_bbox * part.tx).ctr + + # Now find the orientation that has the largest decrease (or smallest increase) in cost. + # Go through four rotations, then flip the part and go through the rotations again. + best_delta_cost = float("inf") + calc_starting_cost = True + for i in range(2): + for j in range(4): + + if calc_starting_cost: + # Calculate the cost of the starting orientation before any changes in orientation. + starting_cost = net_tension(part, **options) + # Skip the starting orientation but set flag to process the others. + calc_starting_cost = False + else: + # Calculate the cost of the current orientation. + delta_cost = net_tension(part, **options) - starting_cost + if delta_cost < best_delta_cost: + # Save the largest decrease in cost and the associated orientation. + best_delta_cost = delta_cost + best_tx = copy(part.tx) + + # Proceed to the next rotation. + part.tx = part.tx.move(-part_ctr).rot_90cw().move(part_ctr) + + # Flip the part and go through the rotations again. + part.tx = part.tx.move(-part_ctr).flip_x().move(part_ctr) + + # Save the largest decrease in cost and the associated orientation. + part.delta_cost = best_delta_cost + part.delta_cost_tx = best_tx + + # Restore the original orientation. + part.tx = part.prev_tx + + # Get the list of parts that don't have their orientations locked. + movable_parts = [part for part in parts if not part.orientation_locked] + + if not movable_parts: + # No movable parts, so exit without doing anything. + return + + # Kernighan-Lin algorithm for finding near-optimal part orientations. + # Because of the way the tension for part alignment is computed based on + # the nearest part, it is possible for an infinite loop to occur. + # Hence the ad-hoc loop limit. + for iter_cnt in range(10): + # Find the best part to move and move it until there are no more parts to move. + moved_parts = [] + unmoved_parts = movable_parts[:] + while unmoved_parts: + # Find the best current orientation for each unmoved part. + for part in unmoved_parts: + find_best_orientation(part) + + # Find the part that has the largest decrease in cost. + part_to_move = min(unmoved_parts, key=lambda p: p.delta_cost) + + # Reorient the part with the Tx that created the largest decrease in cost. + part_to_move.tx = part_to_move.delta_cost_tx + + # Transfer the part from the unmoved to the moved part list. + unmoved_parts.remove(part_to_move) + moved_parts.append(part_to_move) + + # Find the point at which the cost reaches its lowest point. + # delta_cost at location i is the change in cost *before* part i is moved. + # Start with cost change of zero before any parts are moved. + delta_costs = [0,] + delta_costs.extend((part.delta_cost for part in moved_parts)) + try: + cost_seq = list(itertools.accumulate(delta_costs)) + except AttributeError: + # Python 2.7 doesn't have itertools.accumulate(). + cost_seq = list(delta_costs) + for i in range(1, len(cost_seq)): + cost_seq[i] = cost_seq[i - 1] + cost_seq[i] + min_cost = min(cost_seq) + min_index = cost_seq.index(min_cost) + + # Move all the parts after that point back to their starting positions. + for part in moved_parts[min_index:]: + part.tx = part.prev_tx + + # Terminate the search if no part orientations were changed. + if min_index == 0: + break + + rmv_attr(parts, ("prev_tx", "delta_cost", "delta_cost_tx")) + + # Return True if one or more iterations were done, indicating part orientations were changed. + return iter_cnt > 0 + + +def net_tension_dist(part, **options): + """Calculate the tension of the nets trying to rotate/flip the part. + + Args: + part (Part): Part affected by forces from other connected parts. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + float: Total tension on the part. + """ + + # Compute the force for each net attached to the part. + tension = 0.0 + for net, anchor_pins in part.anchor_pins.items(): + pull_pins = part.pull_pins[net] + + if not anchor_pins or not pull_pins: + # Skip nets without pulling or anchor points. + continue + + # Compute the net force acting on each anchor point on the part. + for anchor_pin in anchor_pins: + # Compute the anchor point's (x,y). + anchor_pt = anchor_pin.place_pt * anchor_pin.part.tx + + # Find the dist from the anchor point to each pulling point. + dists = [ + (anchor_pt - pp.place_pt * pp.part.tx).magnitude for pp in pull_pins + ] + + # Only the closest pulling point affects the tension since that is + # probably where the wire routing will go to. + tension += min(dists) + + return tension + + +def net_torque_dist(part, **options): + """Calculate the torque of the nets trying to rotate/flip the part. + + Args: + part (Part): Part affected by forces from other connected parts. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + float: Total torque on the part. + """ + + # Part centroid for computing torque. + ctr = part.place_bbox.ctr * part.tx + + # Get the force multiplier applied to point-to-point nets. + pt_to_pt_mult = options.get("pt_to_pt_mult", 1) + + # Compute the torque for each net attached to the part. + torque = 0.0 + for net, anchor_pins in part.anchor_pins.items(): + pull_pins = part.pull_pins[net] + + if not anchor_pins or not pull_pins: + # Skip nets without pulling or anchor points. + continue + + pull_pin_pts = [pin.place_pt * pin.part.tx for pin in pull_pins] + + # Multiply the force exerted by point-to-point nets. + force_mult = pt_to_pt_mult if len(pull_pin_pts) <= 1 else 1 + + # Compute the net torque acting on each anchor point on the part. + for anchor_pin in anchor_pins: + # Compute the anchor point's (x,y). + anchor_pt = anchor_pin.place_pt * part.tx + + # Compute torque around part center from force between anchor & pull pins. + normalize = len(pull_pin_pts) + lever_norm = (anchor_pt - ctr).norm + for pull_pt in pull_pin_pts: + frc_norm = (pull_pt - anchor_pt).norm + torque += lever_norm.xprod(frc_norm) * force_mult / normalize + + return abs(torque) + + +# Select the net tension method used for the adjusting the orientation of parts. +net_tension = net_tension_dist +# net_tension = net_torque_dist + + +@export_to_all +def net_force_dist(part, **options): + """Compute attractive force on a part from all the other parts connected to it. + + Args: + part (Part): Part affected by forces from other connected parts. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Force upon given part. + """ + + # Get the anchor and pull pins for each net connected to this part. + anchor_pins = part.anchor_pins + pull_pins = part.pull_pins + + # Get the force multiplier applied to point-to-point nets. + pt_to_pt_mult = options.get("pt_to_pt_mult", 1) + + # Compute the total force on the part from all the anchor/pulling points on each net. + total_force = Vector(0, 0) + + # Parts with a lot of pins can accumulate large net forces that move them very quickly. + # Accumulate the number of individual net forces and use that to attenuate + # the total force, effectively normalizing the forces between large & small parts. + net_normalizer = 0 + + # Compute the force for each net attached to the part. + for net in anchor_pins.keys(): + if not anchor_pins[net] or not pull_pins[net]: + # Skip nets without pulling or anchor points. + continue + + # Multiply the force exerted by point-to-point nets. + force_mult = pt_to_pt_mult if len(pull_pins[net]) <= 1 else 1 + + # Initialize net force. + net_force = Vector(0, 0) + + pin_normalizer = 0 + + # Compute the anchor and pulling point (x,y)s for the net. + anchor_pts = [pin.place_pt * pin.part.tx for pin in anchor_pins[net]] + pull_pts = [pin.place_pt * pin.part.tx for pin in pull_pins[net]] + + # Compute the net force acting on each anchor point on the part. + for anchor_pt in anchor_pts: + # Sum the forces from each pulling point on the anchor point. + for pull_pt in pull_pts: + # Get the distance from the pull pt to the anchor point. + dist_vec = pull_pt - anchor_pt + + # Add the force on the anchor pin from the pulling pin. + net_force += dist_vec + + # Increment the normalizer for every pull force added to the net force. + pin_normalizer += 1 + + if options.get("pin_normalize"): + # Normalize the net force across all the anchor & pull pins. + pin_normalizer = pin_normalizer or 1 # Prevent div-by-zero. + net_force /= pin_normalizer + + # Accumulate force from this net into the total force on the part. + # Multiply force if the net meets stated criteria. + total_force += net_force * force_mult + + # Increment the normalizer for every net force added to the total force. + net_normalizer += 1 + + if options.get("net_normalize"): + # Normalize the total force across all the nets. + net_normalizer = net_normalizer or 1 # Prevent div-by-zero. + total_force /= net_normalizer + + return total_force + + +# Select the net force method used for the attraction of parts during placement. +attractive_force = net_force_dist + + +@export_to_all +def overlap_force(part, parts, **options): + """Compute the repulsive force on a part from overlapping other parts. + + Args: + part (Part): Part affected by forces from other overlapping parts. + parts (list): List of parts to check for overlaps. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Force upon given part. + """ + + # Bounding box of given part. + part_bbox = part.place_bbox * part.tx + + # Compute the overlap force of the bbox of this part with every other part. + total_force = Vector(0, 0) + for other_part in set(parts) - {part}: + other_part_bbox = other_part.place_bbox * other_part.tx + + # No force unless parts overlap. + if part_bbox.intersects(other_part_bbox): + # Compute the movement needed to separate the bboxes in left/right/up/down directions. + # Add some small random offset to break symmetry when parts exactly overlay each other. + # Move right edge of part to the left of other part's left edge, etc... + moves = [] + rnd = Vector(random.random()-0.5, random.random()-0.5) + for edges, dir in ((("ll", "lr"), Vector(1,0)), (("ul", "ll"), Vector(0,1))): + move = (getattr(other_part_bbox, edges[0]) - getattr(part_bbox, edges[1]) - rnd) * dir + moves.append([move.magnitude, move]) + # Flip edges... + move = (getattr(other_part_bbox, edges[1]) - getattr(part_bbox, edges[0]) - rnd) * dir + moves.append([move.magnitude, move]) + + # Select the smallest move that separates the parts. + move = min(moves, key=lambda m: m[0]) + + # Add the move to the total force on the part. + total_force += move[1] + + return total_force + + +@export_to_all +def overlap_force_rand(part, parts, **options): + """Compute the repulsive force on a part from overlapping other parts. + + Args: + part (Part): Part affected by forces from other overlapping parts. + parts (list): List of parts to check for overlaps. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Force upon given part. + """ + + # Bounding box of given part. + part_bbox = part.place_bbox * part.tx + + # Compute the overlap force of the bbox of this part with every other part. + total_force = Vector(0, 0) + for other_part in set(parts) - {part}: + other_part_bbox = other_part.place_bbox * other_part.tx + + # No force unless parts overlap. + if part_bbox.intersects(other_part_bbox): + # Compute the movement needed to clear the bboxes in left/right/up/down directions. + # Add some small random offset to break symmetry when parts exactly overlay each other. + # Move right edge of part to the left of other part's left edge. + moves = [] + rnd = Vector(random.random()-0.5, random.random()-0.5) + for edges, dir in ((("ll", "lr"), Vector(1,0)), (("lr", "ll"), Vector(1,0)), + (("ul", "ll"), Vector(0,1)), (("ll", "ul"), Vector(0,1))): + move = (getattr(other_part_bbox, edges[0]) - getattr(part_bbox, edges[1]) - rnd) * dir + moves.append([move.magnitude, move]) + accum = 0 + for move in moves: + accum += move[0] + for move in moves: + move[0] = accum - move[0] + new_accum = 0 + for move in moves: + move[0] += new_accum + new_accum = move[0] + select = new_accum * random.random() + for move in moves: + if move[0] >= select: + total_force += move[1] + break + + return total_force + + +# Select the overlap force method used for the repulsion of parts during placement. +repulsive_force = overlap_force +# repulsive_force = overlap_force_rand + + +def scale_attractive_repulsive_forces(parts, force_func, **options): + """Set scaling between attractive net forces and repulsive part overlap forces.""" + + # Store original part placement. + for part in parts: + part.original_tx = copy(part.tx) + + # Find attractive forces when they are maximized by random part placement. + random_placement(parts, **options) + attractive_forces_sum = sum( + force_func(p, parts, alpha=0, scale=1, **options).magnitude for p in parts + ) + + # Find repulsive forces when they are maximized by compacted part placement. + central_placement(parts, **options) + repulsive_forces_sum = sum( + force_func(p, parts, alpha=1, scale=1, **options).magnitude for p in parts + ) + + # Restore original part placement. + for part in parts: + part.tx = part.original_tx + rmv_attr(parts, ["original_tx"]) + + # Return scaling factor that makes attractive forces about the same as repulsive forces. + try: + return repulsive_forces_sum / attractive_forces_sum + except ZeroDivisionError: + # No attractive forces, so who cares about scaling? Set it to 1. + return 1 + + +def total_part_force(part, parts, scale, alpha, **options): + """Compute the total of the attractive net and repulsive overlap forces on a part. + + Args: + part (Part): Part affected by forces from other overlapping parts. + parts (list): List of parts to check for overlaps. + scale (float): Scaling factor for net forces to make them equivalent to overlap forces. + alpha (float): Fraction of the total that is the overlap force (range [0,1]). + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Weighted total of net attractive and overlap repulsion forces. + """ + force = scale * (1 - alpha) * attractive_force( + part, **options + ) + alpha * repulsive_force(part, parts, **options) + part.force = force # For debug drawing. + return force + + +def similarity_force(part, parts, similarity, **options): + """Compute attractive force on a part from all the other parts connected to it. + + Args: + part (Part): Part affected by similarity forces with other parts. + similarity (dict): Similarity score for any pair of parts used as keys. + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Force upon given part. + """ + + # Get the single anchor point for similarity forces affecting this part. + anchor_pt = part.anchor_pins["similarity"][0].place_pt * part.tx + + # Compute the combined force of all the similarity pulling points. + total_force = Vector(0, 0) + for pull_pin in part.pull_pins["similarity"]: + pull_pt = pull_pin.place_pt * pull_pin.part.tx + # Force from pulling to anchor point is proportional to part similarity and distance. + total_force += (pull_pt - anchor_pt) * similarity[part][pull_pin.part] + + return total_force + + +def total_similarity_force(part, parts, similarity, scale, alpha, **options): + """Compute the total of the attractive similarity and repulsive overlap forces on a part. + + Args: + part (Part): Part affected by forces from other overlapping parts. + parts (list): List of parts to check for overlaps. + similarity (dict): Similarity score for any pair of parts used as keys. + scale (float): Scaling factor for similarity forces to make them equivalent to overlap forces. + alpha (float): Proportion of the total that is the overlap force (range [0,1]). + options (dict): Dict of options and values that enable/disable functions. + + Returns: + Vector: Weighted total of net attractive and overlap repulsion forces. + """ + force = scale * (1 - alpha) * similarity_force( + part, parts, similarity, **options + ) + alpha * repulsive_force(part, parts, **options) + part.force = force # For debug drawing. + return force + + +def define_placement_bbox(parts, **options): + """Return a bounding box big enough to hold the parts being placed.""" + + # Compute appropriate size to hold the parts based on their areas. + area = 0 + for part in parts: + area += part.place_bbox.area + side = 3 * math.sqrt(area) # HACK: Multiplier is ad-hoc. + return BBox(Point(0, 0), Point(side, side)) + + +def central_placement(parts, **options): + """Cluster all part centroids onto a common point. + + Args: + parts (list): List of Parts. + options (dict): Dict of options and values that enable/disable functions. + """ + + if len(parts) <= 1: + # No need to do placement if there's less than two parts. + return + + # Find the centroid of all the parts. + ctr = get_enclosing_bbox(parts).ctr + + # Collapse all the parts to the centroid. + for part in parts: + mv = ctr - part.place_bbox.ctr * part.tx + part.tx *= Tx(dx=mv.x, dy=mv.y) + + +def random_placement(parts, **options): + """Randomly place parts within an appropriately-sized area. + + Args: + parts (list): List of Parts to place. + """ + + # Compute appropriate size to hold the parts based on their areas. + bbox = define_placement_bbox(parts, **options) + + # Place parts randomly within area. + for part in parts: + pt = Point(random.random() * bbox.w, random.random() * bbox.h) + part.tx = part.tx.move(pt) + + +def push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options): + """Move parts under influence of attractive nets and repulsive part overlaps. + + Args: + anchored_parts (list): Set of immobile Parts whose position affects placement. + mobile_parts (list): Set of Parts that can be moved. + nets (list): List of nets that interconnect parts. + force_func: Function for calculating forces between parts. + options (dict): Dict of options and values that enable/disable functions. + """ + + if not options.get("use_push_pull"): + # Abort if push & pull of parts is disabled. + return + + if not mobile_parts: + # No need to do placement if there's nothing to move. + return + + def cost(parts, alpha): + """Cost function for use in debugging. Should decrease as parts move.""" + for part in parts: + part.force = force_func(part, parts, scale=scale, alpha=alpha, **options) + return sum((part.force.magnitude for part in parts)) + + # Get PyGame screen, real-to-screen coord Tx matrix, font for debug drawing. + scr = options.get("draw_scr") + tx = options.get("draw_tx") + font = options.get("draw_font") + txt_org = Point(10, 10) + + # Create the total set of parts exerting forces on each other. + parts = anchored_parts + mobile_parts + + # If there are no anchored parts, then compute the overall drift force + # across all the parts. This will be subtracted so the + # entire group of parts doesn't just continually drift off in one direction. + # This only needs to be done if ALL parts are mobile (i.e., no anchored parts). + rmv_drift = not anchored_parts + + # Set scale factor between attractive net forces and repulsive part overlap forces. + scale = scale_attractive_repulsive_forces(parts, force_func, **options) + + # Setup the schedule for adjusting the alpha coefficient that weights the + # combination of the attractive net forces and the repulsive part overlap forces. + # Start at 0 (all attractive) and gradually progress to 1 (all repulsive). + # Also, set parameters for determining when parts are stable and for restricting + # movements in the X & Y directions when parts are being aligned. + force_schedule = [ + (0.50, 0.0, 0.1, False, (1, 1)), # Attractive forces only. + (0.25, 0.0, 0.01, False, (1, 1)), # Attractive forces only. + # (0.25, 0.2, 0.01, False, (1,1)), # Some repulsive forces. + (0.25, 0.4, 0.1, False, (1, 1)), # More repulsive forces. + # (0.25, 0.6, 0.01, False, (1,1)), # More repulsive forces. + (0.25, 0.8, 0.1, False, (1, 1)), # More repulsive forces. + # (0.25, 0.7, 0.01, True, (1,0)), # Align parts horiz. + # (0.25, 0.7, 0.01, True, (0,1)), # Align parts vert. + # (0.25, 0.7, 0.01, True, (1,0)), # Align parts horiz. + # (0.25, 0.7, 0.01, True, (0,1)), # Align parts vert. + (0.25, 1.0, 0.01, False, (1, 1)), # Remove any part overlaps. + ] + # N = 7 + # force_schedule = [(0.50, i/N, 0.01, False, (1,1)) for i in range(N+1)] + + # Step through the alpha sequence going from all-attractive to all-repulsive forces. + for speed, alpha, stability_coef, align_parts, force_mask in force_schedule: + if align_parts: + # Align parts by only using forces between the closest anchor/pull pins. + retain_closest_anchor_pull_pins(mobile_parts) + else: + # For general placement, use forces between all anchor/pull pins. + restore_anchor_pull_pins(mobile_parts) + + # This stores the threshold below which all the parts are assumed to be stabilized. + # Since it can never be negative, set it to -1 to indicate it's uninitialized. + stable_threshold = -1 + + # Move parts for this alpha until they all settle into fixed positions. + # Place an iteration limit to prevent an infinite loop. + for _ in range(1000): # HACK: Ad-hoc iteration limit. + # Compute forces exerted on the parts by each other. + sum_of_forces = 0 + for part in mobile_parts: + part.force = force_func( + part, parts, scale=scale, alpha=alpha, **options + ) + # Mask X or Y component of force during part alignment. + part.force = part.force.mask(force_mask) + sum_of_forces += part.force.magnitude + + if rmv_drift: + # Calculate the drift force across all parts and subtract it from each part + # to prevent them from continually drifting in one direction. + drift_force = force_sum([part.force for part in mobile_parts]) / len( + mobile_parts + ) + for part in mobile_parts: + part.force -= drift_force + + # Apply movements to part positions. + for part in mobile_parts: + part.mv = part.force * speed + part.tx *= Tx(dx=part.mv.x, dy=part.mv.y) + + # Keep iterating until all the parts are still. + if stable_threshold < 0: + # Set the threshold after the first iteration. + initial_sum_of_forces = sum_of_forces + stable_threshold = sum_of_forces * stability_coef + elif sum_of_forces <= stable_threshold: + # Part positions have stabilized if forces have dropped below threshold. + break + elif sum_of_forces > 10 * initial_sum_of_forces: + # If the forces are getting higher, then that usually means the parts are + # spreading out. This can happen if speed is too large, so reduce it so + # the forces may start to decrease. + speed *= 0.50 + + if scr: + # Draw current part placement for debugging purposes. + draw_placement(parts, nets, scr, tx, font) + draw_text( + "alpha:{alpha:3.2f} iter:{_} force:{sum_of_forces:.1f} stable:{stable_threshold}".format( + **locals() + ), + txt_org, + scr, + tx, + font, + color=(0, 0, 0), + real=False, + ) + draw_redraw() + + +def evolve_placement(anchored_parts, mobile_parts, nets, force_func, **options): + """Evolve part placement looking for optimum using force function. + + Args: + anchored_parts (list): Set of immobile Parts whose position affects placement. + mobile_parts (list): Set of Parts that can be moved. + nets (list): List of nets that interconnect parts. + force_func (function): Computes the force affecting part positions. + options (dict): Dict of options and values that enable/disable functions. + """ + + parts = anchored_parts + mobile_parts + + # Force-directed placement. + push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options) + + # Snap parts to grid. + for part in parts: + snap_to_grid(part) + + +def place_net_terminals(net_terminals, placed_parts, nets, force_func, **options): + """Place net terminals around already-placed parts. + + Args: + net_terminals (list): List of NetTerminals + placed_parts (list): List of placed Parts. + nets (list): List of nets that interconnect parts. + force_func (function): Computes the force affecting part positions. + options (dict): Dict of options and values that enable/disable functions. + """ + + def trim_pull_pins(terminals, bbox): + """Trim pullpins of NetTerminals to the part pins closest to an edge of the bounding box of placed parts. + + Args: + terminals (list): List of NetTerminals. + bbox (BBox): Bounding box of already-placed parts. + + Note: + The rationale for this is that pin closest to an edge of the bounding box will be easier to access. + """ + + for terminal in terminals: + for net, pull_pins in terminal.pull_pins.items(): + insets = [] + for pull_pin in pull_pins: + pull_pt = pull_pin.place_pt * pull_pin.part.tx + + # Get the inset of the terminal pulling pin from each side of the placement area. + # Left side. + insets.append((abs(pull_pt.x - bbox.ll.x), pull_pin)) + # Right side. + insets.append((abs(pull_pt.x - bbox.lr.x), pull_pin)) + # Top side. + insets.append((abs(pull_pt.y - bbox.ul.y), pull_pin)) + # Bottom side. + insets.append((abs(pull_pt.y - bbox.ll.y), pull_pin)) + + # Retain only the pulling pin closest to an edge of the bounding box (i.e., minimum inset). + terminal.pull_pins[net] = [min(insets, key=lambda off: off[0])[1]] + + def orient(terminals, bbox): + """Set orientation of NetTerminals to point away from closest bounding box edge. + + Args: + terminals (list): List of NetTerminals. + bbox (BBox): Bounding box of already-placed parts. + """ + + for terminal in terminals: + # A NetTerminal should already be trimmed so it is attached to a single pin of a part on a single net. + pull_pin = list(terminal.pull_pins.values())[0][0] + pull_pt = pull_pin.place_pt * pull_pin.part.tx + + # Get the inset of the terminal pulling pin from each side of the placement area + # and the Tx() that should be applied if the terminal is placed on that side. + insets = [] + # Left side, so terminal label juts out to the left. + insets.append((abs(pull_pt.x - bbox.ll.x), Tx())) + # Right side, so terminal label flipped to jut out to the right. + insets.append((abs(pull_pt.x - bbox.lr.x), Tx().flip_x())) + # Top side, so terminal label rotated by 270 to jut out to the top. + insets.append((abs(pull_pt.y - bbox.ul.y), Tx().rot_90cw().rot_90cw().rot_90cw())) + # Bottom side. so terminal label rotated 90 to jut out to the bottom. + insets.append((abs(pull_pt.y - bbox.ll.y), Tx().rot_90cw())) + + # Apply the Tx() for the side the terminal is closest to. + terminal.tx = min(insets, key=lambda inset: inset[0])[1] + + def move_to_pull_pin(terminals): + """Move NetTerminals immediately to their pulling pins.""" + for terminal in terminals: + anchor_pin = list(terminal.anchor_pins.values())[0][0] + anchor_pt = anchor_pin.place_pt * anchor_pin.part.tx + pull_pin = list(terminal.pull_pins.values())[0][0] + pull_pt = pull_pin.place_pt * pull_pin.part.tx + terminal.tx = terminal.tx.move(pull_pt - anchor_pt) + + def evolution(net_terminals, placed_parts, bbox): + """Evolve placement of NetTerminals starting from outermost from center to innermost.""" + + evolution_type = options.get("terminal_evolution", "all_at_once") + + if evolution_type == "all_at_once": + evolve_placement( + placed_parts, net_terminals, nets, total_part_force, **options + ) + + elif evolution_type == "outer_to_inner": + # Start off with the previously-placed parts as anchored parts. NetTerminals will be added to this as they are placed. + anchored_parts = copy(placed_parts) + + # Sort terminals from outermost to innermost w.r.t. the center. + def dist_to_bbox_edge(term): + pt = term.pins[0].place_pt * term.tx + return min(( + abs(pt.x - bbox.ll.x), + abs(pt.x - bbox.lr.x), + abs(pt.y - bbox.ll.y), + abs(pt.y - bbox.ul.y)) + ) + + terminals = sorted( + net_terminals, + key=lambda term: dist_to_bbox_edge(term), + reverse=True, + ) + + # Grab terminals starting from the outside and work towards the inside until a terminal intersects a previous one. + mobile_terminals = [] + mobile_bboxes = [] + for terminal in terminals: + terminal_bbox = terminal.place_bbox * terminal.tx + mobile_terminals.append(terminal) + mobile_bboxes.append(terminal_bbox) + for bbox in mobile_bboxes[:-1]: + if terminal_bbox.intersects(bbox): + # The current NetTerminal intersects one of the previously-selected mobile terminals, so evolve the + # placement of all the mobile terminals except the current one. + evolve_placement( + anchored_parts, + mobile_terminals[:-1], + nets, + force_func, + **options + ) + # Anchor the mobile terminals after their placement is done. + anchored_parts.extend(mobile_terminals[:-1]) + # Remove the placed terminals, leaving only the current terminal. + mobile_terminals = mobile_terminals[-1:] + mobile_bboxes = mobile_bboxes[-1:] + + if mobile_terminals: + # Evolve placement of any remaining terminals. + evolve_placement( + anchored_parts, mobile_terminals, nets, total_part_force, **options + ) + + bbox = get_enclosing_bbox(placed_parts) + save_anchor_pull_pins(net_terminals) + trim_pull_pins(net_terminals, bbox) + orient(net_terminals, bbox) + move_to_pull_pin(net_terminals) + evolution(net_terminals, placed_parts, bbox) + restore_anchor_pull_pins(net_terminals) + + +@export_to_all +class Placer: + """Mixin to add place function to Node class.""" + + def group_parts(node, **options): + """Group parts in the Node that are connected by internal nets + + Args: + node (Node): Node with parts. + options (dict, optional): Dictionary of options and values. Defaults to {}. + + Returns: + list: List of lists of Parts that are connected. + list: List of internal nets connecting parts. + list: List of Parts that are not connected to anything (floating). + """ + + if not node.parts: + return [], [], [] + + # Extract list of nets having at least one pin in the node. + internal_nets = node.get_internal_nets() + + # Group all the parts that have some interconnection to each other. + # Start with groups of parts on each individual net. + connected_parts = [ + set(pin.part for pin in net.pins if pin.part in node.parts) + for net in internal_nets + ] + + # Now join groups that have parts in common. + for i in range(len(connected_parts) - 1): + group1 = connected_parts[i] + for j in range(i + 1, len(connected_parts)): + group2 = connected_parts[j] + if group1 & group2: + # If part groups intersect, collect union of parts into one group + # and empty-out the other. + connected_parts[j] = connected_parts[i] | connected_parts[j] + connected_parts[i] = set() + # No need to check against group1 any more since it has been + # unioned into group2 that will be checked later in the loop. + break + + # Remove any empty groups that were unioned into other groups. + connected_parts = [group for group in connected_parts if group] + + # Find parts that aren't connected to anything. + floating_parts = set(node.parts) - set(itertools.chain(*connected_parts)) + + return connected_parts, internal_nets, floating_parts + + def place_connected_parts(node, parts, nets, **options): + """Place individual parts. + + Args: + node (Node): Node with parts. + parts (list): List of Part sets connected by nets. + nets (list): List of internal Nets connecting the parts. + options (dict): Dict of options and values that enable/disable functions. + """ + + if not parts: + # Abort if nothing to place. + return + + # Add bboxes with surrounding area so parts are not butted against each other. + add_placement_bboxes(parts, **options) + + # Set anchor and pull pins that determine attractive forces between parts. + add_anchor_pull_pins(parts, nets, **options) + + # Randomly place connected parts. + random_placement(parts) + + if options.get("draw_placement"): + # Draw the placement for debug purposes. + bbox = get_enclosing_bbox(parts) + draw_scr, draw_tx, draw_font = draw_start(bbox) + options.update( + {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} + ) + + if options.get("compress_before_place"): + central_placement(parts, **options) + + # Do force-directed placement of the parts in the parts. + + # Separate the NetTerminals from the other parts. + net_terminals = [part for part in parts if is_net_terminal(part)] + real_parts = [part for part in parts if not is_net_terminal(part)] + + # Do the first trial placement. + evolve_placement([], real_parts, nets, total_part_force, **options) + + if options.get("rotate_parts"): + # Adjust part orientations after first trial placement is done. + if adjust_orientations(real_parts, **options): + # Some part orientations were changed, so re-do placement. + evolve_placement([], real_parts, nets, total_part_force, **options) + + # Place NetTerminals after all the other parts. + place_net_terminals( + net_terminals, real_parts, nets, total_part_force, **options + ) + + if options.get("draw_placement"): + # Pause to look at placement for debugging purposes. + draw_pause() + + def place_floating_parts(node, parts, **options): + """Place individual parts. + + Args: + node (Node): Node with parts. + parts (list): List of Parts not connected by explicit nets. + options (dict): Dict of options and values that enable/disable functions. + """ + + if not parts: + # Abort if nothing to place. + return + + # Add bboxes with surrounding area so parts are not butted against each other. + add_placement_bboxes(parts) + + # Set anchor and pull pins that determine attractive forces between similar parts. + add_anchor_pull_pins(parts, [], **options) + + # Randomly place the floating parts. + random_placement(parts) + + if options.get("draw_placement"): + # Compute the drawing area for the floating parts + bbox = get_enclosing_bbox(parts) + draw_scr, draw_tx, draw_font = draw_start(bbox) + options.update( + {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} + ) + + # For non-connected parts, do placement based on their similarity to each other. + part_similarity = defaultdict(lambda: defaultdict(lambda: 0)) + for part in parts: + for other_part in parts: + # Don't compute similarity of a part to itself. + if other_part is part: + continue + + # HACK: Get similarity forces right-sized. + part_similarity[part][other_part] = part.similarity(other_part) / 100 + # part_similarity[part][other_part] = 0.1 + + # Select the top-most pin in each part as the anchor point for force-directed placement. + # tx = part.tx + # part.anchor_pin = max(part.anchor_pins, key=lambda pin: (pin.place_pt * tx).y) + + force_func = functools.partial( + total_similarity_force, similarity=part_similarity + ) + + if options.get("compress_before_place"): + # Compress all floating parts together. + central_placement(parts, **options) + + # Do force-directed placement of the parts in the group. + evolve_placement([], parts, [], force_func, **options) + + if options.get("draw_placement"): + # Pause to look at placement for debugging purposes. + draw_pause() + + def place_blocks(node, connected_parts, floating_parts, children, **options): + """Place blocks of parts and hierarchical sheets. + + Args: + node (Node): Node with parts. + connected_parts (list): List of Part sets connected by nets. + floating_parts (set): Set of Parts not connected by any of the internal nets. + children (list): Child nodes in the hierarchy. + non_sheets (list): Hierarchical set of Parts that are visible. + sheets (list): List of hierarchical blocks. + options (dict): Dict of options and values that enable/disable functions. + """ + + # Global dict of pull pins for all blocks as they each pull on each other the same way. + block_pull_pins = defaultdict(list) + + # Class for movable groups of parts/child nodes. + class PartBlock: + def __init__(self, src, bbox, anchor_pt, snap_pt, tag): + self.src = src # Source for this block. + self.place_bbox = bbox # FIXME: Is this needed if place_bbox includes room for routing? + + # Create anchor pin to which forces are applied to this block. + anchor_pin = Pin() + anchor_pin.part = self + anchor_pin.place_pt = anchor_pt + + # This block has only a single anchor pin, but it needs to be in a list + # in a dict so it can be processed by the part placement functions. + self.anchor_pins = dict() + self.anchor_pins["similarity"] = [anchor_pin] + + # Anchor pin for this block is also a pulling pin for all other blocks. + block_pull_pins["similarity"].append(anchor_pin) + + # All blocks have the same set of pulling pins because they all pull each other. + self.pull_pins = block_pull_pins + + self.snap_pt = snap_pt # For snapping to grid. + self.tx = Tx() # For placement. + self.ref = "REF" # Name for block in debug drawing. + self.tag = tag # FIXME: what is this for? + + # Create a list of blocks from the groups of interconnected parts and the group of floating parts. + part_blocks = [] + for part_list in connected_parts + [floating_parts]: + if not part_list: + # No parts in this list for some reason... + continue + + # Find snapping point and bounding box for this group of parts. + snap_pt = None + bbox = BBox() + for part in part_list: + bbox.add(part.lbl_bbox * part.tx) + if not snap_pt: + # Use the first snapping point of a part you can find. + snap_pt = get_snap_pt(part) + + # Tag indicates the type of part block. + tag = 2 if (part_list is floating_parts) else 1 + + # pad the bounding box so part blocks don't butt-up against each other. + pad = BLK_EXT_PAD + bbox = bbox.resize(Vector(pad, pad)) + + # Create the part block and place it on the list. + part_blocks.append(PartBlock(part_list, bbox, bbox.ctr, snap_pt, tag)) + + # Add part blocks for child nodes. + for child in children: + # Calculate bounding box of child node. + bbox = child.calc_bbox() + + # Set padding for separating bounding box from others. + if child.flattened: + # This is a flattened node so the parts will be shown. + # Set the padding to include a pad between the parts and the + # graphical box that contains them, plus the padding around + # the outside of the graphical box. + pad = BLK_INT_PAD + BLK_EXT_PAD + else: + # This is an unflattened child node showing no parts on the inside + # so just pad around the outside of its graphical box. + pad = BLK_EXT_PAD + bbox = bbox.resize(Vector(pad, pad)) + + # Set the grid snapping point and tag for this child node. + snap_pt = child.get_snap_pt() + tag = 3 # Standard child node. + if not snap_pt: + # No snap point found, so just use the center of the bounding box. + snap_pt = bbox.ctr + tag = 4 # A child node with no snapping point. + + # Create the child block and place it on the list. + part_blocks.append(PartBlock(child, bbox, bbox.ctr, snap_pt, tag)) + + # Get ordered list of all block tags. Use this list to tell if tags are + # adjacent since there may be missing tags if a particular type of block + # isn't present. + tags = sorted({blk.tag for blk in part_blocks}) + + # Tie the blocks together with strong links between blocks with the same tag, + # and weaker links between blocks with adjacent tags. This ties similar + # blocks together into "super blocks" and ties the super blocks into a linear + # arrangement (1 -> 2 -> 3 ->...). + blk_attr = defaultdict(lambda: defaultdict(lambda: 0)) + for blk in part_blocks: + for other_blk in part_blocks: + if blk is other_blk: + # No attraction between a block and itself. + continue + if blk.tag == other_blk.tag: + # Large attraction between blocks of same type. + blk_attr[blk][other_blk] = 1 + elif abs(tags.index(blk.tag) - tags.index(other_blk.tag)) == 1: + # Some attraction between blocks of adjacent types. + blk_attr[blk][other_blk] = 0.1 + else: + # Otherwise, no attraction between these blocks. + blk_attr[blk][other_blk] = 0 + + if not part_blocks: + # Abort if nothing to place. + return + + # Start off with a random placement of part blocks. + random_placement(part_blocks) + + if options.get("draw_placement"): + # Setup to draw the part block placement for debug purposes. + bbox = get_enclosing_bbox(part_blocks) + draw_scr, draw_tx, draw_font = draw_start(bbox) + options.update( + {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} + ) + + # Arrange the part blocks with similarity force-directed placement. + force_func = functools.partial(total_similarity_force, similarity=blk_attr) + evolve_placement([], part_blocks, [], force_func, **options) + + if options.get("draw_placement"): + # Pause to look at placement for debugging purposes. + draw_pause() + + # Apply the placement moves of the part blocks to their underlying sources. + for blk in part_blocks: + try: + # Update the Tx matrix of the source (usually a child node). + blk.src.tx = blk.tx + except AttributeError: + # The source doesn't have a Tx so it must be a collection of parts. + # Apply the block placement to the Tx of each part. + for part in blk.src: + part.tx *= blk.tx + + def get_attrs(node): + """Return dict of attribute sets for the parts, pins, and nets in a node.""" + attrs = {"parts": set(), "pins": set(), "nets": set()} + for part in node.parts: + attrs["parts"].update(set(dir(part))) + for pin in part.pins: + attrs["pins"].update(set(dir(pin))) + for net in node.get_internal_nets(): + attrs["nets"].update(set(dir(net))) + return attrs + + def show_added_attrs(node): + """Show attributes that were added to parts, pins, and nets in a node.""" + current_attrs = node.get_attrs() + for key in current_attrs.keys(): + print( + "added {} attrs: {}".format(key, current_attrs[key] - node.attrs[key]) + ) + + def rmv_placement_stuff(node): + """Remove attributes added to parts, pins, and nets of a node during the placement phase.""" + + for part in node.parts: + rmv_attr(part.pins, ("route_pt", "place_pt")) + rmv_attr( + node.parts, + ("anchor_pins", "pull_pins", "pin_ctrs", "force", "mv"), + ) + rmv_attr(node.get_internal_nets(), ("parts",)) + + def place(node, tool=None, **options): + """Place the parts and children in this node. + + Args: + node (Node): Hierarchical node containing the parts and children to be placed. + tool (str): Backend tool for schematics. + options (dict): Dictionary of options and values to control placement. + """ + + # Inject the constants for the backend tool into this module. + import skidl + from skidl.tools import tool_modules + + tool = tool or skidl.config.tool + this_module = sys.modules[__name__] + this_module.__dict__.update(tool_modules[tool].constants.__dict__) + + random.seed(options.get("seed")) + + # Store the starting attributes of the node's parts, pins, and nets. + node.attrs = node.get_attrs() + + try: + # First, recursively place children of this node. + # TODO: Child nodes are independent, so can they be processed in parallel? + for child in node.children.values(): + child.place(tool=tool, **options) + + # Group parts into those that are connected by explicit nets and + # those that float freely connected only by stub nets. + connected_parts, internal_nets, floating_parts = node.group_parts(**options) + + # Place each group of connected parts. + for group in connected_parts: + node.place_connected_parts(list(group), internal_nets, **options) + + # Place the floating parts that have no connections to anything else. + node.place_floating_parts(list(floating_parts), **options) + + # Now arrange all the blocks of placed parts and the child nodes within this node. + node.place_blocks( + connected_parts, floating_parts, node.children.values(), **options + ) + + # Remove any stuff leftover from this place & route run. + # print(f"added part attrs = {new_part_attrs}") + node.rmv_placement_stuff() + # node.show_added_attrs() + + # Calculate the bounding box for the node after placement of parts and children. + node.calc_bbox() + + except PlacementFailure: + node.rmv_placement_stuff() + raise PlacementFailure + + def get_snap_pt(node): + """Get a Point to use for snapping the node to the grid. + + Args: + node (Node): The Node to which the snapping point applies. + + Returns: + Point: The snapping point or None. + """ + + if node.flattened: + # Look for a snapping point based on one of its parts. + for part in node.parts: + snap_pt = get_snap_pt(part) + if snap_pt: + return snap_pt + + # If no part snapping point, look for one in its children. + for child in node.children.values(): + if child.flattened: + snap_pt = child.get_snap_pt() + if snap_pt: + # Apply the child transformation to its snapping point. + return snap_pt * child.tx + + # No snapping point if node is not flattened or no parts in it or its children. + return None diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py new file mode 100644 index 00000000..760beb49 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -0,0 +1,3238 @@ +# -*- coding: utf-8 -*- + +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Autorouter for generating wiring between symbols in a schematic. +""" + +import copy +import random +import sys +from collections import Counter, defaultdict +from enum import Enum +from itertools import chain, zip_longest + +from skidl import Part +from skidl.utilities import export_to_all, rmv_attr +from .debug_draw import draw_end, draw_endpoint, draw_routing, draw_seg, draw_start +from .geometry import BBox, Point, Segment, Tx, Vector, tx_rot_90 + + +__all__ = ["RoutingFailure", "GlobalRoutingFailure", "SwitchboxRoutingFailure"] + + +################################################################### +# +# OVERVIEW OF SCHEMATIC AUTOROUTER +# +# The input is a Node containing child nodes and parts, each with a +# bounding box and an assigned (x,y) position. The following operations +# are done for each child node, and then for the parts within this node. +# +# The edges of each part bbox are extended to form tracks that divide the +# routing area into a set of four-sided, non-overlapping switchboxes. Each +# side of a switchbox is a Face, and each Face is a member of two adjoining +# switchboxes (except those Faces on the boundary of the total +# routing area.) Each face is adjacent to the six other faces of +# the two switchboxes it is part of. +# +# Each face has a capacity that indicates the number of wires that can +# cross through it. The capacity is the length of the face divided by the +# routing grid. (Faces on a part boundary have zero capacity to prevent +# routing from entering a part.) +# +# Each face on a part bbox is assigned terminals associated with the I/O +# pins of that symbol. +# +# After creating the faces and terminals, the global routing phase creates +# wires that connect the part pins on the nets. Each wire passes from +# a face of a switchbox to one of the other three faces, either directly +# across the switchbox to the opposite face or changing direction to +# either of the right-angle faces. The global router is basically a maze +# router that uses the switchboxes as high-level grid squares. +# +# After global routing, each net has a sequence of switchbox faces +# through which it will transit. The exact coordinate that each net +# enters a face is then assigned to create a Terminal. +# +# At this point there are a set of switchboxes which have fixed terminals located +# along their four faces. A greedy switchbox router +# (https://doi.org/10.1016/0167-9260(85)90029-X) +# does the detailed routing within each switchbox. +# +# The detailed wiring within all the switchboxes is combined and output +# as the total wiring for the parts in the Node. +# +################################################################### + + +# Orientations and directions. +class Orientation(Enum): + HORZ = 1 + VERT = 2 + + +class Direction(Enum): + LEFT = 3 + RIGHT = 4 + + +# Put the orientation/direction enums in global space to make using them easier. +for orientation in Orientation: + globals()[orientation.name] = orientation.value +for direction in Direction: + globals()[direction.name] = direction.value + + +# Dictionary for storing colors to visually distinguish routed nets. +net_colors = defaultdict( + lambda: (random.randint(0, 200), random.randint(0, 200), random.randint(0, 200)) +) + + +class NoSwitchBox(Exception): + """Exception raised when a switchbox cannot be generated.""" + + pass + + +class TerminalClashException(Exception): + """Exception raised when trying to place two terminals at the same coord on a Face.""" + + pass + + +class RoutingFailure(Exception): + """Exception raised when a net connecting pins cannot be routed.""" + + pass + + +class GlobalRoutingFailure(RoutingFailure): + """Failure during global routing phase.""" + + pass + + +class SwitchboxRoutingFailure(RoutingFailure): + """Failure during switchbox routing phase.""" + + pass + + +class Boundary: + """Class for indicating a boundary. + + When a Boundary object is placed in the part attribute of a Face, it + indicates the Face is on the outer boundary of the Node routing area + and no routes can pass through it. + """ + + pass + + +# Boundary object for placing in the bounding Faces of the Node routing area. +boundary = Boundary() + +# Absolute coords of all part pins. Used when trimming stub nets. +pin_pts = [] + + +class Terminal: + def __init__(self, net, face, coord): + """Terminal on a Face from which a net is routed within a SwitchBox. + + Args: + net (Net): Net upon which the Terminal resides. + face (Face): SwitchBox Face upon which the Terminal resides. + coord (int): Absolute position along the track the face is in. + + Notes: + A terminal exists on a Face and is assigned to a net. + The terminal's (x,y) position is determined by the terminal's + absolute coordinate along the track parallel to the face, + and by the Face's absolute coordinate in the orthogonal direction. + """ + + self.net = net + self.face = face + self.coord = coord + + @property + def route_pt(self): + """Return (x,y) Point for a Terminal on a Face.""" + track = self.face.track + if track.orientation == HORZ: + return Point(self.coord, track.coord) + else: + return Point(track.coord, self.coord) + + def get_next_terminal(self, next_face): + """Get the terminal on the next face that lies on the same net as this terminal. + + This method assumes the terminal's face and the next face are faces of the + same switchbox. Hence, they're either parallel and on opposite sides, or they're + at right angles so they meet at a corner. + + Args: + next_face (Face): Face to search for a terminal on the same net as this. + + Raises: + RoutingFailure: If no terminal exists. + + Returns: + Terminal: The terminal found on the next face. + """ + + from_face = self.face + if next_face.track in (from_face.beg, from_face.end): + # The next face bounds the interval of the terminals's face, so + # they're at right angles. With right angle faces, we want to + # select a terminal on the next face that's close to this corner + # because that will minimize the length of wire needed to make + # the connection. + if next_face.beg == from_face.track: + # next_face is oriented upward or rightward w.r.t. from_face. + # Start searching for a terminal from the lowest index + # because this is closest to the corner. + search_terminals = next_face.terminals + elif next_face.end == from_face.track: + # next_face is oriented downward or leftward w.r.t. from_face. + # Start searching for a terminal from the highest index + # because this is closest to the corner. + search_terminals = next_face.terminals[::-1] + else: + raise GlobalRoutingFailure + else: + # The next face must be the parallel face on the other side of the + # switchbox. With parallel faces, we want to selected a terminal + # having close to the same position as the given terminal. + # So if the given terminal is at position i, then search for the + # next terminal on the other face at positions i, i+1, i-1, i+2, i-2... + coord = self.coord + lower_terminals = [t for t in next_face.terminals if t.coord <= coord] + lower_terminals.sort(key=lambda t: t.coord, reverse=True) + upper_terminals = [t for t in next_face.terminals if t.coord > coord] + upper_terminals.sort(key=lambda t: t.coord, reverse=False) + search_terminals = list( + chain(*zip_longest(lower_terminals, upper_terminals)) + ) + search_terminals = [t for t in search_terminals if t is not None] + + # Search to find a terminal on the same net. + for terminal in search_terminals: + if terminal.net is self.net: + return terminal # Return found terminal. + + # No terminal on the same net, so search to find an unassigned terminal. + for terminal in search_terminals: + if terminal.net is None: + terminal.net = self.net # Assign net to terminal. + return terminal # Return newly-assigned terminal. + + # Well, something went wrong. Should have found *something*! + raise GlobalRoutingFailure + + def draw(self, scr, tx, **options): + """Draw a Terminal for debugging purposes. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + options (dict, optional): Dictionary of options and values. Defaults to {}. + """ + + # Don't draw terminal if it isn't on a net. It's just a placeholder. + if self.net or options.get("draw_all_terminals"): + draw_endpoint(self.route_pt, scr, tx, color=(255, 0, 0)) + # draw_endpoint(self.route_pt, scr, tx, color=net_colors[self.net]) + + +class Interval(object): + def __init__(self, beg, end): + """Define an interval with a beginning and an end. + + Args: + beg (GlobalTrack): Beginning orthogonal track that bounds interval. + end (GlobalTrack): Ending orthogonal track that bounds interval. + + Note: The beginning and ending Tracks are orthogonal to the Track containing the interval. + Also, beg and end are sorted so beg <= end. + """ + + # Order beginning and end so beginning <= end. + if beg > end: + beg, end = end, beg + self.beg = beg + self.end = end + + def __bool__(self): + """An Interval object always returns True.""" + return True + + @property + def len(self): + """Return the length of the interval.""" + return self.end - self.beg + + def __len__(self): + """Return the length of the interval.""" + return self.len + + def intersects(self, other): + """Return True if the intervals overlap (even if only at one point).""" + return not ((self.beg > other.end) or (self.end < other.beg)) + + def interval_intersection(self, other): + """Return intersection of two intervals as an interval, otherwise None.""" + if self.intersects(other): + beg = max(self.beg, other.beg) + end = min(self.end, other.end) + assert beg <= end + if beg != end: + return Interval(beg, end) + return None + + def merge(self, other): + """Return a merged interval if the given intervals intersect, otherwise return None.""" + if Interval.intersects(self, other): + return Interval(min(self.beg, other.beg), max(self.end, other.end)) + return None + + +class NetInterval(Interval): + def __init__(self, net, beg, end): + """Define an Interval with an associated net (useful for wire traces in a switchbox). + + Args: + net (Net): Net associated with interval. + beg (GlobalTrack): Beginning orthogonal track that bounds interval. + end (GlobalTrack): Ending track that bounds interval. + """ + super().__init__(beg, end) + self.net = net + + def obstructs(self, other): + """Return True if the intervals intersect and have different nets.""" + return super().intersects(other) and (self.net is not other.net) + + def merge(self, other): + """Return a merged interval if the given intervals intersect and are on the same net, otherwise return None.""" + if self.net is other.net: + merged_intvl = super().merge(other) + if merged_intvl: + merged_intvl = NetInterval(self.net, merged_intvl.beg, merged_intvl.end) + return merged_intvl + return None + + +class Adjacency: + def __init__(self, from_face, to_face): + """Define an adjacency between two Faces. + + Args: + from_face (Face): One Face. + to_face (Face): The other Face. + + Note: The Adjacency object will be associated with the from_face object, so there's + no need to store from_face in the Adjacency object. + """ + + self.face = to_face + if from_face.track.orientation == to_face.track.orientation: + # Parallel faces, either both vertical or horizontal. + # Distance straight-across from one face to the other. + dist_a = abs(from_face.track.coord - to_face.track.coord) + # Average distance parallel to the faces. + dist_b = (from_face.length + to_face.length) / 2 + # Compute the average distance from a terminal on one face to the other. + self.dist = dist_a + dist_b / 2 + else: + # Else, orthogonal faces. + # Compute the average face-to-face distance. + dist_a = from_face.length + dist_b = to_face.length + # Average distance of dogleg route from a terminal on one face to the other. + self.dist = (dist_a + dist_b) / 2 + + +class Face(Interval): + """A side of a rectangle bounding a routing switchbox.""" + + def __init__(self, part, track, beg, end): + """One side of a routing switchbox. + + Args: + part (set,Part,Boundary): Element(s) the Face is part of. + track (GlobalTrack): Horz/vert track the Face is on. + beg (GlobalTrack): Vert/horz track the Face begins at. + end (GlobalTrack): Vert/horz track the Face ends at. + + Notes: + The beg and end tracks have to be in the same direction + (i.e., both vertical or both horizontal) and orthogonal + to the track containing the face. + """ + + # Initialize the interval beginning and ending defining the Face. + super().__init__(beg, end) + + # Store Part/Boundary the Face is part of, if any. + self.part = set() + if isinstance(part, set): + self.part.update(part) + elif part is not None: + self.part.add(part) + + # Storage for any part pins that lie along this Face. + self.pins = [] + + # Storage for routing terminals along this face. + self.terminals = [] + + # Set of Faces adjacent to this one. (Starts empty.) + self.adjacent = set() + + # Add this new face to the track it belongs to so it isn't lost. + self.track = track + track.add_face(self) + + # Storage for switchboxes this face is part of. + self.switchboxes = set() + + def combine(self, other): + """Combine information from other face into this one. + + Args: + other (Face): Other Face. + + Returns: + None. + """ + + self.pins.extend(other.pins) + self.terminals.extend(other.terminals) + self.part.update(other.part) + self.adjacent.update(other.adjacent) + self.switchboxes.update(other.switchboxes) + + @property + def length(self): + """Return the length of the face.""" + return self.end.coord - self.beg.coord + + @property + def bbox(self): + """Return the bounding box of the 1-D face segment.""" + bbox = BBox() + + if self.track.orientation == VERT: + # Face runs vertically, so bbox width is zero. + bbox.add(Point(self.track.coord, self.beg.coord)) + bbox.add(Point(self.track.coord, self.end.coord)) + else: + # Face runs horizontally, so bbox height is zero. + bbox.add(Point(self.beg.coord, self.track.coord)) + bbox.add(Point(self.end.coord, self.track.coord)) + + return bbox + + def add_terminal(self, net, coord): + """Create a Terminal on the Face. + + Args: + net (Net): The net the terminal is on. + coord (int): The absolute coordinate along the track containing the Face. + + Raises: + TerminalClashException: + """ + + if self.part and not net: + # Don't add pin terminals with no net to a Face on a part or boundary. + return + + # Search for pre-existing terminal at the same coordinate. + for terminal in self.terminals: + if terminal.coord == coord: + # There is a pre-existing terminal at this coord. + if not net: + # The new terminal has no net (i.e., non-pin terminal), + # so just quit and don't bother to add it. The pre-existing + # terminal is retained. + return + elif terminal.net and terminal.net is not net: + # The pre-existing and new terminals have differing nets, so + # raise an exception. + raise TerminalClashException + # The pre-existing and new terminals have the same net. + # Remove the pre-existing terminal. It will be replaced + # with the new terminal below. + self.terminals.remove(terminal) + + # Create a new Terminal and add it to the list of terminals for this face. + self.terminals.append(Terminal(net, self, coord)) + + def trim_repeated_terminals(self): + """Remove all but one terminal of each individual net from the face. + + Notes: + A non-part Face with multiple terminals on the same net will lead + to multi-path routing. + """ + + # Find the intersection of every non-part face in the track with this one. + intersections = [] + for face in self.track: + if not face.part: + intersection = self.interval_intersection(face) + if intersection: + intersections.append(intersection) + + # Merge any overlapping intersections to create larger ones. + for i in range(len(intersections)): + for j in range(i + 1, len(intersections)): + merge = intersections[i].merge(intersections[j]) + if merge: + intersections[j] = merge + intersections[i] = None + break + + # Remove None from the list of intersections. + intersections = list(set(intersections) - {None}) + + # The intersections are now as large as they can be and not associated + # with any parts, so there are no terminals associated with part pins. + # Look for terminals within an intersection on the same net and + # remove all but one of them. + for intersection in intersections: + # Make a dict with nets and the terminals on each one. + net_term_dict = defaultdict(list) + for terminal in self.terminals: + if intersection.beg.coord <= terminal.coord <= intersection.end.coord: + net_term_dict[terminal.net].append(terminal) + if None in net_term_dict.keys(): + del net_term_dict[None] # Get rid of terminals not assigned to nets. + + # For each multi-terminal net, remove all but one terminal. + # This terminal must be removed from all faces on the track. + for terminals in net_term_dict.values(): + for terminal in terminals[1:]: # Keep only the 1st terminal. + self.track.remove_terminal(terminal) + + def create_nonpin_terminals(self): + """Create unassigned terminals along a non-part Face with GRID spacing. + + These terminals will be used during global routing of nets from + face-to-face and during switchbox routing. + """ + + # Add terminals along a Face. A terminal can be right at the start if the Face + # starts on a grid point, but there cannot be a terminal at the end + # if the Face ends on a grid point. Otherwise, there would be two terminals + # at exactly the same point (one at the ending point of a Face and the + # other at the beginning point of the next Face). + # FIXME: This seems to cause wiring with a lot of doglegs. + if self.end.coord - self.beg.coord <= GRID: + # Allow a terminal right at the start of the Face if the Face is small. + beg = (self.beg.coord + GRID - 1) // GRID * GRID + else: + # For larger faces with lengths greater than the GRID spacing, + # don't allow terminals right at the start of the Face. + beg = (self.beg.coord + GRID) // GRID * GRID + end = self.end.coord + + # Create terminals along the Face. + for coord in range(beg, end, GRID): + self.add_terminal(None, coord) + + def set_capacity(self): + """Set the wire routing capacity of a Face.""" + + if self.part: + # Part/boundary faces have zero capacity for wires to pass thru. + self.capacity = 0 + else: + # Wire routing capacity for other faces is the number of terminals they have. + self.capacity = len(self.terminals) + + def has_nets(self): + """Return True if any Terminal on the Face is attached to a net.""" + return any((terminal.net for terminal in self.terminals)) + + def add_adjacencies(self): + """Add adjacent faces of the switchbox having this face as the top face.""" + + # Create a temporary switchbox. + try: + swbx = SwitchBox(self) + except NoSwitchBox: + # This face doesn't belong to a valid switchbox. + return + + def add_adjacency(from_, to): + # Faces on the boundary can never accept wires so they are never + # adjacent to any other face. + if boundary in from_.part or boundary in to.part: + return + + # If a face is an edge of a part, then it can never be adjacent to + # another face on the *same part* or else wires might get routed over + # the part bounding box. + if from_.part.intersection(to.part): + return + + # OK, no parts in common between the two faces so they can be adjacent. + from_.adjacent.add(Adjacency(from_, to)) + to.adjacent.add(Adjacency(to, from_)) + + # Add adjacent faces. + add_adjacency(swbx.top_face, swbx.bottom_face) + add_adjacency(swbx.left_face, swbx.right_face) + add_adjacency(swbx.left_face, swbx.top_face) + add_adjacency(swbx.left_face, swbx.bottom_face) + add_adjacency(swbx.right_face, swbx.top_face) + add_adjacency(swbx.right_face, swbx.bottom_face) + + # Get rid of the temporary switchbox. + del swbx + + def extend(self, orthogonal_tracks): + """Extend a Face along its track until it is blocked by an orthogonal face. + + This is used to create Faces that form the irregular grid of switchboxes. + + Args: + orthogonal_tracks (list): List of tracks at right-angle to this face. + """ + + # Only extend faces that compose part bounding boxes. + if not self.part: + return + + # Extend the face backward from its beginning and forward from its end. + for start, dir in ((self.beg, -1), (self.end, 1)): + # Get tracks to extend face towards. + search_tracks = orthogonal_tracks[start.idx :: dir] + + # The face extension starts off non-blocked by any orthogonal faces. + blocked = False + + # Search for a orthogonal face in a track that intersects this extension. + for ortho_track in search_tracks: + for ortho_face in ortho_track: + # Intersection only occurs if the extending face hits the open + # interval of the orthogonal face, not if it touches an endpoint. + if ortho_face.beg < self.track < ortho_face.end: + # OK, this face intersects the extension. It also means the + # extending face will block the face just found, so split + # each track at the intersection point. + ortho_track.add_split(self.track) + self.track.add_split(ortho_track) + + # If the intersecting face is also a face of a part bbox, + # then the extension is blocked, so create the extended face + # and stop the extension. + if ortho_face.part: + # This creates a face and adds it to the track. + Face(None, self.track, start, ortho_track) + blocked = True + + # Stop checking faces in this track after an intersection is found. + break + + # Stop checking any further tracks once the face extension is blocked. + if blocked: + break + + def split(self, trk): + """If a track intersects in the middle of a face, split the face into two faces.""" + + if self.beg < trk < self.end: + # Add a Face from beg to trk to self.track. + Face(self.part, self.track, self.beg, trk) + # Move the beginning of the original Face to trk. + self.beg = trk + + def coincides_with(self, other_face): + """Return True if both faces have the same beginning and ending point on the same track.""" + return (self.beg, self.end) == (other_face.beg, other_face.end) + + def has_overlap(self, other_face): + """Return True if the two faces overlap.""" + return self.beg < other_face.end and self.end > other_face.beg + + def audit(self): + """Raise exception if face is malformed.""" + assert len(self.switchboxes) <= 2 + + @property + def seg(self): + """Return a Segment that coincides with the Face.""" + + if self.track.orientation == VERT: + p1 = Point(self.track.coord, self.beg.coord) + p2 = Point(self.track.coord, self.end.coord) + else: + p1 = Point(self.beg.coord, self.track.coord) + p2 = Point(self.end.coord, self.track.coord) + + return Segment(p1, p2) + + def draw( + self, scr, tx, font, color=(128, 128, 128), thickness=2, dot_radius=0, **options + ): + """Draw a Face in the drawing area. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + options (dict, optional): Dictionary of options and values. + + Returns: + None. + """ + + # Draw a line segment for the Face. + draw_seg( + self.seg, scr, tx, color=color, thickness=thickness, dot_radius=dot_radius + ) + + # Draw the terminals on the Face. + for terminal in self.terminals: + terminal.draw(scr, tx, **options) + + if options.get("show_capacities"): + # Show the wiring capacity at the midpoint of the Face. + mid_pt = (self.seg.p1 + self.seg.p2) / 2 + draw_text(str(self.capacity), mid_pt, scr, tx, font=font, color=color) + + +class GlobalWire(list): + def __init__(self, net, *args, **kwargs): + """A list connecting switchbox faces and terminals. + + Global routes start off as a sequence of switchbox faces that the route + goes thru. Later, these faces are converted to terminals at fixed positions + on their respective faces. + + Args: + net (Net): The net associated with the wire. + *args: Positional args passed to list superclass __init__(). + **kwargs: Keyword args passed to list superclass __init__(). + """ + self.net = net + super().__init__(*args, **kwargs) + + def cvt_faces_to_terminals(self): + """Convert global face-to-face route to switchbox terminal-to-terminal route.""" + + if not self: + # Global route is empty so do nothing. + return + + # Non-empty global routes should always start from a face on a part. + assert self[0].part + + # All part faces already have terminals created from the part pins. Find all + # the route faces on part boundaries and convert them to pin terminals if + # one or more pins are attached to the same net as the route. + for i, face in enumerate(self[:]): + if face.part: + # This route face is on a part boundary, so find the terminal with the route's net. + for terminal in face.terminals: + if self.net is terminal.net: + # Replace the route face with the terminal on the part. + self[i] = terminal + break + else: + # Route should never touch a part face if there is no terminal with the route's net. + raise RuntimeError + + # Proceed through all the Faces/Terminals on the GlobalWire, converting + # all the Faces to Terminals. + for i in range(len(self) - 1): + # The current element on a GlobalWire should always be a Terminal. Use that terminal + # to convert the next Face on the wire to a Terminal (if it isn't one already). + if isinstance(self[i], Face): + # Logic error if the current element has not been converted to a Terminal. + raise RuntimeError + + if isinstance(self[i + 1], Face): + # Convert the next Face element into a Terminal on this net. This terminal will + # be the current element on the next iteration. + self[i + 1] = self[i].get_next_terminal(self[i + 1]) + + def draw(self, scr, tx, color=(0, 0, 0), thickness=1, dot_radius=10, **options): + """Draw a global wire from Face-to-Face in the drawing area. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + color (list): Three-element list of RGB integers with range [0, 255]. + thickness (int): Thickness of drawn wire in pixels. + dot_radius (int): Radius of drawn terminal in pixels. + options (dict, optional): Dictionary of options and values. Defaults to {}. + + Returns: + None. + """ + + # Draw pins on the net associated with the wire. + for pin in self.net.pins: + # Only draw pins in the current node being routed which have the route_pt attribute. + if hasattr(pin, "route_pt"): + pt = pin.route_pt * pin.part.tx + track = pin.face.track + pt = { + HORZ: Point(pt.x, track.coord), + VERT: Point(track.coord, pt.y), + }[track.orientation] + draw_endpoint(pt, scr, tx, color=color, dot_radius=10) + + # Draw global wire segment. + face_to_face = zip(self[:-1], self[1:]) + for terminal1, terminal2 in face_to_face: + p1 = terminal1.route_pt + p2 = terminal2.route_pt + draw_seg( + Segment(p1, p2), scr, tx, color=color, thickness=thickness, dot_radius=0 + ) + + +class GlobalRoute(list): + def __init__(self, *args, **kwargs): + """A list containing GlobalWires that form an entire routing of a net. + + Args: + net (Net): The net associated with the wire. + *args: Positional args passed to list superclass __init__(). + **kwargs: Keyword args passed to list superclass __init__(). + """ + super().__init__(*args, **kwargs) + + def cvt_faces_to_terminals(self): + """Convert GlobalWires in route to switchbox terminal-to-terminal route.""" + for wire in self: + wire.cvt_faces_to_terminals() + + def draw( + self, scr, tx, font, color=(0, 0, 0), thickness=1, dot_radius=10, **options + ): + """Draw the GlobalWires of this route in the drawing area. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (list): Three-element list of RGB integers with range [0, 255]. + thickness (int): Thickness of drawn wire in pixels. + dot_radius (int): Radius of drawn terminal in pixels. + options (dict, optional): Dictionary of options and values. Defaults to {}. + + Returns: + None. + """ + + for wire in self: + wire.draw(scr, tx, color, thickness, dot_radius, **options) + + +class GlobalTrack(list): + def __init__(self, orientation=HORZ, coord=0, idx=None, *args, **kwargs): + """A horizontal/vertical track holding zero or more faces all having the same Y/X coordinate. + + These global tracks are made by extending the edges of part bounding boxes to + form a non-regular grid of rectangular switchboxes. These tracks are *NOT* the same + as the tracks used within a switchbox for the detailed routing phase. + + Args: + orientation (Orientation): Orientation of track (horizontal or vertical). + coord (int): Coordinate of track on axis orthogonal to track direction. + idx (int): Index of track into a list of X or Y coords. + *args: Positional args passed to list superclass __init__(). + **kwargs: Keyword args passed to list superclass __init__(). + """ + + self.orientation = orientation + self.coord = coord + self.idx = idx + super().__init__(*args, **kwargs) + + # This stores the orthogonal tracks that intersect this one. + self.splits = set() + + def __eq__(self, track): + """Used for ordering tracks.""" + return self.coord == track.coord + + def __ne__(self, track): + """Used for ordering tracks.""" + return self.coord != track.coord + + def __lt__(self, track): + """Used for ordering tracks.""" + return self.coord < track.coord + + def __le__(self, track): + """Used for ordering tracks.""" + return self.coord <= track.coord + + def __gt__(self, track): + """Used for ordering tracks.""" + return self.coord > track.coord + + def __ge__(self, track): + """Used for ordering tracks.""" + return self.coord >= track.coord + + def __sub__(self, other): + """Subtract coords of two tracks.""" + return self.coord - other.coord + + def extend_faces(self, orthogonal_tracks): + """Extend the faces in a track. + + This is part of forming the irregular grid of switchboxes. + + Args: + orthogonal_tracks (list): List of tracks orthogonal to this one (L/R vs. H/V). + """ + + for face in self[:]: + face.extend(orthogonal_tracks) + + def __hash__(self): + """This method lets a track be inserted into a set of splits.""" + return self.idx + + def add_split(self, orthogonal_track): + """Store the orthogonal track that intersects this one.""" + self.splits.add(orthogonal_track) + + def add_face(self, face): + """Add a face to a track. + + Args: + face (Face): Face to be added to track. + """ + + self.append(face) + + # The orthogonal tracks that bound the added face will split this track. + self.add_split(face.beg) + self.add_split(face.end) + + def split_faces(self): + """Split track faces by any intersecting orthogonal tracks.""" + + for split in self.splits: + for face in self[:]: + # Apply the split track to the face. The face will only be split + # if the split track intersects it. Any split faces will be added + # to the track this face is on. + face.split(split) + + def remove_duplicate_faces(self): + """Remove faces from the track having the same endpoints.""" + + # Create lists of faces having the same endpoints. + dup_faces_dict = defaultdict(list) + for face in self: + key = (face.beg, face.end) + dup_faces_dict[key].append(face) + + # Remove all but the first face from each list. + for dup_faces in dup_faces_dict.values(): + retained_face = dup_faces[0] + for dup_face in dup_faces[1:]: + # Add info from duplicate face to the retained face. + retained_face.combine(dup_face) + self.remove(dup_face) + + def remove_terminal(self, terminal): + """Remove a terminal from any non-part Faces in the track.""" + + coord = terminal.coord + # Look for the terminal in all non-part faces on the track. + for face in self: + if not face.part: + for term in face.terminals[:]: + if term.coord == coord: + face.terminals.remove(term) + + def add_adjacencies(self): + """Add adjacent switchbox faces to each face in a track.""" + + for top_face in self: + top_face.add_adjacencies() + + def audit(self): + """Raise exception if track is malformed.""" + + for i, first_face in enumerate(self): + first_face.audit() + for second_face in self[i + 1 :]: + if first_face.has_overlap(second_face): + raise AssertionError + + def draw(self, scr, tx, font, **options): + """Draw the Faces in a track. + + Args: + scr (_type_): _descriptio scr (PyGame screen): Screen object for PyGame drawing. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + options (dict, optional): Dictionary of options and values. Defaults to {}. + """ + for face in self: + face.draw(scr, tx, font, **options) + + +class Target: + def __init__(self, net, row, col): + """A point on a switchbox face that switchbox router has not yet reached. + + Targets are used to direct the switchbox router towards terminals that + need to be connected to nets. So wiring will be nudged up/down to + get closer to terminals along the upper/lower faces. Wiring will also + be nudged toward the track rows where terminals on the right face reside + as the router works from the left to the right. + + Args: + net (Net): Target net. + row (int): Track row for the target, including top or bottom faces. + col (int): Switchbox column for the target. + """ + self.row = row + self.col = col + self.net = net + + def __lt__(self, other): + """Used for ordering Targets in terms of priority.""" + + # Targets in the left-most columns are given priority since they will be reached + # first as the switchbox router proceeds from left-to-right. + return (self.col, self.row, id(self.net)) < ( + other.col, + other.row, + id(other.net), + ) + + +class SwitchBox: + # Indices for faces of the switchbox. + TOP, LEFT, BOTTOM, RIGHT = 0, 1, 2, 3 + + def __init__(self, top_face, left_face=None, bottom_face=None, right_face=None): + """Routing switchbox. + + A switchbox is a rectangular region through which wires are routed. + It has top, bottom, left and right faces. + + Args: + top_face (Face): The top face of the switchbox (needed to find the other faces). + bottom_face (Face): The bottom face. Will be calculated if set to None. + left_face (Face): The left face. Will be calculated if set to None. + right_face (Face): The right face. Will be calculated if set to None. + + Raises: + NoSwitchBox: Exception raised if the switchbox is an + unroutable region inside a part bounding box. + """ + + # Find the left face in the left track that bounds the top face. + if left_face == None: + left_track = top_face.beg + for face in left_track: + # The left face will end at the track for the top face. + if face.end.coord == top_face.track.coord: + left_face = face + break + else: + raise NoSwitchBox("Unroutable switchbox (left)!") + + # Find the right face in the right track that bounds the top face. + if right_face == None: + right_track = top_face.end + for face in right_track: + # The right face will end at the track for the top face. + if face.end.coord == top_face.track.coord: + right_face = face + break + else: + raise NoSwitchBox("Unroutable switchbox (right)!") + + # For a routable switchbox, the left and right faces should each + # begin at the same point. + if left_face.beg != right_face.beg: + # Inequality only happens when two parts are butted up against each other + # to form a non-routable switchbox inside a part bounding box. + raise NoSwitchBox("Unroutable switchbox (left-right)!") + + # Find the bottom face in the track where the left/right faces begin. + if bottom_face == None: + bottom_track = left_face.beg + for face in bottom_track: + # The bottom face should begin/end in the same places as the top face. + if (face.beg.coord, face.end.coord) == ( + top_face.beg.coord, + top_face.end.coord, + ): + bottom_face = face + break + else: + raise NoSwitchBox("Unroutable switchbox (bottom)!") + + # If all four sides have a part in common, then the switchbox is inside + # a part bbox that wires cannot be routed through. + if top_face.part & bottom_face.part & left_face.part & right_face.part: + raise NoSwitchBox("Unroutable switchbox (part)!") + + # Store the faces. + self.top_face = top_face + self.bottom_face = bottom_face + self.left_face = left_face + self.right_face = right_face + + # Each face records which switchboxes it belongs to. + self.top_face.switchboxes.add(self) + self.bottom_face.switchboxes.add(self) + self.left_face.switchboxes.add(self) + self.right_face.switchboxes.add(self) + + def find_terminal_net(terminals, terminal_coords, coord): + """Return the net attached to a terminal at the given coordinate. + + Args: + terminals (list): List of Terminals to search. + terminal_coords (list): List of integer coordinates for Terminals. + coord (int): Terminal coordinate to search for. + + Returns: + Net/None: Net at given coordinate or None if no net exists. + """ + try: + return terminals[terminal_coords.index(coord)].net + except ValueError: + return None + + # Find the coordinates of all the horizontal routing tracks + left_coords = [terminal.coord for terminal in self.left_face.terminals] + right_coords = [terminal.coord for terminal in self.right_face.terminals] + tb_coords = [self.top_face.track.coord, self.bottom_face.track.coord] + # Remove duplicate coords. + self.track_coords = list(set(left_coords + right_coords + tb_coords)) + + if len(self.track_coords) == 2: + # This is a weird case. If the switchbox channel is too narrow to hold + # a routing track in the middle, then place two pseudo-tracks along the + # top and bottom faces to allow routing to proceed. The routed wires will + # end up in the top or bottom faces, but maybe that's OK. + # FIXME: Should this be extending with tb_coords? + # FIXME: Should we always extend with tb_coords? + self.track_coords.extend(self.track_coords) + + # Sort horiz. track coords from bottom to top. + self.track_coords = sorted(self.track_coords) + + # Create a list of nets for each of the left/right faces. + self.left_nets = [ + find_terminal_net(self.left_face.terminals, left_coords, coord) + for coord in self.track_coords + ] + self.right_nets = [ + find_terminal_net(self.right_face.terminals, right_coords, coord) + for coord in self.track_coords + ] + + # Find the coordinates of all the vertical columns and then create + # a list of nets for each of the top/bottom faces. + top_coords = [terminal.coord for terminal in self.top_face.terminals] + bottom_coords = [terminal.coord for terminal in self.bottom_face.terminals] + lr_coords = [self.left_face.track.coord, self.right_face.track.coord] + self.column_coords = sorted(set(top_coords + bottom_coords + lr_coords)) + self.top_nets = [ + find_terminal_net(self.top_face.terminals, top_coords, coord) + for coord in self.column_coords + ] + self.bottom_nets = [ + find_terminal_net(self.bottom_face.terminals, bottom_coords, coord) + for coord in self.column_coords + ] + + # Remove any nets that only have a single terminal in the switchbox. + all_nets = self.left_nets + self.right_nets + self.top_nets + self.bottom_nets + net_counts = Counter(all_nets) + single_terminal_nets = [net for net, count in net_counts.items() if count <= 1] + if single_terminal_nets: + for side_nets in ( + self.left_nets, + self.right_nets, + self.top_nets, + self.bottom_nets, + ): + for i, net in enumerate(side_nets): + if net in single_terminal_nets: + side_nets[i] = None + + # Handle special case when a terminal is right on the corner of the switchbox. + self.move_corner_nets() + + # Storage for detailed routing. + self.segments = defaultdict(list) + + def audit(self): + """Raise exception if switchbox is malformed.""" + + for face in self.face_list: + face.audit() + assert self.top_face.track.orientation == HORZ + assert self.bottom_face.track.orientation == HORZ + assert self.left_face.track.orientation == VERT + assert self.right_face.track.orientation == VERT + assert len(self.top_nets) == len(self.bottom_nets) + assert len(self.left_nets) == len(self.right_nets) + + @property + def face_list(self): + """Return list of switchbox faces in CCW order, starting from top face.""" + flst = [None] * 4 + flst[self.TOP] = self.top_face + flst[self.LEFT] = self.left_face + flst[self.BOTTOM] = self.bottom_face + flst[self.RIGHT] = self.right_face + return flst + + def move_corner_nets(self): + """ + Move any nets at the edges of the left/right faces + (i.e., the corners) to the edges of the top/bottom faces. + This will allow these nets to be routed within the switchbox columns + as the routing proceeds from left to right. + """ + + if self.left_nets[0]: + # Move bottommost net on left face to leftmost net on bottom face. + self.bottom_nets[0] = self.left_nets[0] + self.left_nets[0] = None + + if self.left_nets[-1]: + # Move topmost net on left face to leftmost net on top face. + self.top_nets[0] = self.left_nets[-1] + self.left_nets[-1] = None + + if self.right_nets[0]: + # Move bottommost net on right face to rightmost net on bottom face. + self.bottom_nets[-1] = self.right_nets[0] + self.right_nets[0] = None + + if self.right_nets[-1]: + # Move topmost net on right face to rightmost net on top face. + self.top_nets[-1] = self.right_nets[-1] + self.right_nets[-1] = None + + def flip_xy(self): + """Flip X-Y of switchbox to route from top-to-bottom instead of left-to-right.""" + + # Flip coords of tracks and columns. + self.column_coords, self.track_coords = self.track_coords, self.column_coords + + # Flip top/right and bottom/left nets. + self.top_nets, self.right_nets = self.right_nets, self.top_nets + self.bottom_nets, self.left_nets = self.left_nets, self.bottom_nets + + # Flip top/right and bottom/left faces. + self.top_face, self.right_face = self.right_face, self.top_face + self.bottom_face, self.left_face = self.left_face, self.bottom_face + + # Move any corner nets from the new left/right faces to the new top/bottom faces. + self.move_corner_nets() + + # Flip X/Y coords of any routed segments. + for segments in self.segments.values(): + for seg in segments: + seg.flip_xy() + + def coalesce(self, switchboxes): + """Group switchboxes around a seed switchbox into a larger switchbox. + + Args: + switchboxes (list): List of seed switchboxes that have not yet been coalesced into a larger switchbox. + + Returns: + A coalesced switchbox or None if the seed was no longer available for coalescing. + """ + + # Abort if the switchbox is no longer a potential seed (it was already merged into a previous switchbox). + if self not in switchboxes: + return None + + # Remove the switchbox from the list of seeds. + switchboxes.remove(self) + + # List the switchboxes along the top, left, bottom and right borders of the coalesced switchbox. + box_lists = [[self], [self], [self], [self]] + + # Iteratively search to the top, left, bottom, and right for switchboxes to add. + active_directions = {self.TOP, self.LEFT, self.BOTTOM, self.RIGHT} + while active_directions: + # Grow in the shortest dimension so the coalesced switchbox stays "squarish". + bbox = BBox() + for box_list in box_lists: + bbox.add(box_list[0].bbox) + if bbox.w == bbox.h: + # Already square, so grow in any direction. + growth_directions = {self.TOP, self.LEFT, self.BOTTOM, self.RIGHT} + elif bbox.w < bbox.h: + # Taller than wide, so grow left or right. + growth_directions = {self.LEFT, self.RIGHT} + else: + # Wider than tall, so grow up or down. + growth_directions = {self.TOP, self.BOTTOM} + + # Only keep growth directions that are still active. + growth_directions = growth_directions & active_directions + + # If there is no active growth direction, then stop the growth iterations. + if not growth_directions: + break + + # Take a random choice of the active growth directions. + direction = random.choice(list(growth_directions)) + + # Check the switchboxes along the growth side to see if further expansion is possible. + box_list = box_lists[direction] + for box in box_list: + # Get the face of the box from which growth will occur. + box_face = box.face_list[direction] + if box_face.part: + # This box butts up against a part, so expansion in this direction is blocked. + active_directions.remove(direction) + break + # Get the box which will be added if expansion occurs. + # Every face borders two switchboxes, so the adjacent box is the other one. + adj_box = (box_face.switchboxes - {box}).pop() + if adj_box not in switchboxes: + # This box cannot be added, so expansion in this direction is blocked. + active_directions.remove(direction) + break + else: + # All the switchboxes along the growth side are available for expansion, + # so replace the current boxes in the growth side with these new ones. + for i, box in enumerate(box_list[:]): + # Get the adjacent box for the current box on the growth side. + box_face = box.face_list[direction] + adj_box = (box_face.switchboxes - {box}).pop() + # Replace the current box with the new box from the expansion. + box_list[i] = adj_box + # Remove the newly added box from the list of available boxes for growth. + switchboxes.remove(adj_box) + + # Add the first box on the growth side to the end of the list of boxes on the + # preceding direction: (top,left,bottom,right) if current direction is (left,bottom,right,top). + box_lists[direction - 1].append(box_list[0]) + + # Add the last box on the growth side to the start of the list of boxes on the + # next direction: (bottom,right,top,left) if current direction is (left,bottom,right,top). + box_lists[(direction + 1) % 4].insert(0, box_list[-1]) + + # Create new faces that bound the coalesced group of switchboxes. + total_faces = [None, None, None, None] + directions = (self.TOP, self.LEFT, self.BOTTOM, self.RIGHT) + for direction, box_list in zip(directions, box_lists): + # Create a face that spans all the faces of the boxes along one side. + face_list = [box.face_list[direction] for box in box_list] + beg = min([face.beg for face in face_list]) + end = max([face.end for face in face_list]) + total_face = Face(None, face_list[0].track, beg, end) + + # Add terminals from the box faces along one side. + total_face.create_nonpin_terminals() + for face in face_list: + for terminal in face.terminals: + if terminal.net: + total_face.add_terminal(terminal.net, terminal.coord) + + # Set the routing capacity of the new face. + total_face.set_capacity() + + # Store the new face for this side. + total_faces[direction] = total_face + + # Return the coalesced switchbox created from the new faces. + return SwitchBox(*total_faces) + + def trim_repeated_terminals(self): + """Trim terminals on each face.""" + for face in self.face_list: + face.trim_repeated_terminals() + + @property + def bbox(self): + """Return bounding box for a switchbox.""" + return BBox().add(self.top_face.bbox).add(self.left_face.bbox) + + def has_nets(self): + """Return True if switchbox has any terminals on any face with nets attached.""" + return ( + self.top_face.has_nets() + or self.bottom_face.has_nets() + or self.left_face.has_nets() + or self.right_face.has_nets() + ) + + def route(self, **options): + """Route wires between terminals on the switchbox faces. + + Args: + options (dict, optional): Dictionary of options and values. Defaults to {}. + + Raises: + RoutingFailure: Raised if routing could not be completed. + + Returns: + List of Segments: List of wiring segments for switchbox routes. + """ + + if not self.has_nets(): + # Return what should be an empty dict. + assert not self.segments.keys() + return self.segments + + def collect_targets(top_nets, bottom_nets, right_nets): + """Collect target nets along top, bottom, right faces of switchbox.""" + + min_row = 1 + max_row = len(right_nets) - 2 + max_col = len(top_nets) + targets = [] + + # Collect target nets along top and bottom faces of the switchbox. + for col, (t_net, b_net) in enumerate(zip(top_nets, bottom_nets)): + if t_net is not None: + targets.append(Target(t_net, max_row, col)) + if b_net is not None: + targets.append(Target(b_net, min_row, col)) + + # Collect target nets on the right face of the switchbox. + for row, r_net in enumerate(right_nets): + if r_net is not None: + targets.append(Target(r_net, row, max_col)) + + # Sort the targets by increasing column order so targets closer to + # the left-to-right routing have priority. + targets.sort() + + return targets + + def connect_top_btm(track_nets): + """Connect nets from top/bottom terminals in a column to nets in horizontal tracks of the switchbox.""" + + def find_connection(net, tracks, direction): + """ + Searches for the closest track with the same net followed by the + closest empty track. The indices of these tracks are returned. + If the net cannot be connected to any track, return []. + If the net given to connect is None, then return a list of [None]. + + Args: + net (Net): Net to be connected. + tracks (list): Nets on tracks + direction (int): Search direction for connection (-1: down, +1:up). + + Returns: + list: Indices of tracks where the net can connect. + """ + + if net: + if direction < 0: + # Searching down so reverse tracks. + tracks = tracks[::-1] + + connections = [] + + try: + # Find closest track with the given net. + connections.append(tracks[1:-1].index(net) + 1) + except ValueError: + pass + + try: + # Find closest empty track. + connections.append(tracks[1:-1].index(None) + 1) + except ValueError: + pass + + if direction < 0: + # Reverse track indices if searching down. + l = len(tracks) + connections = [l - 1 - cnct for cnct in connections] + else: + # No net so return no connections. + connections = [None] + + return connections + + # Stores net intervals connecting top/bottom nets to horizontal tracks. + column_intvls = [] + + # Top/bottom nets for this switchbox column. Horizontal track nets are + # at indexes 1..-2. + b_net = track_nets[0] + t_net = track_nets[-1] + + if t_net and (t_net is b_net): + # If top & bottom nets are the same, just create a single net interval + # connecting them and that's it. + column_intvls.append(NetInterval(t_net, 0, len(track_nets) - 1)) + return column_intvls + + # Find which tracks the top/bottom nets can connect to. + t_cncts = find_connection(t_net, track_nets, -1) + b_cncts = find_connection(b_net, track_nets, 1) + + # Create all possible pairs of top/bottom connections. + tb_cncts = [(t, b) for t in t_cncts for b in b_cncts] + + if not tb_cncts: + # No possible connections for top and/or bottom. + if options.get("allow_routing_failure"): + return column_intvls # Return empty column. + else: + raise SwitchboxRoutingFailure + + # Test each possible pair of connections to find one that is free of interference. + for t_cnct, b_cnct in tb_cncts: + if t_cnct is None or b_cnct is None: + # No possible interference if at least one connection is None. + break + if t_cnct > b_cnct: + # Top & bottom connections don't interfere. + break + if t_cnct == b_cnct and t_net is b_net: + # Top & bottom connect to the same track but they're the same net so that's OK. + break + else: + if options.get("allow_routing_failure"): + return column_intvls + else: + raise SwitchboxRoutingFailure + + if t_cnct is not None: + # Connection from track to terminal on top of switchbox. + column_intvls.append(NetInterval(t_net, t_cnct, len(track_nets) - 1)) + if b_cnct is not None: + # Connection from terminal on bottom of switchbox to track. + column_intvls.append(NetInterval(b_net, 0, b_cnct)) + + # Return connection segments. + return column_intvls + + def prune_targets(targets, current_col): + """Remove targets in columns to the left of the current left-to-right routing column""" + return [target for target in targets if target.col > current_col] + + def insert_column_nets(track_nets, column_intvls): + """Return the active nets with the added nets of the column's vertical intervals.""" + + nets = track_nets[:] + for intvl in column_intvls: + nets[intvl.beg] = intvl.net + nets[intvl.end] = intvl.net + return nets + + def net_search(net, start, track_nets): + """Search for the closest points for the net before and after the start point.""" + + # illegal offset past the end of the list of track nets. + large_offset = 2 * len(track_nets) + + try: + # Find closest occurrence of net going up. + up = track_nets[start:].index(net) + except ValueError: + # Net not found, so use out-of-bounds index. + up = large_offset + + try: + # Find closest occurrence of net going down. + down = track_nets[start::-1].index(net) + except ValueError: + # Net not found, so use out-of-bounds index. + down = large_offset + + if up <= down: + return up + else: + return -down + + def insert_target_nets(track_nets, targets, right_nets): + """Return copy of active track nets with additional prioritized targets from the top, bottom, right faces.""" + + # Allocate storage for potential target nets to be added to the list of active track nets. + placed_target_nets = [None] * len(track_nets) + + # Get a list of nets on the right face that are being actively routed right now + # so we can steer the routing as it proceeds rightward. + active_right_nets = [ + net if net in track_nets else None for net in right_nets + ] + + # Strip-off the top/bottom rows where terminals are and routing doesn't go. + search_nets = track_nets[1:-1] + + for target in targets: + target_net, target_row = target.net, target.row + + # Skip target nets that aren't currently active or have already been + # placed (prevents multiple insertions of the same target net). + # Also ignore targets on the far right face until the last step. + if ( + target_net not in track_nets # TODO: Use search_nets??? + or target_net in placed_target_nets + or target_net in active_right_nets + ): + continue + + # Assign the target net to the closest row to the target row that is either + # empty or has the same net. + net_row_offset = net_search(target_net, target_row, search_nets) + empty_row_offset = net_search(None, target_row, search_nets) + if abs(net_row_offset) <= abs(empty_row_offset): + row_offset = net_row_offset + else: + row_offset = empty_row_offset + try: + placed_target_nets[target_row + row_offset + 1] = target_net + search_nets[target_row + row_offset] = target_net + except IndexError: + # There was no place for this target net + pass + + return [ + active_net or target_net or right_net + # active_net or right_net or target_net + for (active_net, right_net, target_net) in zip( + track_nets, active_right_nets, placed_target_nets + ) + ] + + def connect_splits(track_nets, column): + """Vertically connect nets on multiple tracks.""" + + # Make a copy so the original isn't disturbed. + track_nets = track_nets[:] + + # Find nets that are running on multiple tracks. + multi_nets = set( + net for net in set(track_nets) if track_nets.count(net) > 1 + ) + multi_nets.discard(None) # Ignore empty tracks. + + # Find possible intervals for multi-track nets. + net_intervals = [] + for net in multi_nets: + net_trk_idxs = [idx for idx, nt in enumerate(track_nets) if nt is net] + for index, trk1 in enumerate(net_trk_idxs[:-1], 1): + for trk2 in net_trk_idxs[index:]: + net_intervals.append(NetInterval(net, trk1, trk2)) + + # Sort interval lengths from smallest to largest. + net_intervals.sort(key=lambda ni: len(ni)) + # Sort interval lengths from largest to smallest. + # net_intervals.sort(key=lambda ni: -len(ni)) + + # Connect tracks for each interval if it doesn't intersect an + # already existing connection. + for net_interval in net_intervals: + for col_interval in column: + if net_interval.obstructs(col_interval): + break + else: + # No conflicts found with existing connections. + column.append(net_interval) + + # Get the nets that have vertical wires in the column. + column_nets = set(intvl.net for intvl in column) + + # Merge segments of each net in the column. + for net in column_nets: + # Extract intervals if the current net has more than one interval. + intervals = [intvl for intvl in column if intvl.net is net] + if len(intervals) < 2: + # Skip if there's only a single interval for this net. + continue + + # Remove the intervals so they can be replaced with joined intervals. + for intvl in intervals: + column.remove(intvl) + + # Merge the extracted intervals as much as possible. + + # Sort intervals by their beginning coordinates. + intervals.sort(key=lambda intvl: intvl.beg) + + # Try merging consecutive pairs of intervals. + for i in range(len(intervals) - 1): + # Try to merge consecutive intervals. + merged_intvl = intervals[i].merge(intervals[i + 1]) + if merged_intvl: + # Keep only the merged interval and place it so it's compared to the next one. + intervals[i : i + 2] = None, merged_intvl + + # Remove the None entries that are inserted when segments get merged. + intervals = [intvl for intvl in intervals if intvl] + + # Place merged intervals back into column. + column.extend(intervals) + + return column + + def extend_tracks(track_nets, column, targets): + """Extend track nets into the next column.""" + + # These are nets to the right of the current column. + rightward_nets = set(target.net for target in targets) + + # Keep extending nets to next column if they do not intersect intervals in the + # current column with the same net. + flow_thru_nets = track_nets[:] + for intvl in column: + for trk_idx in range(intvl.beg, intvl.end + 1): + if flow_thru_nets[trk_idx] is intvl.net: + # Remove net from track since it intersects an interval with the + # same net. The net may be extended from the interval in the next phase, + # or it may terminate here. + flow_thru_nets[trk_idx] = None + + next_track_nets = flow_thru_nets[:] + + # Extend track net if net has multiple column intervals that need further interconnection + # or if there are terminals in rightward columns that need connections to this net. + first_track = 0 + last_track = len(track_nets) - 1 + column_nets = set([intvl.net for intvl in column]) + for net in column_nets: + # Get all the vertical intervals for this net in the current column. + net_intervals = [i for i in column if i.net is net] + + # No need to extend tracks for this net into next column if there aren't multiple + # intervals or further terminals to connect. + if net not in rightward_nets and len(net_intervals) < 2: + continue + + # Sort the net's intervals from bottom of the column to top. + net_intervals.sort(key=lambda e: e.beg) + + # Find the nearest target to the right matching the current net. + target_row = None + for target in targets: + if target.net is net: + target_row = target.row + break + + for i, intvl in enumerate(net_intervals): + # Sanity check: should never get here if interval goes from top-to-bottom of + # column (hence, only one interval) and there is no further terminal for this + # net to the right. + assert not ( + intvl.beg == first_track + and intvl.end == last_track + and not target_row + ) + + if intvl.beg == first_track and intvl.end < last_track: + # Interval starts on bottom of column, so extend net in the track where it ends. + assert i == 0 + assert track_nets[intvl.end] in (net, None) + exit_row = intvl.end + next_track_nets[exit_row] = net + continue + + if intvl.end == last_track and intvl.beg > first_track: + # Interval ends on top of column, so extend net in the track where it begins. + assert i == len(net_intervals) - 1 + assert track_nets[intvl.beg] in (net, None) + exit_row = intvl.beg + next_track_nets[exit_row] = net + continue + + if target_row is None: + # No target to the right, so we must be trying to connect multiple column intervals for this net. + if i == 0: + # First interval in column so extend from its top-most point. + exit_row = intvl.end + next_track_nets[exit_row] = net + elif i == len(net_intervals) - 1: + # Last interval in column so extend from its bottom-most point. + exit_row = intvl.beg + next_track_nets[exit_row] = net + else: + # This interval is between the top and bottom intervals. + beg_end = ( + bool(flow_thru_nets[intvl.beg]), + bool(flow_thru_nets[intvl.end]), + ) + if beg_end == (True, False): + # The net enters this interval at its bottom, so extend from the top (dogleg). + exit_row = intvl.end + next_track_nets[exit_row] = net + elif beg_end == (False, True): + # The net enters this interval at its top, so extend from the bottom (dogleg). + exit_row = intvl.beg + next_track_nets[exit_row] = net + else: + raise RuntimeError + continue + + else: + # Target to the right, so aim for it. + + if target_row > intvl.end: + # target track is above the interval's end, so bound it to the end. + target_row = intvl.end + elif target_row < intvl.beg: + # target track is below the interval's start, so bound it to the start. + target_row = intvl.beg + + # Search for the closest track to the target row that is either open + # or occupied by the same target net. + intvl_nets = track_nets[intvl.beg : intvl.end + 1] + net_row = ( + net_search(net, target_row - intvl.beg, intvl_nets) + + target_row + ) + open_row = ( + net_search(None, target_row - intvl.beg, intvl_nets) + + target_row + ) + net_dist = abs(net_row - target_row) + open_dist = abs(open_row - target_row) + if net_dist <= open_dist: + exit_row = net_row + else: + exit_row = open_row + assert intvl.beg <= exit_row <= intvl.end + next_track_nets[exit_row] = net + continue + + return next_track_nets + + def trim_column_intervals(column, track_nets, next_track_nets): + """Trim stubs from column intervals.""" + + # All nets entering and exiting the column. + trk_nets = list(enumerate(zip(track_nets, next_track_nets))) + + for intvl in column: + # Get all the entry/exit track positions having the same net as the interval + # and that are within the bounds of the interval. + net = intvl.net + beg = intvl.beg + end = intvl.end + trks = [i for (i, nets) in trk_nets if net in nets and beg <= i <= end] + + # Chop off any stubs of the interval that extend past where it could + # connect to an entry/exit point of its net. + intvl.beg = min(trks) + intvl.end = max(trks) + + ######################################## + # Main switchbox routing loop. + ######################################## + + # Get target nets as routing proceeds from left-to-right. + targets = collect_targets(self.top_nets, self.bottom_nets, self.right_nets) + + # Store the nets in each column that are in the process of being routed, + # starting with the nets in the left-hand face of the switchbox. + nets_in_column = [self.left_nets[:]] + + # Store routing intervals in each column. + all_column_intvls = [] + + # Route left-to-right across the columns connecting the top & bottom nets + # on each column to tracks within the switchbox. + for col, (t_net, b_net) in enumerate(zip(self.top_nets, self.bottom_nets)): + # Nets in the previous column become the currently active nets being routed + active_nets = nets_in_column[-1][:] + + if col == 0 and not t_net and not b_net: + # Nothing happens in the first column if there are no top & bottom nets. + # Just continue the active nets from the left-hand face to the next column. + column_intvls = [] + next_active_nets = active_nets[:] + + else: + # Bring any nets on the top & bottom of this column into the list of active nets. + active_nets[0] = b_net + active_nets[-1] = t_net + + # Generate the intervals that will vertically connect the top & bottom nets to + # horizontal tracks in the switchbox. + column_intvls = connect_top_btm(active_nets) + + # Add the nets from the new vertical connections to the active nets. + augmented_active_nets = insert_column_nets(active_nets, column_intvls) + + # Remove the nets processed in this column from the list of target nets. + targets = prune_targets(targets, col) + + # Insert target nets from rightward columns into this column to direct + # the placement of additional vertical intervals towards them. + augmented_active_nets = insert_target_nets( + augmented_active_nets, targets, self.right_nets + ) + + # Make vertical connections between tracks in the column having the same net. + column_intvls = connect_splits(augmented_active_nets, column_intvls) + + # Get the nets that will be active in the next column. + next_active_nets = extend_tracks(active_nets, column_intvls, targets) + + # Trim any hanging stubs from vertical routing intervals in the current column. + trim_column_intervals(column_intvls, active_nets, next_active_nets) + + # Store the active nets for the next column. + nets_in_column.append(next_active_nets) + + # Store the vertical routing intervals for this column. + all_column_intvls.append(column_intvls) + + ######################################## + # End of switchbox routing loop. + ######################################## + + # After routing from left-to-right, verify the active track nets coincide + # with the positions of the nets on the right-hand face of the switchbox. + for track_net, right_net in zip(nets_in_column[-1], self.right_nets): + if track_net is not right_net: + if not options.get("allow_routing_failure"): + raise SwitchboxRoutingFailure + + # Create wiring segments along each horizontal track. + # Add left and right faces to coordinates of the vertical columns. + column_coords = ( + [self.left_face.track.coord] + + self.column_coords + + [self.right_face.track.coord] + ) + # Proceed column-by-column from left-to-right creating horizontal wires. + for col_idx, nets in enumerate(nets_in_column): + beg_col_coord = column_coords[col_idx] + end_col_coord = column_coords[col_idx + 1] + # Create segments for each track (skipping bottom & top faces). + for trk_idx, net in enumerate(nets[1:-1], start=1): + if net: + # Create a wire segment for the net in this horizontal track of the column. + trk_coord = self.track_coords[trk_idx] + p1 = Point(beg_col_coord, trk_coord) + p2 = Point(end_col_coord, trk_coord) + seg = Segment(p1, p2) + self.segments[net].append(seg) + + # Create vertical wiring segments for each switchbox column. + for idx, column in enumerate(all_column_intvls): + # Get X coord of this column. + col_coord = self.column_coords[idx] + # Create vertical wire segments for wire interval in the column. + for intvl in column: + p1 = Point(col_coord, self.track_coords[intvl.beg]) + p2 = Point(col_coord, self.track_coords[intvl.end]) + self.segments[intvl.net].append(Segment(p1, p2)) + + return self.segments + + def draw( + self, scr=None, tx=None, font=None, color=(128, 0, 128), thickness=2, **options + ): + """Draw a switchbox and its routing for debugging purposes. + + Args: + scr (PyGame screen): Screen object for PyGame drawing. Initialize PyGame if None. + tx (Tx): Transformation matrix from real to screen coords. + font (PyGame font): Font for rendering text. + color (tuple, optional): Switchbox boundary color. Defaults to (128, 0, 128). + thickness (int, optional): Switchbox boundary thickness. Defaults to 2. + options (dict, optional): Dictionary of options and values. Defaults to {}. + """ + + # If the screen object is not None, then PyGame drawing is enabled so set flag + # to initialize PyGame. + do_start_end = not bool(scr) + + if do_start_end: + # Initialize PyGame. + scr, tx, font = draw_start( + self.bbox.resize(Vector(DRAWING_BOX_RESIZE, DRAWING_BOX_RESIZE)) + ) + + if options.get("draw_switchbox_boundary"): + # Draw switchbox boundary. + self.top_face.draw(scr, tx, font, color, thickness, **options) + self.bottom_face.draw(scr, tx, font, color, thickness, **options) + self.left_face.draw(scr, tx, font, color, thickness, **options) + self.right_face.draw(scr, tx, font, color, thickness, **options) + + if options.get("draw_switchbox_routing"): + # Draw routed wire segments. + try: + for segments in self.segments.values(): + for segment in segments: + draw_seg(segment, scr, tx, dot_radius=0) + except AttributeError: + pass + + if options.get("draw_routing_channels"): + # Draw routing channels from midpoint of one switchbox face to midpoint of another. + + def draw_channel(face1, face2): + seg1 = face1.seg + seg2 = face2.seg + p1 = (seg1.p1 + seg1.p2) / 2 + p2 = (seg2.p1 + seg2.p2) / 2 + draw_seg(Segment(p1, p2), scr, tx, (128, 0, 128), 1, dot_radius=0) + + draw_channel(self.top_face, self.bottom_face) + draw_channel(self.top_face, self.left_face) + draw_channel(self.top_face, self.right_face) + draw_channel(self.bottom_face, self.left_face) + draw_channel(self.bottom_face, self.right_face) + draw_channel(self.left_face, self.right_face) + + if do_start_end: + # Terminate PyGame. + draw_end() + + +@export_to_all +class Router: + """Mixin to add routing function to Node class.""" + + def add_routing_points(node, nets): + """Add routing points by extending wires from pins out to the edge of the part bounding box. + + Args: + nets (list): List of nets to be routed. + """ + + def add_routing_pt(pin): + """Add the point for a pin on the boundary of a part.""" + + bbox = pin.part.lbl_bbox + pin.route_pt = copy.copy(pin.pt) + if pin.orientation == "U": + # Pin points up, so extend downward to the bottom of the bounding box. + pin.route_pt.y = bbox.min.y + elif pin.orientation == "D": + # Pin points down, so extend upward to the top of the bounding box. + pin.route_pt.y = bbox.max.y + elif pin.orientation == "L": + # Pin points left, so extend rightward to the right-edge of the bounding box. + pin.route_pt.x = bbox.max.x + elif pin.orientation == "R": + # Pin points right, so extend leftward to the left-edge of the bounding box. + pin.route_pt.x = bbox.min.x + else: + raise RuntimeError("Unknown pin orientation.") + + # Global set of part pin (x,y) points may have stuff from processing previous nodes, so clear it. + del pin_pts[:] # Clear the list. Works for Python 2 and 3. + + for net in nets: + # Add routing points for all pins on the net that are inside this node. + for pin in node.get_internal_pins(net): + # Store the point where the pin is. (This is used after routing to trim wire stubs.) + pin_pts.append((pin.pt * pin.part.tx).round()) + + # Add the point to which the wiring should be extended. + add_routing_pt(pin) + + # Add a wire to connect the part pin to the routing point on the bounding box periphery. + if pin.route_pt != pin.pt: + seg = Segment(pin.pt, pin.route_pt) * pin.part.tx + node.wires[pin.net].append(seg) + + def create_routing_tracks(node, routing_bbox): + """Create horizontal & vertical global routing tracks.""" + + # Find the coords of the horiz/vert tracks that will hold the H/V faces of the routing switchboxes. + v_track_coord = [] + h_track_coord = [] + + # The top/bottom/left/right of each part's labeled bounding box define the H/V tracks. + for part in node.parts: + bbox = (part.lbl_bbox * part.tx).round() + v_track_coord.append(bbox.min.x) + v_track_coord.append(bbox.max.x) + h_track_coord.append(bbox.min.y) + h_track_coord.append(bbox.max.y) + + # Create delimiting tracks around the routing area. Just take the number of nets to be routed + # and create a channel that size around the periphery. That's guaranteed to be big enough. + # This is overkill but probably not worth optimizing since any excess boundary area is ignored. + v_track_coord.append(routing_bbox.min.x) + v_track_coord.append(routing_bbox.max.x) + h_track_coord.append(routing_bbox.min.y) + h_track_coord.append(routing_bbox.max.y) + + # Remove any duplicate track coords and then sort them. + v_track_coord = list(set(v_track_coord)) + h_track_coord = list(set(h_track_coord)) + v_track_coord.sort() + h_track_coord.sort() + + # Create an H/V track for each H/V coord containing a list for holding the faces in that track. + v_tracks = [ + GlobalTrack(orientation=VERT, idx=idx, coord=coord) + for idx, coord in enumerate(v_track_coord) + ] + h_tracks = [ + GlobalTrack(orientation=HORZ, idx=idx, coord=coord) + for idx, coord in enumerate(h_track_coord) + ] + + def bbox_to_faces(part, bbox): + left_track = v_tracks[v_track_coord.index(bbox.min.x)] + right_track = v_tracks[v_track_coord.index(bbox.max.x)] + bottom_track = h_tracks[h_track_coord.index(bbox.min.y)] + top_track = h_tracks[h_track_coord.index(bbox.max.y)] + Face(part, left_track, bottom_track, top_track) + Face(part, right_track, bottom_track, top_track) + Face(part, bottom_track, left_track, right_track) + Face(part, top_track, left_track, right_track) + if isinstance(part, Part): + part.left_track = left_track + part.right_track = right_track + part.top_track = top_track + part.bottom_track = bottom_track + + # Add routing box faces for each side of a part's labeled bounding box. + for part in node.parts: + part_bbox = (part.lbl_bbox * part.tx).round() + bbox_to_faces(part, part_bbox) + + # Add routing box faces for each side of the expanded bounding box surrounding all parts. + bbox_to_faces(boundary, routing_bbox) + + # Extend the part faces in each horizontal track and then each vertical track. + for track in h_tracks: + track.extend_faces(v_tracks) + for track in v_tracks: + track.extend_faces(h_tracks) + + # Apply splits to all faces and combine coincident faces. + for track in h_tracks + v_tracks: + track.split_faces() + track.remove_duplicate_faces() + + # Add adjacencies between faces that define global routing paths within switchboxes. + for h_track in h_tracks[1:]: + h_track.add_adjacencies() + + return h_tracks, v_tracks + + def create_terminals(node, internal_nets, h_tracks, v_tracks): + """Create terminals on the faces in the routing tracks.""" + + # Add terminals to all non-part/non-boundary faces. + for track in h_tracks + v_tracks: + for face in track: + face.create_nonpin_terminals() + + # Add terminals to switchbox faces for all part pins on internal nets. + for net in internal_nets: + for pin in node.get_internal_pins(net): + # Find the track (top/bottom/left/right) that the pin is on. + part = pin.part + pt = pin.route_pt * part.tx + closest_dist = abs(pt.y - part.top_track.coord) + pin_track = part.top_track + coord = pt.x # Pin coord within top track. + dist = abs(pt.y - part.bottom_track.coord) + if dist < closest_dist: + closest_dist = dist + pin_track = part.bottom_track + coord = pt.x # Pin coord within bottom track. + dist = abs(pt.x - part.left_track.coord) + if dist < closest_dist: + closest_dist = dist + pin_track = part.left_track + coord = pt.y # Pin coord within left track. + dist = abs(pt.x - part.right_track.coord) + if dist < closest_dist: + closest_dist = dist + pin_track = part.right_track + coord = pt.y # Pin coord within right track. + + # Now search for the face in the track that the pin is on. + for face in pin_track: + if part in face.part and face.beg.coord <= coord <= face.end.coord: + if not getattr(pin, "face", None): + # Only assign pin to face if it hasn't already been assigned to + # another face. This handles the case where a pin is exactly + # at the end coordinate and beginning coordinate of two + # successive faces in the same track. + pin.face = face + face.pins.append(pin) + terminal = Terminal(pin.net, face, coord) + face.terminals.append(terminal) + break + + # Set routing capacity of faces based on # of terminals on each face. + for track in h_tracks + v_tracks: + for face in track: + face.set_capacity() + + def global_router(node, nets): + """Globally route a list of nets from face to face. + + Args: + nets (list): List of Nets to be routed. + + Returns: + List: List of GlobalRoutes. + """ + + # This maze router assembles routes from each pin sequentially. + # + # 1. Find faces with net pins on them and place them on the + # start_faces list. + # 2. Randomly select one of the start faces. Add all the other + # faces to the stop_faces list. + # 3. Find a route from the start face to closest stop face. + # This concludes the initial phase of the routing. + # 4. Iterate through the remaining faces on the start_faces list. + # a. Randomly select a start face. + # b. Set stop faces to be all the faces currently on + # global routes. + # c. Find a route from the start face to any face on + # the global routes, thus enlarging the set of + # contiguous routes while reducing the number of + # unrouted start faces. + # d. Add the faces on the new route to the stop_faces list. + + # Core routing function. + def rt_srch(start_face, stop_faces): + """Return a minimal-distance path from the start face to one of the stop faces. + + Args: + start_face (Face): Face from which path search begins + stop_faces (List): List of Faces at which search will end. + + Raises: + RoutingFailure: No path was found. + + Returns: + GlobalWire: List of Faces from start face to one of the stop faces. + """ + + # Return empty path if no stop faces or already starting from a stop face. + if start_face in stop_faces or not stop_faces: + return GlobalWire(net) + + # Record faces that have been visited and their distance from the start face. + visited_faces = [start_face] + start_face.dist_from_start = 0 + + # Path searches are allowed to touch a Face on a Part if it + # has a Pin on the net being routed or if it is one of the stop faces. + # This is necessary to allow a search to terminate on a stop face or to + # pass through a face with a net pin on the way to finding a connection + # to one of the stop faces. + unconstrained_faces = stop_faces | net_pin_faces + + # Search through faces until a path is found & returned or a routing exception occurs. + while True: + # Set up for finding the closest unvisited face. + closest_dist = float("inf") + closest_face = None + + # Search for the closest face adjacent to the visited faces. + visited_faces.sort(key=lambda f: f.dist_from_start) + for visited_face in visited_faces: + if visited_face.dist_from_start > closest_dist: + # Visited face is already further than the current + # closest face, so no use continuing search since + # any remaining visited faces are even more distant. + break + + # Get the distances to the faces adjacent to this previously-visited face + # and update the closest face if appropriate. + for adj in visited_face.adjacent: + if adj.face in visited_faces: + # Don't re-visit faces that have already been visited. + continue + + if ( + adj.face not in unconstrained_faces + and adj.face.capacity <= 0 + ): + # Skip faces with insufficient routing capacity. + continue + + # Compute distance of this adjacent face to the start face. + dist = visited_face.dist_from_start + adj.dist + + if dist < closest_dist: + # Record the closest face seen so far. + closest_dist = dist + closest_face = adj.face + closest_face.prev_face = visited_face + + if not closest_face: + # Exception raised if couldn't find a path from start to stop faces. + raise GlobalRoutingFailure( + "Global routing failure: {net.name} {net} {start_face.pins}".format( + **locals() + ) + ) + + # Add the closest adjacent face to the list of visited faces. + closest_face.dist_from_start = closest_dist + visited_faces.append(closest_face) + + if closest_face in stop_faces: + # The newest, closest face is actually on the list of stop faces, so the search is done. + # Now search back from this face to find the path back to the start face. + face_path = [closest_face] + while face_path[-1] is not start_face: + face_path.append(face_path[-1].prev_face) + + # Decrement the routing capacities of the path faces to account for this new routing. + # Don't decrement the stop face because any routing through it was accounted for + # during a previous routing. + for face in face_path[:-1]: + if face.capacity > 0: + face.capacity -= 1 + + # Reverse face path to go from start-to-stop face and return it. + return GlobalWire(net, reversed(face_path)) + + # Key function for setting the order in which nets will be globally routed. + def rank_net(net): + """Rank net based on W/H of bounding box of pins and the # of pins.""" + + # Nets with a small bounding box probably have fewer routing resources + # so they should be routed first. + + bbox = BBox() + for pin in node.get_internal_pins(net): + bbox.add(pin.route_pt) + return (bbox.w + bbox.h, len(net.pins)) + + # Set order in which nets will be routed. + nets.sort(key=rank_net) + + # Globally route each net. + global_routes = [] + + for net in nets: + # List for storing GlobalWires connecting pins on net. + global_route = GlobalRoute() + + # Faces with pins from which paths/routing originate. + net_pin_faces = {pin.face for pin in node.get_internal_pins(net)} + start_faces = set(net_pin_faces) + + # Select a random start face and look for a route to *any* of the other start faces. + start_face = random.choice(list(start_faces)) + start_faces.discard(start_face) + stop_faces = set(start_faces) + initial_route = rt_srch(start_face, stop_faces) + global_route.append(initial_route) + + # The faces on the route that was found now become the stopping faces for any further routing. + stop_faces = set(initial_route) + + # Go thru the other start faces looking for a connection to any existing route. + for start_face in start_faces: + next_route = rt_srch(start_face, stop_faces) + global_route.append(next_route) + + # Update the set of stopping faces with the faces on the newest route. + stop_faces |= set(next_route) + + # Add the complete global route for this net to the list of global routes. + global_routes.append(global_route) + + return global_routes + + def create_switchboxes(node, h_tracks, v_tracks, **options): + """Create routing switchboxes from the faces in the horz/vert tracks. + + Args: + h_tracks (list): List of horizontal Tracks. + v_tracks (list): List of vertical Tracks. + options (dict, optional): Dictionary of options and values. + + Returns: + list: List of Switchboxes. + """ + + # Clear any switchboxes associated with faces because we'll be making new ones. + for track in h_tracks + v_tracks: + for face in track: + face.switchboxes.clear() + + # For each horizontal Face, create a switchbox where it is the top face of the box. + switchboxes = [] + for h_track in h_tracks[1:]: + for face in h_track: + try: + # Create a Switchbox with the given Face on top and add it to the list. + switchboxes.append(SwitchBox(face)) + except NoSwitchBox: + continue + + # Check the switchboxes for problems. + for swbx in switchboxes: + swbx.audit() + + # Small switchboxes are more likely to fail routing so try to combine them into larger switchboxes. + # Use switchboxes containing nets for routing as seeds for coalescing into larger switchboxes. + seeds = [swbx for swbx in switchboxes if swbx.has_nets()] + + # Sort seeds by perimeter so smaller ones are coalesced before larger ones. + seeds.sort(key=lambda swbx: swbx.bbox.w + swbx.bbox.h) + + # Coalesce smaller switchboxes into larger ones having more routing area. + # The smaller switchboxes are removed from the list of switchboxes. + switchboxes = [seed.coalesce(switchboxes) for seed in seeds] + switchboxes = [swbx for swbx in switchboxes if swbx] # Remove None boxes. + + # A coalesced switchbox may have non-part faces containing multiple terminals + # on the same net. Remove all but one to prevent multi-path routes. + for switchbox in switchboxes: + switchbox.trim_repeated_terminals() + + return switchboxes + + def switchbox_router(node, switchboxes, **options): + """Create detailed wiring between the terminals along the sides of each switchbox. + + Args: + switchboxes (list): List of SwitchBox objects to be individually routed. + options (dict, optional): Dictionary of options and values. + + Returns: + None + """ + + # Do detailed routing inside each switchbox. + # TODO: Switchboxes are independent so could they be routed in parallel? + for swbx in switchboxes: + try: + # Try routing switchbox from left-to-right. + swbx.route(**options) + + except RoutingFailure: + # Routing failed, so try routing top-to-bottom instead. + swbx.flip_xy() + # If this fails, then a routing exception will terminate the whole routing process. + swbx.route(**options) + swbx.flip_xy() + + # Add switchbox routes to existing node wiring. + for net, segments in swbx.segments.items(): + node.wires[net].extend(segments) + + def cleanup_wires(node): + """Try to make wire segments look prettier.""" + + def order_seg_points(segments): + """Order endpoints in a horizontal or vertical segment.""" + for seg in segments: + if seg.p2 < seg.p1: + seg.p1, seg.p2 = seg.p2, seg.p1 + + def segments_bbox(segments): + """Return bounding box containing the given list of segments.""" + seg_pts = list(chain(*((s.p1, s.p2) for s in segments))) + return BBox(*seg_pts) + + def extract_horz_vert_segs(segments): + """Separate segments and return lists of horizontal & vertical segments.""" + horz_segs = [seg for seg in segments if seg.p1.y == seg.p2.y] + vert_segs = [seg for seg in segments if seg.p1.x == seg.p2.x] + assert len(horz_segs) + len(vert_segs) == len(segments) + return horz_segs, vert_segs + + def split_segments(segments, net_pin_pts): + """Return list of net segments split into the smallest intervals without intersections with other segments.""" + + # Check each horizontal segment against each vertical segment and split each one if they intersect. + # (This clunky iteration is used so the horz/vert lists can be updated within the loop.) + horz_segs, vert_segs = extract_horz_vert_segs(segments) + i = 0 + while i < len(horz_segs): + hseg = horz_segs[i] + hseg_y = hseg.p1.y + j = 0 + while j < len(vert_segs): + vseg = vert_segs[j] + vseg_x = vseg.p1.x + if ( + hseg.p1.x <= vseg_x <= hseg.p2.x + and vseg.p1.y <= hseg_y <= vseg.p2.y + ): + int_pt = Point(vseg_x, hseg_y) + if int_pt != hseg.p1 and int_pt != hseg.p2: + horz_segs.append( + Segment(copy.copy(int_pt), copy.copy(hseg.p2)) + ) + hseg.p2 = copy.copy(int_pt) + if int_pt != vseg.p1 and int_pt != vseg.p2: + vert_segs.append( + Segment(copy.copy(int_pt), copy.copy(vseg.p2)) + ) + vseg.p2 = copy.copy(int_pt) + j += 1 + i += 1 + + i = 0 + while i < len(horz_segs): + hseg = horz_segs[i] + hseg_y = hseg.p1.y + for pt in net_pin_pts: + if pt.y == hseg_y and hseg.p1.x < pt.x < hseg.p2.x: + horz_segs.append(Segment(copy.copy(pt), copy.copy(hseg.p2))) + hseg.p2 = copy.copy(pt) + i += 1 + + j = 0 + while j < len(vert_segs): + vseg = vert_segs[j] + vseg_x = vseg.p1.x + for pt in net_pin_pts: + if pt.x == vseg_x and vseg.p1.y < pt.y < vseg.p2.y: + vert_segs.append(Segment(copy.copy(pt), copy.copy(vseg.p2))) + vseg.p2 = copy.copy(pt) + j += 1 + + return horz_segs + vert_segs + + def merge_segments(segments): + """Return segments after merging those that run the same direction and overlap.""" + + # Preprocess the segments. + horz_segs, vert_segs = extract_horz_vert_segs(segments) + + merged_segs = [] + + # Separate horizontal segments having the same Y coord. + horz_segs_v = defaultdict(list) + for seg in horz_segs: + horz_segs_v[seg.p1.y].append(seg) + + # Merge overlapping segments having the same Y coord. + for segs in horz_segs_v.values(): + # Order segments by their starting X coord. + segs.sort(key=lambda s: s.p1.x) + # Append first segment to list of merged segments. + merged_segs.append(segs[0]) + # Go thru the remaining segments looking for overlaps with the last entry on the merge list. + for seg in segs[1:]: + if seg.p1.x <= merged_segs[-1].p2.x: + # Segments overlap, so update the extent of the last entry. + merged_segs[-1].p2.x = max(seg.p2.x, merged_segs[-1].p2.x) + else: + # No overlap, so append the current segment to the merge list and use it for + # further checks of intersection with remaining segments. + merged_segs.append(seg) + + # Separate vertical segments having the same X coord. + vert_segs_h = defaultdict(list) + for seg in vert_segs: + vert_segs_h[seg.p1.x].append(seg) + + # Merge overlapping segments having the same X coord. + for segs in vert_segs_h.values(): + # Order segments by their starting Y coord. + segs.sort(key=lambda s: s.p1.y) + # Append first segment to list of merged segments. + merged_segs.append(segs[0]) + # Go thru the remaining segments looking for overlaps with the last entry on the merge list. + for seg in segs[1:]: + if seg.p1.y <= merged_segs[-1].p2.y: + # Segments overlap, so update the extent of the last entry. + merged_segs[-1].p2.y = max(seg.p2.y, merged_segs[-1].p2.y) + else: + # No overlap, so append the current segment to the merge list and use it for + # further checks of intersection with remaining segments. + merged_segs.append(seg) + + return merged_segs + + def break_cycles(segments): + """Remove segments to break any cycles of a net's segments.""" + + # Create a dict storing set of segments adjacent to each endpoint. + adj_segs = defaultdict(set) + for seg in segments: + # Add segment to set for each endpoint. + adj_segs[seg.p1].add(seg) + adj_segs[seg.p2].add(seg) + + # Create a dict storing the list of endpoints adjacent to each endpoint. + adj_pts = dict() + for pt, segs in adj_segs.items(): + # Store endpoints of all segments adjacent to endpoint, then remove the endpoint. + adj_pts[pt] = list({p for seg in segs for p in (seg.p1, seg.p2)}) + adj_pts[pt].remove(pt) + + # Start at any endpoint and visit adjacent endpoints until all have been visited. + # If an endpoint is seen more than once, then a cycle exists. Remove the segment forming the cycle. + visited_pts = [] # List of visited endpoints. + frontier_pts = list(adj_pts.keys())[:1] # Arbitrary starting point. + while frontier_pts: + # Visit a point on the frontier. + frontier_pt = frontier_pts.pop() + visited_pts.append(frontier_pt) + + # Check each adjacent endpoint for cycles. + for adj_pt in adj_pts[frontier_pt][:]: + if adj_pt in visited_pts + frontier_pts: + # This point was already reached by another path so there is a cycle. + # Break it by removing segment between frontier_pt and adj_pt. + loop_seg = (adj_segs[frontier_pt] & adj_segs[adj_pt]).pop() + segments.remove(loop_seg) + adj_segs[frontier_pt].remove(loop_seg) + adj_segs[adj_pt].remove(loop_seg) + adj_pts[frontier_pt].remove(adj_pt) + adj_pts[adj_pt].remove(frontier_pt) + else: + # First time adjacent point has been reached, so add it to frontier. + frontier_pts.append(adj_pt) + # Keep this new frontier point from backtracking to the current frontier point later. + adj_pts[adj_pt].remove(frontier_pt) + + return segments + + def is_pin_pt(pt): + """Return True if the point is on one of the part pins.""" + return pt in pin_pts + + def contains_pt(seg, pt): + """Return True if the point is contained within the horz/vert segment.""" + return seg.p1.x <= pt.x <= seg.p2.x and seg.p1.y <= pt.y <= seg.p2.y + + def trim_stubs(segments): + """Return segments after removing stubs that have an unconnected endpoint.""" + + def get_stubs(segments): + """Return set of stub segments.""" + + # For end point, the dict entry contains a list of the segments that meet there. + stubs = defaultdict(list) + + # Process the segments looking for points that are on only a single segment. + for seg in segments: + # Add the segment to the segment list of each end point. + stubs[seg.p1].append(seg) + stubs[seg.p2].append(seg) + + # Keep only the segments with an unconnected endpoint that is not on a part pin. + stubs = { + segs[0] + for endpt, segs in stubs.items() + if len(segs) == 1 and not is_pin_pt(endpt) + } + return stubs + + trimmed_segments = set(segments[:]) + stubs = get_stubs(trimmed_segments) + while stubs: + trimmed_segments -= stubs + stubs = get_stubs(trimmed_segments) + return list(trimmed_segments) + + def remove_jogs_old(net, segments, wires, net_bboxes, part_bboxes): + """Remove jogs in wiring segments. + + Args: + net (Net): Net whose wire segments will be modified. + segments (list): List of wire segments for the given net. + wires (dict): Dict of lists of wire segments indexed by nets. + net_bboxes (dict): Dict of BBoxes for wire segments indexed by nets. + part_bboxes (list): List of BBoxes for the placed parts. + """ + + def get_touching_segs(seg, ortho_segs): + """Return list of orthogonal segments that touch the given segment.""" + touch_segs = set() + for oseg in ortho_segs: + # oseg horz, seg vert. Do they intersect? + if contains_pt(oseg, Point(seg.p2.x, oseg.p1.y)): + touch_segs.add(oseg) + # oseg vert, seg horz. Do they intersect? + elif contains_pt(oseg, Point(oseg.p2.x, seg.p1.y)): + touch_segs.add(oseg) + return list(touch_segs) # Convert to list with no dups. + + def get_overlap(*segs): + """Find extent of overlap of parallel horz/vert segments and return as (min, max) tuple.""" + ov1 = float("-inf") + ov2 = float("inf") + for seg in segs: + if seg.p1.y == seg.p2.y: + # Horizontal segment. + p1, p2 = seg.p1.x, seg.p2.x + else: + # Vertical segment. + p1, p2 = seg.p1.y, seg.p2.y + ov1 = max(ov1, p1) # Max of extent minimums. + ov2 = min(ov2, p2) # Min of extent maximums. + # assert ov1 <= ov2 + return ov1, ov2 + + def obstructed(segment): + """Return true if segment obstructed by parts or segments of other nets.""" + + # Obstructed if segment bbox intersects one of the part bboxes. + segment_bbox = BBox(segment.p1, segment.p2) + for part_bbox in part_bboxes: + if part_bbox.intersects(segment_bbox): + return True + + # BBoxes don't intersect if they line up exactly edge-to-edge. + # So expand the segment bbox slightly so intersections with bboxes of + # other segments will be detected. + segment_bbox = segment_bbox.resize(Vector(1, 1)) + + # Look for an overlay intersection with a segment of another net. + for nt, nt_bbox in net_bboxes.items(): + if nt is net: + # Don't check this segment with other segments of its own net. + continue + + if not segment_bbox.intersects(nt_bbox): + # Don't check this segment against segments of another net whose + # bbox doesn't even intersect this segment. + continue + + # Check for overlay intersectionss between this segment and the + # parallel segments of the other net. + for seg in wires[nt]: + if segment.p1.x == segment.p2.x == seg.p1.x == seg.p2.x: + # Segments are both aligned vertically on the same track X coord. + if segment.p1.y <= seg.p2.y and segment.p2.y >= seg.p1.y: + # Segments overlap so segment is obstructed. + return True + elif segment.p1.y == segment.p2.y == seg.p1.y == seg.p2.y: + # Segments are both aligned horizontally on the same track Y coord. + if segment.p1.x <= seg.p2.x and segment.p2.x >= seg.p1.x: + # Segments overlap so segment is obstructed. + return True + + # No obstructions found, so return False. + return False + + # Make sure p1 <= p2 for segment endpoints. + order_seg_points(segments) + + # Split segments into horizontal/vertical groups. + horz_segs, vert_segs = extract_horz_vert_segs(segments) + + # Look for a segment touched by ends of orthogonal segments all pointing in the same direction. + # Then slide this segment to the other end of the interval by which the touching segments + # overlap. This will reduce or eliminate the jog. + stop = True + for segs, ortho_segs in ((horz_segs, vert_segs), (vert_segs, horz_segs)): + for seg in segs: + # Don't move a segment if one of its endpoints connects to a part pin. + if is_pin_pt(seg.p1) or is_pin_pt(seg.p2): + continue + + # Find all orthogonal segments that touch this one. + touching_segs = get_touching_segs(seg, ortho_segs) + + # Find extent of overlap of all orthogonal segments. + ov1, ov2 = get_overlap(*touching_segs) + + if ov1 >= ov2: + # No overlap, so this segment can't be moved one way or the other. + continue + + if seg in horz_segs: + # Move horz segment vertically to other end of overlap to remove jog. + test_seg = Segment(seg.p1, seg.p2) + seg_y = test_seg.p1.y + if seg_y == ov1: + # Segment is at one end of the overlay, so move it to the other end. + test_seg.p1.y = ov2 + test_seg.p2.y = ov2 + if not obstructed(test_seg): + # Segment move is not obstructed, so accept it. + seg.p1 = test_seg.p1 + seg.p2 = test_seg.p2 + # If one segment is moved, maybe more can be moved so don't stop. + stop = False + elif seg_y == ov2: + # Segment is at one end of the overlay, so move it to the other end. + test_seg.p1.y = ov1 + test_seg.p2.y = ov1 + if not obstructed(test_seg): + # Segment move is not obstructed, so accept it. + seg.p1 = test_seg.p1 + seg.p2 = test_seg.p2 + # If one segment is moved, maybe more can be moved so don't stop. + stop = False + else: + # Segment in interior of overlay, so it's not a jog. Don't move it. + pass + else: + # Move vert segment horizontally to other end of overlap to remove jog. + test_seg = Segment(seg.p1, seg.p2) + seg_x = seg.p1.x + if seg_x == ov1: + # Segment is at one end of the overlay, so move it to the other end. + test_seg.p1.x = ov2 + test_seg.p2.x = ov2 + if not obstructed(test_seg): + # Segment move is not obstructed, so accept it. + seg.p1 = test_seg.p1 + seg.p2 = test_seg.p2 + # If one segment is moved, maybe more can be moved so don't stop. + stop = False + elif seg_x == ov2: + # Segment is at one end of the overlay, so move it to the other end. + test_seg.p1.x = ov1 + test_seg.p2.x = ov1 + if not obstructed(test_seg): + # Segment move is not obstructed, so accept it. + seg.p1 = test_seg.p1 + seg.p2 = test_seg.p2 + # If one segment is moved, maybe more can be moved so don't stop. + stop = False + else: + # Segment in interior of overlay, so it's not a jog. Don't move it. + pass + + # Return updated segments. If no segments for this net were updated, then stop is True. + return segments, stop + + def remove_jogs(net, segments, wires, net_bboxes, part_bboxes): + """Remove jogs and staircases in wiring segments. + + Args: + net (Net): Net whose wire segments will be modified. + segments (list): List of wire segments for the given net. + wires (dict): Dict of lists of wire segments indexed by nets. + net_bboxes (dict): Dict of BBoxes for wire segments indexed by nets. + part_bboxes (list): List of BBoxes for the placed parts. + """ + + def obstructed(segment): + """Return true if segment obstructed by parts or segments of other nets.""" + + # Obstructed if segment bbox intersects one of the part bboxes. + segment_bbox = BBox(segment.p1, segment.p2) + for part_bbox in part_bboxes: + if part_bbox.intersects(segment_bbox): + return True + + # BBoxes don't intersect if they line up exactly edge-to-edge. + # So expand the segment bbox slightly so intersections with bboxes of + # other segments will be detected. + segment_bbox = segment_bbox.resize(Vector(2, 2)) + + # Look for an overlay intersection with a segment of another net. + for nt, nt_bbox in net_bboxes.items(): + if nt is net: + # Don't check this segment with other segments of its own net. + continue + + if not segment_bbox.intersects(nt_bbox): + # Don't check this segment against segments of another net whose + # bbox doesn't even intersect this segment. + continue + + # Check for overlay intersectionss between this segment and the + # parallel segments of the other net. + for seg in wires[nt]: + if segment.p1.x == segment.p2.x == seg.p1.x == seg.p2.x: + # Segments are both aligned vertically on the same track X coord. + if segment.p1.y <= seg.p2.y and segment.p2.y >= seg.p1.y: + # Segments overlap so segment is obstructed. + return True + elif segment.p1.y == segment.p2.y == seg.p1.y == seg.p2.y: + # Segments are both aligned horizontally on the same track Y coord. + if segment.p1.x <= seg.p2.x and segment.p2.x >= seg.p1.x: + # Segments overlap so segment is obstructed. + return True + + # No obstructions found, so return False. + return False + + def get_corners(segments): + """Return dictionary of right-angle corner points and lists of associated segments.""" + + # For each corner point, the dict entry contains a list of the segments that meet there. + corners = defaultdict(list) + + # Process the segments so that any potential right-angle corner has the horizontal + # segment followed by the vertical segment. + horz_segs, vert_segs = extract_horz_vert_segs(segments) + for seg in horz_segs + vert_segs: + # Add the segment to the segment list of each end point. + corners[seg.p1].append(seg) + corners[seg.p2].append(seg) + + # Keep only the corner points where two segments meet at right angles at a point not on a part pin. + corners = { + corner: segs + for corner, segs in corners.items() + if len(segs) == 2 + and not is_pin_pt(corner) + and segs[0] in horz_segs + and segs[1] in vert_segs + } + return corners + + def get_jogs(segments): + """Yield the three segments and starting and end points of a staircase or tophat jog.""" + + # Get dict of right-angle corners formed by segments. + corners = get_corners(segments) + + # Look for segments with both endpoints on right-angle corners, indicating this segment + # is in the middle of a three-segment staircase or tophat jog. + for segment in segments: + if segment.p1 in corners and segment.p2 in corners: + # Get the three segments in the jog. + jog_segs = set() + jog_segs.add(corners[segment.p1][0]) + jog_segs.add(corners[segment.p1][1]) + jog_segs.add(corners[segment.p2][0]) + jog_segs.add(corners[segment.p2][1]) + + # Get the points where the three-segment jog starts and stops. + start_stop_pts = set() + for seg in jog_segs: + start_stop_pts.add(seg.p1) + start_stop_pts.add(seg.p2) + start_stop_pts.discard(segment.p1) + start_stop_pts.discard(segment.p2) + + # Send the jog that was found. + yield list(jog_segs), list(start_stop_pts) + + # Shuffle segments to vary the order of detected jogs. + random.shuffle(segments) + + # Get iterator for jogs. + jogs = get_jogs(segments) + + # Search for jogs and break from the loop if a correctable jog is found or we run out of jogs. + while True: + # Get the segments and start-stop points for the next jog. + try: + jog_segs, start_stop_pts = next(jogs) + except StopIteration: + # No more jogs and no corrections made, so return segments and stop flag is true. + return segments, True + + # Get the start-stop points and order them so p1 < p3. + p1, p3 = sorted(start_stop_pts) + + # These are the potential routing points for correcting the jog. + # Either start at p1 and move vertically and then horizontally to p3, or + # move horizontally from p1 and then vertically to p3. + p2s = [Point(p1.x, p3.y), Point(p3.x, p1.y)] + + # Shuffle the routing points so the applied correction isn't always the same orientation. + random.shuffle(p2s) + + # Check each routing point to see if it leads to a valid routing. + for p2 in p2s: + # Replace the three-segment jog with these two right-angle segments. + new_segs = [ + Segment(copy.copy(pa), copy.copy(pb)) + for pa, pb in ((p1, p2), (p2, p3)) + if pa != pb + ] + order_seg_points(new_segs) + + # Check the new segments to see if they run into parts or segments of other nets. + if not any((obstructed(new_seg) for new_seg in new_segs)): + # OK, segments are good so replace the old segments in the jog with them. + for seg in jog_segs: + segments.remove(seg) + segments.extend(new_segs) + + # Return updated segments and set stop flag to false because segments were modified. + return segments, False + + # Get part bounding boxes so parts can be avoided when modifying net segments. + part_bboxes = [p.bbox * p.tx for p in node.parts] + + # Get dict of bounding boxes for the nets in this node. + net_bboxes = {net: segments_bbox(segs) for net, segs in node.wires.items()} + + # Get locations for part pins of each net. (For use when splitting net segments.) + net_pin_pts = dict() + for net in node.wires.keys(): + net_pin_pts[net] = [ + (pin.pt * pin.part.tx).round() for pin in node.get_internal_pins(net) + ] + + # Do a generalized cleanup of the wire segments of each net. + for net, segments in node.wires.items(): + # Round the wire segment endpoints to integers. + segments = [seg.round() for seg in segments] + + # Keep only non zero-length segments. + segments = [seg for seg in segments if seg.p1 != seg.p2] + + # Make sure the segment endpoints are in the right order. + order_seg_points(segments) + + # Merge colinear, overlapping segments. Also removes any duplicated segments. + segments = merge_segments(segments) + + # Split intersecting segments. + segments = split_segments(segments, net_pin_pts[net]) + + # Break loops of segments. + segments = break_cycles(segments) + + # Keep only non zero-length segments. + segments = [seg for seg in segments if seg.p1 != seg.p2] + + # Trim wire stubs. + segments = trim_stubs(segments) + + node.wires[net] = segments + + # Remove jogs in the wire segments of each net. + keep_cleaning = True + while keep_cleaning: + keep_cleaning = False + + for net, segments in node.wires.items(): + while True: + # Split intersecting segments. + segments = split_segments(segments, net_pin_pts[net]) + + # Remove unnecessary wire jogs. + segments, stop = remove_jogs( + net, segments, node.wires, net_bboxes, part_bboxes + ) + + # Keep only non zero-length segments. + segments = [seg for seg in segments if seg.p1 != seg.p2] + + # Merge segments made colinear by removing jogs. + segments = merge_segments(segments) + + # Split intersecting segments. + segments = split_segments(segments, net_pin_pts[net]) + + # Keep only non zero-length segments. + segments = [seg for seg in segments if seg.p1 != seg.p2] + + # Trim wire stubs caused by removing jogs. + segments = trim_stubs(segments) + + if stop: + # Break from loop once net segments can no longer be improved. + break + + # Recalculate the net bounding box after modifying its segments. + net_bboxes[net] = segments_bbox(segments) + + keep_cleaning = True + + # Merge segments made colinear by removing jogs. + segments = merge_segments(segments) + + # Update the node net's wire with the cleaned version. + node.wires[net] = segments + + def add_junctions(node): + """Add X & T-junctions where wire segments in the same net meet.""" + + def find_junctions(route): + """Find junctions where segments of a net intersect. + + Args: + route (List): List of Segment objects. + + Returns: + List: List of Points, one for each junction. + + Notes: + You must run merge_segments() before finding junctions + or else the segment endpoints might not be ordered + correctly with p1 < p2. + """ + + # Separate route into vertical and horizontal segments. + horz_segs = [seg for seg in route if seg.p1.y == seg.p2.y] + vert_segs = [seg for seg in route if seg.p1.x == seg.p2.x] + + junctions = [] + + # Check each pair of horz/vert segments for an intersection, except + # where they form a right-angle turn. + for hseg in horz_segs: + hseg_y = hseg.p1.y # Horz seg Y coord. + for vseg in vert_segs: + vseg_x = vseg.p1.x # Vert seg X coord. + if (hseg.p1.x < vseg_x < hseg.p2.x) and ( + vseg.p1.y <= hseg_y <= vseg.p2.y + ): + # The vert segment intersects the interior of the horz seg. + junctions.append(Point(vseg_x, hseg_y)) + elif (vseg.p1.y < hseg_y < vseg.p2.y) and ( + hseg.p1.x <= vseg_x <= hseg.p2.x + ): + # The horz segment intersects the interior of the vert seg. + junctions.append(Point(vseg_x, hseg_y)) + + return junctions + + for net, segments in node.wires.items(): + # Add X & T-junctions between segments in the same net. + junctions = find_junctions(segments) + node.junctions[net].extend(junctions) + + def rmv_routing_stuff(node): + """Remove attributes added to parts/pins during routing.""" + + rmv_attr(node.parts, ("left_track", "right_track", "top_track", "bottom_track")) + for part in node.parts: + rmv_attr(part.pins, ("route_pt", "face")) + + def route(node, tool=None, **options): + """Route the wires between part pins in this node and its children. + + Steps: + 1. Divide the bounding box surrounding the parts into switchboxes. + 2. Do global routing of nets through sequences of switchboxes. + 3. Do detailed routing within each switchbox. + + Args: + node (Node): Hierarchical node containing the parts to be connected. + tool (str): Backend tool for schematics. + options (dict, optional): Dictionary of options and values: + "allow_routing_failure", "draw", "draw_all_terminals", "show_capacities", + "draw_switchbox", "draw_routing", "draw_channels" + """ + + # Inject the constants for the backend tool into this module. + import skidl + from skidl.tools import tool_modules + + tool = tool or skidl.config.tool + this_module = sys.modules[__name__] + this_module.__dict__.update(tool_modules[tool].constants.__dict__) + + random.seed(options.get("seed")) + + # Remove any stuff leftover from a previous place & route run. + node.rmv_routing_stuff() + + # First, recursively route any children of this node. + # TODO: Child nodes are independent so could they be processed in parallel? + for child in node.children.values(): + child.route(tool=tool, **options) + + # Exit if no parts to route in this node. + if not node.parts: + return + + # Get all the nets that have one or more pins within this node. + internal_nets = node.get_internal_nets() + + # Exit if no nets to route. + if not internal_nets: + return + + try: + # Extend routing points of part pins to the edges of their bounding boxes. + node.add_routing_points(internal_nets) + + # Create the surrounding box that contains the entire routing area. + channel_sz = (len(internal_nets) + 1) * GRID + routing_bbox = ( + node.internal_bbox().resize(Vector(channel_sz, channel_sz)) + ).round() + + # Create horizontal & vertical global routing tracks and faces. + h_tracks, v_tracks = node.create_routing_tracks(routing_bbox) + + # Create terminals on the faces in the routing tracks. + node.create_terminals(internal_nets, h_tracks, v_tracks) + + # Draw part outlines, routing tracks and terminals. + if options.get("draw_routing_channels"): + draw_routing( + node, routing_bbox, node.parts, h_tracks, v_tracks, **options + ) + + # Do global routing of nets internal to the node. + global_routes = node.global_router(internal_nets) + + # Convert the global face-to-face routes into terminals on the switchboxes. + for route in global_routes: + route.cvt_faces_to_terminals() + + # If enabled, draw the global routing for debug purposes. + if options.get("draw_global_routing"): + draw_routing( + node, + routing_bbox, + node.parts, + h_tracks, + v_tracks, + global_routes, + **options + ) + + # Create detailed wiring using switchbox routing for the global routes. + switchboxes = node.create_switchboxes(h_tracks, v_tracks) + + # Draw switchboxes and routing channels. + if options.get("draw_assigned_terminals"): + draw_routing( + node, + routing_bbox, + node.parts, + switchboxes, + global_routes, + **options + ) + + node.switchbox_router(switchboxes, **options) + + # If enabled, draw the global and detailed routing for debug purposes. + if options.get("draw_switchbox_routing"): + draw_routing( + node, + routing_bbox, + node.parts, + global_routes, + switchboxes, + **options + ) + + # Now clean-up the wires and add junctions. + node.cleanup_wires() + node.add_junctions() + + # If enabled, draw the global and detailed routing for debug purposes. + if options.get("draw_switchbox_routing"): + draw_routing(node, routing_bbox, node.parts, **options) + + # Remove any stuff leftover from this place & route run. + node.rmv_routing_stuff() + + except RoutingFailure: + # Remove any stuff leftover from this place & route run. + node.rmv_routing_stuff() + raise RoutingFailure From ae48b59b945cad75e6c516d018287b36ca934a77 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 15:57:00 +0200 Subject: [PATCH 10/85] Repoint imports --- .../exporters/schematic/kicad/skidl/bboxes.py | 28 ++++++++++------ .../schematic/kicad/skidl/debug_draw.py | 33 ++----------------- .../schematic/kicad/skidl/draw_objs.py | 1 - .../schematic/kicad/skidl/gen_schematic.py | 28 +++++++--------- .../schematic/kicad/skidl/geometry.py | 23 +------------ .../schematic/kicad/skidl/net_terminal.py | 2 -- .../exporters/schematic/kicad/skidl/node.py | 4 +-- .../exporters/schematic/kicad/skidl/place.py | 8 ++--- .../exporters/schematic/kicad/skidl/route.py | 10 ++---- .../exporters/schematic/kicad/skidl/shims.py | 9 +++++ 10 files changed, 47 insertions(+), 99 deletions(-) create mode 100644 src/faebryk/exporters/schematic/kicad/skidl/shims.py diff --git a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py index 0c36d79b..3167e6bf 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py @@ -6,26 +6,35 @@ Calculate bounding boxes for part symbols and hierarchical sheets. """ +import logging from collections import namedtuple -from skidl.logger import active_logger -from skidl.schematics.geometry import ( - Tx, +from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE +from .draw_objs import ( + DrawArc, + DrawCircle, + DrawDef, + DrawF0, + DrawF1, + DrawPin, + DrawPoly, + DrawRect, + DrawText, +) +from .geometry import ( BBox, Point, + Tx, Vector, tx_rot_0, tx_rot_90, tx_rot_180, tx_rot_270, ) -from skidl.utilities import export_to_all -from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE -from skidl.schematics.geometry import BBox, Point, Tx, Vector -from .draw_objs import * + +logger = logging.getLogger(__name__) -@export_to_all def calc_symbol_bbox(part, **options): """ Return the bounding box of the part symbol. @@ -272,7 +281,7 @@ def make_pin_dir_tbl(abs_xoff=20): obj_bbox.add(end) else: - active_logger.error( + logger.error( "Unknown graphical object {} in part symbol {}.".format( type(obj), part.name ) @@ -295,7 +304,6 @@ def make_pin_dir_tbl(abs_xoff=20): return unit_bboxes -@export_to_all def calc_hier_label_bbox(label, dir): """Calculate the bounding box for a hierarchical label. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index e84274a1..12edc084 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -5,19 +5,16 @@ """ Drawing routines used for debugging place & route. """ - from collections import defaultdict from random import randint -from skidl.utilities import export_to_all -from .geometry import BBox, Point, Segment, Tx, Vector +import pygame +from .geometry import BBox, Point, Segment, Tx, Vector # Dictionary for storing colors to visually distinguish routed nets. net_colors = defaultdict(lambda: (randint(0, 200), randint(0, 200), randint(0, 200))) - -@export_to_all def draw_box(bbox, scr, tx, color=(192, 255, 192), thickness=0): """Draw a box in the drawing area. @@ -40,8 +37,6 @@ def draw_box(bbox, scr, tx, color=(192, 255, 192), thickness=0): ) pygame.draw.polygon(scr, color, corners, thickness) - -@export_to_all def draw_endpoint(pt, scr, tx, color=(100, 100, 100), dot_radius=10): """Draw a line segment endpoint in the drawing area. @@ -69,8 +64,6 @@ def draw_endpoint(pt, scr, tx, color=(100, 100, 100), dot_radius=10): radius = dot_radius * tx.a pygame.draw.circle(scr, color, (pt.x, pt.y), radius) - -@export_to_all def draw_seg(seg, scr, tx, color=(100, 100, 100), thickness=5, dot_radius=10): """Draw a line segment in the drawing area. @@ -101,8 +94,6 @@ def draw_seg(seg, scr, tx, color=(100, 100, 100), thickness=5, dot_radius=10): scr, color, (seg.p1.x, seg.p1.y), (seg.p2.x, seg.p2.y), width=thickness ) - -@export_to_all def draw_text(txt, pt, scr, tx, font, color=(100, 100, 100), real=True): """Render text in drawing area. @@ -123,8 +114,6 @@ def draw_text(txt, pt, scr, tx, font, color=(100, 100, 100), real=True): # Render text. font.render_to(scr, (pt.x, pt.y), txt, color) - -@export_to_all def draw_part(part, scr, tx, font): """Draw part bounding box. @@ -149,8 +138,6 @@ def draw_part(part, scr, tx, font): # Probably trying to draw a block of parts which has no pins and can't iterate thru them. pass - -@export_to_all def draw_net(net, parts, scr, tx, font, color=(0, 0, 0), thickness=2, dot_radius=5): """Draw net and connected terminals. @@ -180,8 +167,6 @@ def draw_net(net, parts, scr, tx, font, color=(0, 0, 0), thickness=2, dot_radius dot_radius=dot_radius, ) - -@export_to_all def draw_force(part, force, scr, tx, font, color=(128, 0, 0)): """Draw force vector affecting a part. @@ -199,8 +184,6 @@ def draw_force(part, force, scr, tx, font, color=(128, 0, 0)): Segment(anchor, anchor + force), scr, tx, color=color, thickness=5, dot_radius=5 ) - -@export_to_all def draw_placement(parts, nets, scr, tx, font): """Draw placement of parts and interconnecting nets. @@ -219,8 +202,6 @@ def draw_placement(parts, nets, scr, tx, font): draw_net(net, parts, scr, tx, font) draw_redraw() - -@export_to_all def draw_routing(node, bbox, parts, *other_stuff, **options): """Draw routing for debugging purposes. @@ -252,8 +233,6 @@ def draw_routing(node, bbox, parts, *other_stuff, **options): draw_end() - -@export_to_all def draw_clear(scr, color=(255, 255, 255)): """Clear drawing area. @@ -263,8 +242,6 @@ def draw_clear(scr, color=(255, 255, 255)): """ scr.fill(color) - -@export_to_all def draw_start(bbox): """ Initialize PyGame drawing area. @@ -322,14 +299,10 @@ def draw_start(bbox): # Return drawing screen, transformation matrix, and font. return scr, tx, font - -@export_to_all def draw_redraw(): """Redraw the PyGame display.""" pygame.display.flip() - -@export_to_all def draw_pause(): """Pause drawing and then resume after button press.""" @@ -343,8 +316,6 @@ def draw_pause(): if event.type in (pygame.KEYDOWN, pygame.QUIT): running = False - -@export_to_all def draw_end(): """Display drawing and wait for user to close PyGame window.""" diff --git a/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py b/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py index f4703981..34aac990 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py @@ -8,7 +8,6 @@ from collections import namedtuple - DrawDef = namedtuple( "DrawDef", "name ref zero name_offset show_nums show_names num_units lock_units power_symbol", diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index 8f3b36c7..b78dfc84 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -1,3 +1,7 @@ +""" +Functions for generating a KiCad EESCHEMA schematic. +""" + # -*- coding: utf-8 -*- # The MIT License (MIT) - Copyright (c) Dave Vandenbout. @@ -9,20 +13,11 @@ import time from collections import Counter, OrderedDict -from skidl.scriptinfo import get_script_name -from skidl.schematics.geometry import BBox, Point, Tx, Vector -from skidl.schematics.net_terminal import NetTerminal -from skidl.utilities import export_to_all +from .bboxes import calc_hier_label_bbox, calc_symbol_bbox from .constants import BLK_INT_PAD, BOX_LABEL_FONT_SIZE, GRID, PIN_LABEL_FONT_SIZE -from .bboxes import calc_symbol_bbox, calc_hier_label_bbox -from skidl.utilities import rmv_attr - - -__all__ = [] - -""" -Functions for generating a KiCad EESCHEMA schematic. -""" +from .geometry import BBox, Point, Tx, Vector +from .net_terminal import NetTerminal +from .shims import get_script_name, rmv_attr def bbox_to_eeschema(bbox, tx, name=None): @@ -426,7 +421,6 @@ def create_eeschema_file( ) -@export_to_all def node_to_eeschema(node, sheet_tx=Tx()): """Convert node circuitry to an EESCHEMA sheet. @@ -677,7 +671,6 @@ def finalize_parts_and_nets(circuit, **options): rmv_attr(circuit.parts, ("force", "bbox", "lbl_bbox", "tx")) -@export_to_all def gen_schematic( circuit, filepath=".", @@ -700,10 +693,10 @@ def gen_schematic( """ from skidl import KICAD + from skidl.schematics.node import Node from skidl.schematics.place import PlacementFailure from skidl.schematics.route import RoutingFailure from skidl.tools import tool_modules - from skidl.schematics.node import Node # Part placement options that should always be turned on. options["use_push_pull"] = True @@ -742,7 +735,8 @@ def gen_schematic( continue # Generate EESCHEMA code for the schematic. - node_to_eeschema(node) + # TODO: + # node_to_eeschema(node) # Clean up. finalize_parts_and_nets(circuit, **options) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index ba98d4f6..f8faa62e 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -2,23 +2,8 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. -from math import sqrt, sin, cos, radians from copy import copy - -from ..utilities import export_to_all - -__all__ = [ - "mms_per_mil", - "mils_per_mm", - "Vector", - "tx_rot_0", - "tx_rot_90", - "tx_rot_180", - "tx_rot_270", - "tx_flip_x", - "tx_flip_y", -] - +from math import cos, radians, sin, sqrt """ Stuff for handling geometry: @@ -33,19 +18,16 @@ mms_per_mil = 0.0254 -@export_to_all def to_mils(mm): """Convert millimeters to thousandths-of-inch and return.""" return mm * mils_per_mm -@export_to_all def to_mms(mils): """Convert thousandths-of-inch to millimeters and return.""" return mils * mms_per_mil -@export_to_all class Tx: def __init__(self, a=1, b=0, c=0, d=1, dx=0, dy=0): """Create a transformation matrix. @@ -165,7 +147,6 @@ def no_translate(self): tx_flip_y = Tx(a=1, b=0, c=0, d=-1) -@export_to_all class Point: def __init__(self, x, y): """Create a Point with coords x,y.""" @@ -283,7 +264,6 @@ def __str__(self): Vector = Point -@export_to_all class BBox: def __init__(self, *pts): """Create a bounding box surrounding the given points.""" @@ -409,7 +389,6 @@ def __str__(self): return "[{}, {}]".format(self.min, self.max) -@export_to_all class Segment: def __init__(self, p1, p2): "Create a line segment between two points." diff --git a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py index e1d9e869..6a521fa8 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py @@ -3,7 +3,6 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. from skidl import Part, Pin -from skidl.utilities import export_to_all from .geometry import Point, Tx, Vector @@ -12,7 +11,6 @@ """ -@export_to_all class NetTerminal(Part): def __init__(self, net, tool_module): """Specialized Part with a single pin attached to a net. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 9c658051..8f67a7ae 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -6,18 +6,15 @@ from collections import defaultdict from itertools import chain -from skidl.utilities import export_to_all, rmv_attr from .geometry import BBox, Point, Tx, Vector from .place import Placer from .route import Router - """ Node class for storing circuit hierarchy. """ -@export_to_all class Node(Placer, Router): """Data structure for holding information about a node in the circuit hierarchy.""" @@ -182,6 +179,7 @@ def add_terminal(self, net): """ from skidl.circuit import HIER_SEP + from .net_terminal import NetTerminal nt = NetTerminal(net, self.tool_module) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index bbe9869c..0aa01429 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -15,7 +15,7 @@ from copy import copy from skidl import Pin -from skidl.utilities import export_to_all, rmv_attr, sgn + from .debug_draw import ( draw_end, draw_pause, @@ -25,11 +25,7 @@ draw_text, ) from .geometry import BBox, Point, Segment, Tx, Vector - - -__all__ = [ - "PlacementFailure", -] +from .shims import rmv_attr ################################################################### diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index 760beb49..c8f74894 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -14,13 +14,10 @@ from itertools import chain, zip_longest from skidl import Part -from skidl.utilities import export_to_all, rmv_attr -from .debug_draw import draw_end, draw_endpoint, draw_routing, draw_seg, draw_start -from .geometry import BBox, Point, Segment, Tx, Vector, tx_rot_90 - - -__all__ = ["RoutingFailure", "GlobalRoutingFailure", "SwitchboxRoutingFailure"] +from .debug_draw import draw_end, draw_endpoint, draw_routing, draw_seg, draw_start +from .geometry import BBox, Point, Segment, Vector +from .shims import rmv_attr ################################################################### # @@ -2008,7 +2005,6 @@ def draw_channel(face1, face2): draw_end() -@export_to_all class Router: """Mixin to add routing function to Node class.""" diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py new file mode 100644 index 00000000..e2aa6044 --- /dev/null +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -0,0 +1,9 @@ +"""Replace common skidl functions with our own""" + +def get_script_name(): + # TODO: + raise NotImplementedError + +def rmv_attr(): + # TODO: + raise NotImplementedError From fb8514789d620950a842330ea37f101c5f3d98b1 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 16:08:14 +0200 Subject: [PATCH 11/85] Remove fucked up globals --- .../schematic/kicad/skidl/debug_draw.py | 7 --- .../exporters/schematic/kicad/skidl/route.py | 46 ++++++++++--------- .../exporters/schematic/kicad/skidl/shims.py | 3 ++ 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index 12edc084..9f3f1656 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -255,13 +255,6 @@ def draw_start(bbox): font: PyGame font for rendering text. """ - # Only import pygame if drawing is being done to avoid the startup message. - import pygame - import pygame.freetype - - # Make pygame module available to other functions. - globals()["pygame"] = pygame - # Screen drawing area. scr_bbox = BBox(Point(0, 0), Point(2000, 1500)) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index c8f74894..c7582931 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -13,9 +13,20 @@ from enum import Enum from itertools import chain, zip_longest -from skidl import Part +from .constants import GRID, DRAWING_BOX_RESIZE -from .debug_draw import draw_end, draw_endpoint, draw_routing, draw_seg, draw_start + +class Part: + pass + +from .debug_draw import ( + draw_end, + draw_endpoint, + draw_routing, + draw_seg, + draw_start, + draw_text, +) from .geometry import BBox, Point, Segment, Vector from .shims import rmv_attr @@ -75,13 +86,6 @@ class Direction(Enum): RIGHT = 4 -# Put the orientation/direction enums in global space to make using them easier. -for orientation in Orientation: - globals()[orientation.name] = orientation.value -for direction in Direction: - globals()[direction.name] = direction.value - - # Dictionary for storing colors to visually distinguish routed nets. net_colors = defaultdict( lambda: (random.randint(0, 200), random.randint(0, 200), random.randint(0, 200)) @@ -160,7 +164,7 @@ def __init__(self, net, face, coord): def route_pt(self): """Return (x,y) Point for a Terminal on a Face.""" track = self.face.track - if track.orientation == HORZ: + if track.orientation == Orientation.HORZ: return Point(self.coord, track.coord) else: return Point(track.coord, self.coord) @@ -424,7 +428,7 @@ def bbox(self): """Return the bounding box of the 1-D face segment.""" bbox = BBox() - if self.track.orientation == VERT: + if self.track.orientation == Orientation.VERT: # Face runs vertically, so bbox width is zero. bbox.add(Point(self.track.coord, self.beg.coord)) bbox.add(Point(self.track.coord, self.end.coord)) @@ -668,7 +672,7 @@ def audit(self): def seg(self): """Return a Segment that coincides with the Face.""" - if self.track.orientation == VERT: + if self.track.orientation == Orientation.VERT: p1 = Point(self.track.coord, self.beg.coord) p2 = Point(self.track.coord, self.end.coord) else: @@ -784,8 +788,8 @@ def draw(self, scr, tx, color=(0, 0, 0), thickness=1, dot_radius=10, **options): pt = pin.route_pt * pin.part.tx track = pin.face.track pt = { - HORZ: Point(pt.x, track.coord), - VERT: Point(track.coord, pt.y), + Orientation.HORZ: Point(pt.x, track.coord), + Orientation.VERT: Point(track.coord, pt.y), }[track.orientation] draw_endpoint(pt, scr, tx, color=color, dot_radius=10) @@ -838,7 +842,7 @@ def draw( class GlobalTrack(list): - def __init__(self, orientation=HORZ, coord=0, idx=None, *args, **kwargs): + def __init__(self, orientation=Orientation.HORZ, coord=0, idx=None, *args, **kwargs): """A horizontal/vertical track holding zero or more faces all having the same Y/X coordinate. These global tracks are made by extending the edges of part bounding boxes to @@ -1186,10 +1190,10 @@ def audit(self): for face in self.face_list: face.audit() - assert self.top_face.track.orientation == HORZ - assert self.bottom_face.track.orientation == HORZ - assert self.left_face.track.orientation == VERT - assert self.right_face.track.orientation == VERT + assert self.top_face.track.orientation == Orientation.HORZ + assert self.bottom_face.track.orientation == Orientation.HORZ + assert self.left_face.track.orientation == Orientation.VERT + assert self.right_face.track.orientation == Orientation.VERT assert len(self.top_nets) == len(self.bottom_nets) assert len(self.left_nets) == len(self.right_nets) @@ -2083,11 +2087,11 @@ def create_routing_tracks(node, routing_bbox): # Create an H/V track for each H/V coord containing a list for holding the faces in that track. v_tracks = [ - GlobalTrack(orientation=VERT, idx=idx, coord=coord) + GlobalTrack(orientation=Orientation.VERT, idx=idx, coord=coord) for idx, coord in enumerate(v_track_coord) ] h_tracks = [ - GlobalTrack(orientation=HORZ, idx=idx, coord=coord) + GlobalTrack(orientation=Orientation.HORZ, idx=idx, coord=coord) for idx, coord in enumerate(h_track_coord) ] diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index e2aa6044..9a0585a6 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -7,3 +7,6 @@ def get_script_name(): def rmv_attr(): # TODO: raise NotImplementedError + +class Part: + pass From 2de863e7ad35faa8c6b2061d3217989cc68ef2a3 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 16:11:17 +0200 Subject: [PATCH 12/85] Part as a trait --- src/faebryk/exporters/schematic/kicad/skidl/route.py | 9 ++------- src/faebryk/exporters/schematic/kicad/skidl/shims.py | 5 ++++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index c7582931..d0daa1f4 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -13,12 +13,7 @@ from enum import Enum from itertools import chain, zip_longest -from .constants import GRID, DRAWING_BOX_RESIZE - - -class Part: - pass - +from .constants import DRAWING_BOX_RESIZE, GRID from .debug_draw import ( draw_end, draw_endpoint, @@ -28,7 +23,7 @@ class Part: draw_text, ) from .geometry import BBox, Point, Segment, Vector -from .shims import rmv_attr +from .shims import Part, rmv_attr ################################################################### # diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 9a0585a6..27d2c769 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -1,5 +1,8 @@ """Replace common skidl functions with our own""" +from faebryk.core.trait import Trait + + def get_script_name(): # TODO: raise NotImplementedError @@ -8,5 +11,5 @@ def rmv_attr(): # TODO: raise NotImplementedError -class Part: +class Part(Trait): pass From 007b559a1fa5b04326ea000ec5f7ce0c1503951a Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 16:13:55 +0200 Subject: [PATCH 13/85] Pre-typing --- .../exporters/schematic/kicad/skidl/node.py | 4 ++-- .../exporters/schematic/kicad/skidl/place.py | 17 ++++++++--------- .../exporters/schematic/kicad/skidl/shims.py | 4 ++++ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 8f67a7ae..9b219084 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -15,7 +15,7 @@ """ -class Node(Placer, Router): +class SchNode(Placer, Router): """Data structure for holding information about a node in the circuit hierarchy.""" filename_sz = 20 @@ -32,7 +32,7 @@ def __init__( ): self.parent = None self.children = defaultdict( - lambda: Node(None, tool_module, filepath, top_name, title, flatness) + lambda: SchNode(None, tool_module, filepath, top_name, title, flatness) ) self.filepath = filepath self.top_name = top_name diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 0aa01429..732ea2f5 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -14,19 +14,15 @@ from collections import defaultdict from copy import copy -from skidl import Pin - from .debug_draw import ( - draw_end, draw_pause, draw_placement, draw_redraw, draw_start, draw_text, ) -from .geometry import BBox, Point, Segment, Tx, Vector -from .shims import rmv_attr - +from .geometry import BBox, Point, Tx, Vector +from .shims import Pin, rmv_attr ################################################################### # @@ -66,12 +62,15 @@ class PlacementFailure(Exception): # Small functions for summing Points and Vectors. -pt_sum = lambda pts: sum(pts, Point(0, 0)) -force_sum = lambda forces: sum(forces, Vector(0, 0)) +def pt_sum(pts): + return sum(pts, Point(0, 0)) + +def force_sum(forces): + return sum(forces, Vector(0, 0)) def is_net_terminal(part): - from skidl.schematics.net_terminal import NetTerminal + from .net_terminal import NetTerminal return isinstance(part, NetTerminal) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 27d2c769..66557531 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -13,3 +13,7 @@ def rmv_attr(): class Part(Trait): pass + + +class Pin(Trait): + pass From 78b578fa1fd09e1345fd3a0438326ddc9b326f13 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 16:28:19 +0200 Subject: [PATCH 14/85] Add typing to first few functions of place.py --- .../exporters/schematic/kicad/skidl/place.py | 75 ++++++++++--------- .../exporters/schematic/kicad/skidl/shims.py | 23 +++++- 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 732ea2f5..3596e395 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -13,7 +13,9 @@ import sys from collections import defaultdict from copy import copy +from typing import TYPE_CHECKING, Protocol +from .constants import GRID from .debug_draw import ( draw_pause, draw_placement, @@ -22,7 +24,10 @@ draw_text, ) from .geometry import BBox, Point, Tx, Vector -from .shims import Pin, rmv_attr +from .shims import Part, Pin, rmv_attr + +if TYPE_CHECKING: + from .node import SchNode ################################################################### # @@ -74,8 +79,35 @@ def is_net_terminal(part): return isinstance(part, NetTerminal) +# Class for movable groups of parts/child nodes. +class PartBlock: + def __init__(self, src, bbox, anchor_pt, snap_pt, tag, block_pull_pins): + self.src = src # Source for this block. + self.place_bbox = bbox # FIXME: Is this needed if place_bbox includes room for routing? + + # Create anchor pin to which forces are applied to this block. + anchor_pin = Pin() + anchor_pin.part = self + anchor_pin.place_pt = anchor_pt + + # This block has only a single anchor pin, but it needs to be in a list + # in a dict so it can be processed by the part placement functions. + self.anchor_pins = dict() + self.anchor_pins["similarity"] = [anchor_pin] + + # Anchor pin for this block is also a pulling pin for all other blocks. + block_pull_pins["similarity"].append(anchor_pin) + + # All blocks have the same set of pulling pins because they all pull each other. + self.pull_pins = block_pull_pins + + self.snap_pt = snap_pt # For snapping to grid. + self.tx = Tx() # For placement. + self.ref = "REF" # Name for block in debug drawing. + self.tag = tag # FIXME: what is this for? + -def get_snap_pt(part_or_blk): +def get_snap_pt(part_or_blk: Part | PartBlock) -> Point | None: """Get the point for snapping the Part or PartBlock to the grid. Args: @@ -93,7 +125,7 @@ def get_snap_pt(part_or_blk): return None -def snap_to_grid(part_or_blk): +def snap_to_grid(part_or_blk: Part | PartBlock): """Snap Part or PartBlock to grid. Args: @@ -114,9 +146,9 @@ def snap_to_grid(part_or_blk): part_or_blk.tx *= snap_tx -def add_placement_bboxes(parts, **options): +def add_placement_bboxes(parts: list[Part], **options): """Expand part bounding boxes to include space for subsequent routing.""" - from skidl.schematics.net_terminal import NetTerminal + from .net_terminal import NetTerminal for part in parts: # Placement bbox starts off with the part bbox (including any net labels). @@ -150,7 +182,7 @@ def add_placement_bboxes(parts, **options): ) -def get_enclosing_bbox(parts): +def get_enclosing_bbox(parts: list[Part]) -> BBox: """Return bounding box that encloses all the parts.""" return BBox().add(*(part.place_bbox * part.tx for part in parts)) @@ -1273,33 +1305,6 @@ def place_blocks(node, connected_parts, floating_parts, children, **options): # Global dict of pull pins for all blocks as they each pull on each other the same way. block_pull_pins = defaultdict(list) - # Class for movable groups of parts/child nodes. - class PartBlock: - def __init__(self, src, bbox, anchor_pt, snap_pt, tag): - self.src = src # Source for this block. - self.place_bbox = bbox # FIXME: Is this needed if place_bbox includes room for routing? - - # Create anchor pin to which forces are applied to this block. - anchor_pin = Pin() - anchor_pin.part = self - anchor_pin.place_pt = anchor_pt - - # This block has only a single anchor pin, but it needs to be in a list - # in a dict so it can be processed by the part placement functions. - self.anchor_pins = dict() - self.anchor_pins["similarity"] = [anchor_pin] - - # Anchor pin for this block is also a pulling pin for all other blocks. - block_pull_pins["similarity"].append(anchor_pin) - - # All blocks have the same set of pulling pins because they all pull each other. - self.pull_pins = block_pull_pins - - self.snap_pt = snap_pt # For snapping to grid. - self.tx = Tx() # For placement. - self.ref = "REF" # Name for block in debug drawing. - self.tag = tag # FIXME: what is this for? - # Create a list of blocks from the groups of interconnected parts and the group of floating parts. part_blocks = [] for part_list in connected_parts + [floating_parts]: @@ -1324,7 +1329,7 @@ def __init__(self, src, bbox, anchor_pt, snap_pt, tag): bbox = bbox.resize(Vector(pad, pad)) # Create the part block and place it on the list. - part_blocks.append(PartBlock(part_list, bbox, bbox.ctr, snap_pt, tag)) + part_blocks.append(PartBlock(part_list, bbox, bbox.ctr, snap_pt, tag, block_pull_pins)) # Add part blocks for child nodes. for child in children: @@ -1353,7 +1358,7 @@ def __init__(self, src, bbox, anchor_pt, snap_pt, tag): tag = 4 # A child node with no snapping point. # Create the child block and place it on the list. - part_blocks.append(PartBlock(child, bbox, bbox.ctr, snap_pt, tag)) + part_blocks.append(PartBlock(child, bbox, bbox.ctr, snap_pt, tag, block_pull_pins)) # Get ordered list of all block tags. Use this list to tell if tags are # adjacent since there may be missing tags if a particular type of block diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 66557531..8112ae35 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -1,6 +1,9 @@ """Replace common skidl functions with our own""" +from typing import Iterator from faebryk.core.trait import Trait +from faebryk.exporters.pcb.kicad.transformer import Point +from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Tx def get_script_name(): @@ -12,8 +15,24 @@ def rmv_attr(): raise NotImplementedError class Part(Trait): - pass + pins: list["Pin"] # TODO: + place_bbox: BBox # TODO: + lbl_bbox: BBox # TODO: + tx: Tx # transformation matrix of the part's position + def __iter__(self) -> Iterator["Pin"]: + # TODO: + raise NotImplementedError class Pin(Trait): - pass + part: Part # TODO: + place_pt: Point # TODO: + pt: Point # TODO: + stub: bool # whether to stub the pin or not + orientation: str # TODO: + + def is_connected(self) -> bool: + # TODO: + raise NotImplementedError + + From 9c593fc4c1b9ee50d456d8ee62c7b50d7643a5cb Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 16:41:59 +0200 Subject: [PATCH 15/85] Typing progress --- .../exporters/schematic/kicad/skidl/place.py | 26 ++++------ .../exporters/schematic/kicad/skidl/shims.py | 48 ++++++++++++++++--- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 3596e395..84a6daae 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -24,7 +24,7 @@ draw_text, ) from .geometry import BBox, Point, Tx, Vector -from .shims import Part, Pin, rmv_attr +from .shims import Net, Part, Pin, rmv_attr if TYPE_CHECKING: from .node import SchNode @@ -187,7 +187,7 @@ def get_enclosing_bbox(parts: list[Part]) -> BBox: return BBox().add(*(part.place_bbox * part.tx for part in parts)) -def add_anchor_pull_pins(parts, nets, **options): +def add_anchor_pull_pins(parts: list[Part], nets: list[Net], **options): """Add positions of anchor and pull pins for attractive net forces between parts. Args: @@ -196,7 +196,7 @@ def add_anchor_pull_pins(parts, nets, **options): options (dict): Dict of options and values that enable/disable functions. """ - def add_place_pt(part, pin): + def add_place_pt(part: Part, pin: Pin): """Add the point for a pin on the placement boundary of a part.""" pin.route_pt = pin.pt # For drawing of nets during debugging. @@ -268,14 +268,14 @@ def add_place_pt(part, pin): all_pull_pins.append(anchor_pull_pin) -def save_anchor_pull_pins(parts): +def save_anchor_pull_pins(parts: list[Part]) -> None: """Save anchor/pull pins for each part before they are changed.""" for part in parts: part.saved_anchor_pins = copy(part.anchor_pins) part.saved_pull_pins = copy(part.pull_pins) -def restore_anchor_pull_pins(parts): +def restore_anchor_pull_pins(parts: list[Part]) -> None: """Restore the original anchor/pull pin lists for each Part.""" for part in parts: @@ -288,7 +288,7 @@ def restore_anchor_pull_pins(parts): rmv_attr(parts, ("saved_anchor_pins", "saved_pull_pins")) -def adjust_orientations(parts, **options): +def adjust_orientations(parts: list[Part], **options) -> bool | None: """Adjust orientation of parts. Args: @@ -299,7 +299,7 @@ def adjust_orientations(parts, **options): bool: True if one or more part orientations were changed. Otherwise, False. """ - def find_best_orientation(part): + def find_best_orientation(part: Part) -> None: """Each part has 8 possible orientations. Find the best of the 7 alternatives from the starting one.""" # Store starting orientation. @@ -354,8 +354,8 @@ def find_best_orientation(part): # Hence the ad-hoc loop limit. for iter_cnt in range(10): # Find the best part to move and move it until there are no more parts to move. - moved_parts = [] - unmoved_parts = movable_parts[:] + moved_parts: list[Part] = [] + unmoved_parts: list[Part] = movable_parts[:] while unmoved_parts: # Find the best current orientation for each unmoved part. for part in unmoved_parts: @@ -376,13 +376,7 @@ def find_best_orientation(part): # Start with cost change of zero before any parts are moved. delta_costs = [0,] delta_costs.extend((part.delta_cost for part in moved_parts)) - try: - cost_seq = list(itertools.accumulate(delta_costs)) - except AttributeError: - # Python 2.7 doesn't have itertools.accumulate(). - cost_seq = list(delta_costs) - for i in range(1, len(cost_seq)): - cost_seq[i] = cost_seq[i - 1] + cost_seq[i] + cost_seq = list(itertools.accumulate(delta_costs)) min_cost = min(cost_seq) min_index = cost_seq.index(min_cost) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 8112ae35..336c45f2 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -1,38 +1,74 @@ """Replace common skidl functions with our own""" -from typing import Iterator +from typing import Any, Iterator + from faebryk.core.trait import Trait -from faebryk.exporters.pcb.kicad.transformer import Point -from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Tx +from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx def get_script_name(): # TODO: raise NotImplementedError -def rmv_attr(): - # TODO: - raise NotImplementedError + +def to_list(x): + """ + Return x if it is already a list, or return a list containing x if x is a scalar. + """ + if isinstance(x, (list, tuple, set)): + return x # Already a list, so just return it. + return [x] # Wasn't a list, so make it into one. + + +def rmv_attr(objs, attrs): + """Remove a list of attributes from a list of objects.""" + for o in to_list(objs): + for a in to_list(attrs): + try: + delattr(o, a) + except AttributeError: + pass + class Part(Trait): pins: list["Pin"] # TODO: place_bbox: BBox # TODO: lbl_bbox: BBox # TODO: tx: Tx # transformation matrix of the part's position + prev_tx: Tx # previous transformation matrix of the part's position + anchor_pins: dict[Any, list["Pin"]] # TODO: better types, what is this? + pull_pins: dict[Any, list["Pin"]] # TODO: better types, what is this? + pin_ctrs: dict # TODO: better types, what is this? + saved_anchor_pins: dict[Any, list["Pin"]] # copy of anchor_pins + saved_pull_pins: dict[Any, list["Pin"]] # copy of pull_pins + delta_cost: float # the largest decrease in cost and the associated orientation. + delta_cost_tx: Tx # transformation matrix associated with delta_cost + orientation_locked: bool # whether the part's orientation is locked def __iter__(self) -> Iterator["Pin"]: # TODO: raise NotImplementedError + class Pin(Trait): part: Part # TODO: place_pt: Point # TODO: pt: Point # TODO: stub: bool # whether to stub the pin or not orientation: str # TODO: + route_pt: Point # TODO: + place_pt: Point # TODO: + orientation: str # TODO: def is_connected(self) -> bool: # TODO: raise NotImplementedError +class Net(Trait): + pins: list[Pin] # TODO: + parts: set[Part] # TODO: + + def __iter__(self) -> Iterator[Pin]: + # TODO: + raise NotImplementedError From 64ac529431d480b4b41c6b99d817d9fe98221687 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 16:52:48 +0200 Subject: [PATCH 16/85] More typing... --- .../exporters/schematic/kicad/skidl/place.py | 68 ++++++++++++------- .../exporters/schematic/kicad/skidl/shims.py | 14 ++-- 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 84a6daae..2a441a90 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -13,7 +13,7 @@ import sys from collections import defaultdict from copy import copy -from typing import TYPE_CHECKING, Protocol +from typing import TYPE_CHECKING, Callable, Protocol from .constants import GRID from .debug_draw import ( @@ -394,7 +394,7 @@ def find_best_orientation(part: Part) -> None: return iter_cnt > 0 -def net_tension_dist(part, **options): +def net_tension_dist(part: Part, **options) -> float: """Calculate the tension of the nets trying to rotate/flip the part. Args: @@ -431,7 +431,7 @@ def net_tension_dist(part, **options): return tension -def net_torque_dist(part, **options): +def net_torque_dist(part: Part, **options) -> float: """Calculate the torque of the nets trying to rotate/flip the part. Args: @@ -482,8 +482,7 @@ def net_torque_dist(part, **options): # net_tension = net_torque_dist -@export_to_all -def net_force_dist(part, **options): +def net_force_dist(part: Part, **options) -> Vector: """Compute attractive force on a part from all the other parts connected to it. Args: @@ -564,8 +563,7 @@ def net_force_dist(part, **options): attractive_force = net_force_dist -@export_to_all -def overlap_force(part, parts, **options): +def overlap_force(part: Part, parts: list[Part], **options) -> Vector: """Compute the repulsive force on a part from overlapping other parts. Args: @@ -590,10 +588,10 @@ def overlap_force(part, parts, **options): # Compute the movement needed to separate the bboxes in left/right/up/down directions. # Add some small random offset to break symmetry when parts exactly overlay each other. # Move right edge of part to the left of other part's left edge, etc... - moves = [] + moves: list[list[float, Vector]] = [] rnd = Vector(random.random()-0.5, random.random()-0.5) for edges, dir in ((("ll", "lr"), Vector(1,0)), (("ul", "ll"), Vector(0,1))): - move = (getattr(other_part_bbox, edges[0]) - getattr(part_bbox, edges[1]) - rnd) * dir + move: Vector = (getattr(other_part_bbox, edges[0]) - getattr(part_bbox, edges[1]) - rnd) * dir moves.append([move.magnitude, move]) # Flip edges... move = (getattr(other_part_bbox, edges[1]) - getattr(part_bbox, edges[0]) - rnd) * dir @@ -604,12 +602,11 @@ def overlap_force(part, parts, **options): # Add the move to the total force on the part. total_force += move[1] - + return total_force -@export_to_all -def overlap_force_rand(part, parts, **options): +def overlap_force_rand(part: Part, parts: list[Part], **options) -> Vector: """Compute the repulsive force on a part from overlapping other parts. Args: @@ -636,9 +633,13 @@ def overlap_force_rand(part, parts, **options): # Move right edge of part to the left of other part's left edge. moves = [] rnd = Vector(random.random()-0.5, random.random()-0.5) - for edges, dir in ((("ll", "lr"), Vector(1,0)), (("lr", "ll"), Vector(1,0)), - (("ul", "ll"), Vector(0,1)), (("ll", "ul"), Vector(0,1))): - move = (getattr(other_part_bbox, edges[0]) - getattr(part_bbox, edges[1]) - rnd) * dir + for edges, dir in ( + (("ll", "lr"), Vector(1, 0)), + (("lr", "ll"), Vector(1, 0)), + (("ul", "ll"), Vector(0, 1)), + (("ll", "ul"), Vector(0, 1)), + ): + move: Vector = (getattr(other_part_bbox, edges[0]) - getattr(part_bbox, edges[1]) - rnd) * dir moves.append([move.magnitude, move]) accum = 0 for move in moves: @@ -654,7 +655,7 @@ def overlap_force_rand(part, parts, **options): if move[0] >= select: total_force += move[1] break - + return total_force @@ -663,7 +664,9 @@ def overlap_force_rand(part, parts, **options): # repulsive_force = overlap_force_rand -def scale_attractive_repulsive_forces(parts, force_func, **options): +def scale_attractive_repulsive_forces( + parts: list[Part], force_func: Callable[[Part, list[Part], ...], Vector], **options +) -> float: """Set scaling between attractive net forces and repulsive part overlap forces.""" # Store original part placement. @@ -695,7 +698,9 @@ def scale_attractive_repulsive_forces(parts, force_func, **options): return 1 -def total_part_force(part, parts, scale, alpha, **options): +def total_part_force( + part: Part, parts: list[Part], scale: float, alpha: float, **options +) -> Vector: """Compute the total of the attractive net and repulsive overlap forces on a part. Args: @@ -715,7 +720,9 @@ def total_part_force(part, parts, scale, alpha, **options): return force -def similarity_force(part, parts, similarity, **options): +def similarity_force( + part: Part, parts: list[Part], similarity: dict, **options +) -> Vector: """Compute attractive force on a part from all the other parts connected to it. Args: @@ -740,7 +747,9 @@ def similarity_force(part, parts, similarity, **options): return total_force -def total_similarity_force(part, parts, similarity, scale, alpha, **options): +def total_similarity_force( + part: Part, parts: list[Part], similarity: dict, scale: float, alpha: float, **options +) -> Vector: """Compute the total of the attractive similarity and repulsive overlap forces on a part. Args: @@ -761,7 +770,7 @@ def total_similarity_force(part, parts, similarity, scale, alpha, **options): return force -def define_placement_bbox(parts, **options): +def define_placement_bbox(parts: list[Part], **options) -> BBox: """Return a bounding box big enough to hold the parts being placed.""" # Compute appropriate size to hold the parts based on their areas. @@ -772,7 +781,7 @@ def define_placement_bbox(parts, **options): return BBox(Point(0, 0), Point(side, side)) -def central_placement(parts, **options): +def central_placement(parts: list[Part], **options): """Cluster all part centroids onto a common point. Args: @@ -793,7 +802,7 @@ def central_placement(parts, **options): part.tx *= Tx(dx=mv.x, dy=mv.y) -def random_placement(parts, **options): +def random_placement(parts: list[Part], **options): """Randomly place parts within an appropriately-sized area. Args: @@ -809,7 +818,13 @@ def random_placement(parts, **options): part.tx = part.tx.move(pt) -def push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options): +def push_and_pull( + anchored_parts: list[Part], + mobile_parts: list[Part], + nets: list[Net], + force_func: Callable[[Part, list[Part], ...], Vector], + **options +): """Move parts under influence of attractive nets and repulsive part overlaps. Args: @@ -828,7 +843,7 @@ def push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options): # No need to do placement if there's nothing to move. return - def cost(parts, alpha): + def cost(parts: list[Part], alpha: float) -> float: """Cost function for use in debugging. Should decrease as parts move.""" for part in parts: part.force = force_func(part, parts, scale=scale, alpha=alpha, **options) @@ -877,7 +892,8 @@ def cost(parts, alpha): for speed, alpha, stability_coef, align_parts, force_mask in force_schedule: if align_parts: # Align parts by only using forces between the closest anchor/pull pins. - retain_closest_anchor_pull_pins(mobile_parts) + # retain_closest_anchor_pull_pins(mobile_parts) + raise NotImplementedError else: # For general placement, use forces between all anchor/pull pins. restore_anchor_pull_pins(mobile_parts) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 336c45f2..d1e15ede 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -3,7 +3,7 @@ from typing import Any, Iterator from faebryk.core.trait import Trait -from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx +from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector def get_script_name(): @@ -30,8 +30,8 @@ def rmv_attr(objs, attrs): pass -class Part(Trait): - pins: list["Pin"] # TODO: +class Part: + pins: list["Pin"] # TODO: source place_bbox: BBox # TODO: lbl_bbox: BBox # TODO: tx: Tx # transformation matrix of the part's position @@ -45,12 +45,16 @@ class Part(Trait): delta_cost_tx: Tx # transformation matrix associated with delta_cost orientation_locked: bool # whether the part's orientation is locked + # attr assigned, eg. I don't need to care about it + original_tx: Tx # internal use + force: Vector # used for debugging + def __iter__(self) -> Iterator["Pin"]: # TODO: raise NotImplementedError -class Pin(Trait): +class Pin: part: Part # TODO: place_pt: Point # TODO: pt: Point # TODO: @@ -65,7 +69,7 @@ def is_connected(self) -> bool: raise NotImplementedError -class Net(Trait): +class Net: pins: list[Pin] # TODO: parts: set[Part] # TODO: From e275f2d876cc1776227b40f3b4b37dcc26cc391e Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 17:06:14 +0200 Subject: [PATCH 17/85] NetTerminal sufficently together --- .../schematic/kicad/skidl/net_terminal.py | 30 +++++++++---------- .../exporters/schematic/kicad/skidl/place.py | 19 ++++++++++-- .../exporters/schematic/kicad/skidl/shims.py | 22 ++++++++++++-- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py index 6a521fa8..9fb16ad0 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py @@ -2,9 +2,9 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. -from skidl import Part, Pin +from .constants import GRID from .geometry import Point, Tx, Vector - +from .shims import Net, Part, Pin """ Net_Terminal class for handling net labels. @@ -12,27 +12,27 @@ class NetTerminal(Part): - def __init__(self, net, tool_module): + pull_pins: dict[Net, list[Pin]] + + def __init__(self, net: Net, tool_module): """Specialized Part with a single pin attached to a net. This is intended for attaching to nets to label them, typically when the net spans across levels of hierarchical nodes. """ - - # Create a Part. - from skidl import SKIDL - - super().__init__(name="NT", ref_prefix="NT", tool=SKIDL) - + from .bboxes import calc_hier_label_bbox # Set a default transformation matrix for this part. self.tx = Tx() # Add a single pin to the part. - pin = Pin(num="1", name="~") - self.add_pins(pin) + pin = Pin() + pin.part = self + pin.num = "1" + pin.name = "~" + self.pins = [pin] # Connect the pin to the net. - pin += net + net.pins.append(pin) # Set the pin at point (0,0) and pointing leftward toward the part body # (consisting of just the net label for this type of part) so any attached routing @@ -43,13 +43,13 @@ def __init__(self, net, tool_module): # Calculate the bounding box, but as if the pin were pointed right so # the associated label text would go to the left. - self.bbox = tool_module.calc_hier_label_bbox(net.name, "R") + self.bbox = calc_hier_label_bbox(net.name, "R") # Resize bbox so it's an integer number of GRIDs. - self.bbox = self.bbox.snap_resize(tool_module.constants.GRID) + self.bbox = self.bbox.snap_resize(GRID) # Extend the bounding box a bit so any attached routing will come straight in. - self.bbox.max += Vector(tool_module.constants.GRID, 0) + self.bbox.max += Vector(GRID, 0) self.lbl_bbox = self.bbox # Flip the NetTerminal horizontally if it is an output net (label on the right). diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 2a441a90..0d11fff0 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: from .node import SchNode + from .net_terminal import NetTerminal ################################################################### # @@ -960,7 +961,13 @@ def cost(parts: list[Part], alpha: float) -> float: draw_redraw() -def evolve_placement(anchored_parts, mobile_parts, nets, force_func, **options): +def evolve_placement( + anchored_parts: list[Part], + mobile_parts: list[Part], + nets: list[Net], + force_func: Callable[[Part, list[Part], ...], Vector], + **options +): """Evolve part placement looking for optimum using force function. Args: @@ -981,7 +988,13 @@ def evolve_placement(anchored_parts, mobile_parts, nets, force_func, **options): snap_to_grid(part) -def place_net_terminals(net_terminals, placed_parts, nets, force_func, **options): +def place_net_terminals( + net_terminals: list["NetTerminal"], + placed_parts: list[Part], + nets: list[Net], + force_func: Callable[[Part, list[Part], ...], Vector], + **options +): """Place net terminals around already-placed parts. Args: @@ -992,7 +1005,7 @@ def place_net_terminals(net_terminals, placed_parts, nets, force_func, **options options (dict): Dict of options and values that enable/disable functions. """ - def trim_pull_pins(terminals, bbox): + def trim_pull_pins(terminals: list["NetTerminal"], bbox: BBox): """Trim pullpins of NetTerminals to the part pins closest to an edge of the bounding box of placed parts. Args: diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index d1e15ede..1ea2f265 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -31,6 +31,9 @@ def rmv_attr(objs, attrs): class Part: + # We need to assign these + + # TODO: where are these expected to be assigned? pins: list["Pin"] # TODO: source place_bbox: BBox # TODO: lbl_bbox: BBox # TODO: @@ -48,6 +51,7 @@ class Part: # attr assigned, eg. I don't need to care about it original_tx: Tx # internal use force: Vector # used for debugging + mv: Vector # internal use def __iter__(self) -> Iterator["Pin"]: # TODO: @@ -55,6 +59,11 @@ def __iter__(self) -> Iterator["Pin"]: class Pin: + # We need to assign these + num: str + name: str + + # TODO: where are these expected to be assigned? part: Part # TODO: place_pt: Point # TODO: pt: Point # TODO: @@ -64,15 +73,24 @@ class Pin: place_pt: Point # TODO: orientation: str # TODO: + # Assigned in NetTerminal, but it's unclear + # whether this is typically something that comes from the user + x: float + y: float + def is_connected(self) -> bool: # TODO: raise NotImplementedError class Net: + # We need to assign these + name: str + netio: str # whether input or output + + # TODO: where are these expected to be assigned? pins: list[Pin] # TODO: parts: set[Part] # TODO: def __iter__(self) -> Iterator[Pin]: - # TODO: - raise NotImplementedError + yield from self.pins From e24a3f5c3167e99302da6d7ab4374eb4c69a0a40 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 17:29:10 +0200 Subject: [PATCH 18/85] Finish typing place.py! --- .../exporters/schematic/kicad/skidl/node.py | 5 +- .../exporters/schematic/kicad/skidl/place.py | 63 ++++++++++--------- .../exporters/schematic/kicad/skidl/shims.py | 1 - 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 9b219084..5275b015 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -9,6 +9,7 @@ from .geometry import BBox, Point, Tx, Vector from .place import Placer from .route import Router +from .shims import Net, Part """ Node class for storing circuit hierarchy. @@ -42,7 +43,7 @@ def __init__( self.flatness = flatness self.flattened = False self.tool_module = tool_module # Backend tool. - self.parts = [] + self.parts: list["Part"] = [] self.wires = defaultdict(list) self.junctions = defaultdict(list) self.tx = Tx() @@ -277,7 +278,7 @@ def flatten(self, flatness=0.0): for child in child_types[child_type]: child.flattened = False - def get_internal_nets(self): + def get_internal_nets(self) -> list[Net]: """Return a list of nets that have at least one pin on a part in this node.""" processed_nets = [] diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 0d11fff0..266f9b77 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -10,12 +10,11 @@ import itertools import math import random -import sys from collections import defaultdict from copy import copy -from typing import TYPE_CHECKING, Callable, Protocol +from typing import TYPE_CHECKING, Callable -from .constants import GRID +from .constants import BLK_EXT_PAD, BLK_INT_PAD, GRID from .debug_draw import ( draw_pause, draw_placement, @@ -27,8 +26,8 @@ from .shims import Net, Part, Pin, rmv_attr if TYPE_CHECKING: - from .node import SchNode from .net_terminal import NetTerminal + from .node import SchNode ################################################################### # @@ -82,7 +81,16 @@ def is_net_terminal(part): # Class for movable groups of parts/child nodes. class PartBlock: - def __init__(self, src, bbox, anchor_pt, snap_pt, tag, block_pull_pins): + + def __init__( + self, + src: list[Part | "SchNode"] | Part | "SchNode", + bbox: BBox, + anchor_pt: Point, + snap_pt: Point, + tag: str, + block_pull_pins: dict[str, list[Pin]], + ): self.src = src # Source for this block. self.place_bbox = bbox # FIXME: Is this needed if place_bbox includes room for routing? @@ -1035,7 +1043,7 @@ def trim_pull_pins(terminals: list["NetTerminal"], bbox: BBox): # Retain only the pulling pin closest to an edge of the bounding box (i.e., minimum inset). terminal.pull_pins[net] = [min(insets, key=lambda off: off[0])[1]] - def orient(terminals, bbox): + def orient(terminals: list["NetTerminal"], bbox: BBox): """Set orientation of NetTerminals to point away from closest bounding box edge. Args: @@ -1063,7 +1071,7 @@ def orient(terminals, bbox): # Apply the Tx() for the side the terminal is closest to. terminal.tx = min(insets, key=lambda inset: inset[0])[1] - def move_to_pull_pin(terminals): + def move_to_pull_pin(terminals: list["NetTerminal"]): """Move NetTerminals immediately to their pulling pins.""" for terminal in terminals: anchor_pin = list(terminal.anchor_pins.values())[0][0] @@ -1072,7 +1080,7 @@ def move_to_pull_pin(terminals): pull_pt = pull_pin.place_pt * pull_pin.part.tx terminal.tx = terminal.tx.move(pull_pt - anchor_pt) - def evolution(net_terminals, placed_parts, bbox): + def evolution(net_terminals: list["NetTerminal"], placed_parts: list[Part], bbox: BBox): """Evolve placement of NetTerminals starting from outermost from center to innermost.""" evolution_type = options.get("terminal_evolution", "all_at_once") @@ -1087,7 +1095,7 @@ def evolution(net_terminals, placed_parts, bbox): anchored_parts = copy(placed_parts) # Sort terminals from outermost to innermost w.r.t. the center. - def dist_to_bbox_edge(term): + def dist_to_bbox_edge(term: "NetTerminal"): pt = term.pins[0].place_pt * term.tx return min(( abs(pt.x - bbox.ll.x), @@ -1141,11 +1149,10 @@ def dist_to_bbox_edge(term): restore_anchor_pull_pins(net_terminals) -@export_to_all class Placer: """Mixin to add place function to Node class.""" - def group_parts(node, **options): + def group_parts(node: "SchNode", **options): """Group parts in the Node that are connected by internal nets Args: @@ -1193,7 +1200,7 @@ def group_parts(node, **options): return connected_parts, internal_nets, floating_parts - def place_connected_parts(node, parts, nets, **options): + def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], **options): """Place individual parts. Args: @@ -1251,7 +1258,7 @@ def place_connected_parts(node, parts, nets, **options): # Pause to look at placement for debugging purposes. draw_pause() - def place_floating_parts(node, parts, **options): + def place_floating_parts(node: "SchNode", parts: list[Part], **options): """Place individual parts. Args: @@ -1312,7 +1319,13 @@ def place_floating_parts(node, parts, **options): # Pause to look at placement for debugging purposes. draw_pause() - def place_blocks(node, connected_parts, floating_parts, children, **options): + def place_blocks( + node: "SchNode", + connected_parts: list[list[Part]], + floating_parts: list[Part], + children: list["SchNode"], + **options + ): """Place blocks of parts and hierarchical sheets. Args: @@ -1329,7 +1342,7 @@ def place_blocks(node, connected_parts, floating_parts, children, **options): block_pull_pins = defaultdict(list) # Create a list of blocks from the groups of interconnected parts and the group of floating parts. - part_blocks = [] + part_blocks: list[PartBlock] = [] for part_list in connected_parts + [floating_parts]: if not part_list: # No parts in this list for some reason... @@ -1439,10 +1452,11 @@ def place_blocks(node, connected_parts, floating_parts, children, **options): except AttributeError: # The source doesn't have a Tx so it must be a collection of parts. # Apply the block placement to the Tx of each part. + assert isinstance(blk.src, list) for part in blk.src: part.tx *= blk.tx - def get_attrs(node): + def get_attrs(node: "SchNode"): """Return dict of attribute sets for the parts, pins, and nets in a node.""" attrs = {"parts": set(), "pins": set(), "nets": set()} for part in node.parts: @@ -1453,7 +1467,7 @@ def get_attrs(node): attrs["nets"].update(set(dir(net))) return attrs - def show_added_attrs(node): + def show_added_attrs(node: "SchNode"): """Show attributes that were added to parts, pins, and nets in a node.""" current_attrs = node.get_attrs() for key in current_attrs.keys(): @@ -1461,7 +1475,7 @@ def show_added_attrs(node): "added {} attrs: {}".format(key, current_attrs[key] - node.attrs[key]) ) - def rmv_placement_stuff(node): + def rmv_placement_stuff(node: "SchNode"): """Remove attributes added to parts, pins, and nets of a node during the placement phase.""" for part in node.parts: @@ -1472,7 +1486,7 @@ def rmv_placement_stuff(node): ) rmv_attr(node.get_internal_nets(), ("parts",)) - def place(node, tool=None, **options): + def place(node: "SchNode", **options): """Place the parts and children in this node. Args: @@ -1480,15 +1494,6 @@ def place(node, tool=None, **options): tool (str): Backend tool for schematics. options (dict): Dictionary of options and values to control placement. """ - - # Inject the constants for the backend tool into this module. - import skidl - from skidl.tools import tool_modules - - tool = tool or skidl.config.tool - this_module = sys.modules[__name__] - this_module.__dict__.update(tool_modules[tool].constants.__dict__) - random.seed(options.get("seed")) # Store the starting attributes of the node's parts, pins, and nets. @@ -1498,7 +1503,7 @@ def place(node, tool=None, **options): # First, recursively place children of this node. # TODO: Child nodes are independent, so can they be processed in parallel? for child in node.children.values(): - child.place(tool=tool, **options) + child.place(**options) # Group parts into those that are connected by explicit nets and # those that float freely connected only by stub nets. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 1ea2f265..1a59067f 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -2,7 +2,6 @@ from typing import Any, Iterator -from faebryk.core.trait import Trait from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector From b872f47121e2beb33f250c2319c46e11b4c2465f Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 17:36:35 +0200 Subject: [PATCH 19/85] Type options --- .../exporters/schematic/kicad/skidl/place.py | 118 +++++++++--------- .../exporters/schematic/kicad/skidl/shims.py | 34 ++++- 2 files changed, 88 insertions(+), 64 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 266f9b77..5f0387bb 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -12,7 +12,7 @@ import random from collections import defaultdict from copy import copy -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, Unpack from .constants import BLK_EXT_PAD, BLK_INT_PAD, GRID from .debug_draw import ( @@ -23,7 +23,7 @@ draw_text, ) from .geometry import BBox, Point, Tx, Vector -from .shims import Net, Part, Pin, rmv_attr +from .shims import Net, Part, Pin, rmv_attr, Options if TYPE_CHECKING: from .net_terminal import NetTerminal @@ -155,7 +155,7 @@ def snap_to_grid(part_or_blk: Part | PartBlock): part_or_blk.tx *= snap_tx -def add_placement_bboxes(parts: list[Part], **options): +def add_placement_bboxes(parts: list[Part], **options: Unpack[Options]): """Expand part bounding boxes to include space for subsequent routing.""" from .net_terminal import NetTerminal @@ -196,7 +196,7 @@ def get_enclosing_bbox(parts: list[Part]) -> BBox: return BBox().add(*(part.place_bbox * part.tx for part in parts)) -def add_anchor_pull_pins(parts: list[Part], nets: list[Net], **options): +def add_anchor_pull_pins(parts: list[Part], nets: list[Net], **options: Unpack[Options]): """Add positions of anchor and pull pins for attractive net forces between parts. Args: @@ -297,7 +297,7 @@ def restore_anchor_pull_pins(parts: list[Part]) -> None: rmv_attr(parts, ("saved_anchor_pins", "saved_pull_pins")) -def adjust_orientations(parts: list[Part], **options) -> bool | None: +def adjust_orientations(parts: list[Part], **options: Unpack[Options]) -> bool | None: """Adjust orientation of parts. Args: @@ -326,12 +326,12 @@ def find_best_orientation(part: Part) -> None: if calc_starting_cost: # Calculate the cost of the starting orientation before any changes in orientation. - starting_cost = net_tension(part, **options) + starting_cost = net_tension(part, **options: Unpack[Options]) # Skip the starting orientation but set flag to process the others. calc_starting_cost = False else: # Calculate the cost of the current orientation. - delta_cost = net_tension(part, **options) - starting_cost + delta_cost = net_tension(part, **options: Unpack[Options]) - starting_cost if delta_cost < best_delta_cost: # Save the largest decrease in cost and the associated orientation. best_delta_cost = delta_cost @@ -403,7 +403,7 @@ def find_best_orientation(part: Part) -> None: return iter_cnt > 0 -def net_tension_dist(part: Part, **options) -> float: +def net_tension_dist(part: Part, **options: Unpack[Options]) -> float: """Calculate the tension of the nets trying to rotate/flip the part. Args: @@ -440,7 +440,7 @@ def net_tension_dist(part: Part, **options) -> float: return tension -def net_torque_dist(part: Part, **options) -> float: +def net_torque_dist(part: Part, **options: Unpack[Options]) -> float: """Calculate the torque of the nets trying to rotate/flip the part. Args: @@ -491,7 +491,7 @@ def net_torque_dist(part: Part, **options) -> float: # net_tension = net_torque_dist -def net_force_dist(part: Part, **options) -> Vector: +def net_force_dist(part: Part, **options: Unpack[Options]) -> Vector: """Compute attractive force on a part from all the other parts connected to it. Args: @@ -572,7 +572,7 @@ def net_force_dist(part: Part, **options) -> Vector: attractive_force = net_force_dist -def overlap_force(part: Part, parts: list[Part], **options) -> Vector: +def overlap_force(part: Part, parts: list[Part], **options: Unpack[Options]) -> Vector: """Compute the repulsive force on a part from overlapping other parts. Args: @@ -615,7 +615,7 @@ def overlap_force(part: Part, parts: list[Part], **options) -> Vector: return total_force -def overlap_force_rand(part: Part, parts: list[Part], **options) -> Vector: +def overlap_force_rand(part: Part, parts: list[Part], **options: Unpack[Options]) -> Vector: """Compute the repulsive force on a part from overlapping other parts. Args: @@ -674,7 +674,7 @@ def overlap_force_rand(part: Part, parts: list[Part], **options) -> Vector: def scale_attractive_repulsive_forces( - parts: list[Part], force_func: Callable[[Part, list[Part], ...], Vector], **options + parts: list[Part], force_func: Callable[[Part, list[Part], ...], Vector], **options: Unpack[Options] ) -> float: """Set scaling between attractive net forces and repulsive part overlap forces.""" @@ -683,15 +683,15 @@ def scale_attractive_repulsive_forces( part.original_tx = copy(part.tx) # Find attractive forces when they are maximized by random part placement. - random_placement(parts, **options) + random_placement(parts, **options: Unpack[Options]) attractive_forces_sum = sum( - force_func(p, parts, alpha=0, scale=1, **options).magnitude for p in parts + force_func(p, parts, alpha=0, scale=1, **options: Unpack[Options]).magnitude for p in parts ) # Find repulsive forces when they are maximized by compacted part placement. - central_placement(parts, **options) + central_placement(parts, **options: Unpack[Options]) repulsive_forces_sum = sum( - force_func(p, parts, alpha=1, scale=1, **options).magnitude for p in parts + force_func(p, parts, alpha=1, scale=1, **options: Unpack[Options]).magnitude for p in parts ) # Restore original part placement. @@ -708,7 +708,7 @@ def scale_attractive_repulsive_forces( def total_part_force( - part: Part, parts: list[Part], scale: float, alpha: float, **options + part: Part, parts: list[Part], scale: float, alpha: float, **options: Unpack[Options] ) -> Vector: """Compute the total of the attractive net and repulsive overlap forces on a part. @@ -723,14 +723,14 @@ def total_part_force( Vector: Weighted total of net attractive and overlap repulsion forces. """ force = scale * (1 - alpha) * attractive_force( - part, **options - ) + alpha * repulsive_force(part, parts, **options) + part, **options: Unpack[Options] + ) + alpha * repulsive_force(part, parts, **options: Unpack[Options]) part.force = force # For debug drawing. return force def similarity_force( - part: Part, parts: list[Part], similarity: dict, **options + part: Part, parts: list[Part], similarity: dict, **options: Unpack[Options] ) -> Vector: """Compute attractive force on a part from all the other parts connected to it. @@ -757,7 +757,7 @@ def similarity_force( def total_similarity_force( - part: Part, parts: list[Part], similarity: dict, scale: float, alpha: float, **options + part: Part, parts: list[Part], similarity: dict, scale: float, alpha: float, **options: Unpack[Options] ) -> Vector: """Compute the total of the attractive similarity and repulsive overlap forces on a part. @@ -773,13 +773,13 @@ def total_similarity_force( Vector: Weighted total of net attractive and overlap repulsion forces. """ force = scale * (1 - alpha) * similarity_force( - part, parts, similarity, **options - ) + alpha * repulsive_force(part, parts, **options) + part, parts, similarity, **options: Unpack[Options] + ) + alpha * repulsive_force(part, parts, **options: Unpack[Options]) part.force = force # For debug drawing. return force -def define_placement_bbox(parts: list[Part], **options) -> BBox: +def define_placement_bbox(parts: list[Part], **options: Unpack[Options]) -> BBox: """Return a bounding box big enough to hold the parts being placed.""" # Compute appropriate size to hold the parts based on their areas. @@ -790,7 +790,7 @@ def define_placement_bbox(parts: list[Part], **options) -> BBox: return BBox(Point(0, 0), Point(side, side)) -def central_placement(parts: list[Part], **options): +def central_placement(parts: list[Part], **options: Unpack[Options]): """Cluster all part centroids onto a common point. Args: @@ -811,7 +811,7 @@ def central_placement(parts: list[Part], **options): part.tx *= Tx(dx=mv.x, dy=mv.y) -def random_placement(parts: list[Part], **options): +def random_placement(parts: list[Part], **options: Unpack[Options]): """Randomly place parts within an appropriately-sized area. Args: @@ -819,7 +819,7 @@ def random_placement(parts: list[Part], **options): """ # Compute appropriate size to hold the parts based on their areas. - bbox = define_placement_bbox(parts, **options) + bbox = define_placement_bbox(parts, **options: Unpack[Options]) # Place parts randomly within area. for part in parts: @@ -832,7 +832,7 @@ def push_and_pull( mobile_parts: list[Part], nets: list[Net], force_func: Callable[[Part, list[Part], ...], Vector], - **options + **options: Unpack[Options] ): """Move parts under influence of attractive nets and repulsive part overlaps. @@ -855,7 +855,7 @@ def push_and_pull( def cost(parts: list[Part], alpha: float) -> float: """Cost function for use in debugging. Should decrease as parts move.""" for part in parts: - part.force = force_func(part, parts, scale=scale, alpha=alpha, **options) + part.force = force_func(part, parts, scale=scale, alpha=alpha, **options: Unpack[Options]) return sum((part.force.magnitude for part in parts)) # Get PyGame screen, real-to-screen coord Tx matrix, font for debug drawing. @@ -874,7 +874,7 @@ def cost(parts: list[Part], alpha: float) -> float: rmv_drift = not anchored_parts # Set scale factor between attractive net forces and repulsive part overlap forces. - scale = scale_attractive_repulsive_forces(parts, force_func, **options) + scale = scale_attractive_repulsive_forces(parts, force_func, **options: Unpack[Options]) # Setup the schedule for adjusting the alpha coefficient that weights the # combination of the attractive net forces and the repulsive part overlap forces. @@ -918,7 +918,7 @@ def cost(parts: list[Part], alpha: float) -> float: sum_of_forces = 0 for part in mobile_parts: part.force = force_func( - part, parts, scale=scale, alpha=alpha, **options + part, parts, scale=scale, alpha=alpha, **options: Unpack[Options] ) # Mask X or Y component of force during part alignment. part.force = part.force.mask(force_mask) @@ -974,7 +974,7 @@ def evolve_placement( mobile_parts: list[Part], nets: list[Net], force_func: Callable[[Part, list[Part], ...], Vector], - **options + **options: Unpack[Options] ): """Evolve part placement looking for optimum using force function. @@ -989,7 +989,7 @@ def evolve_placement( parts = anchored_parts + mobile_parts # Force-directed placement. - push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options) + push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options: Unpack[Options]) # Snap parts to grid. for part in parts: @@ -1001,7 +1001,7 @@ def place_net_terminals( placed_parts: list[Part], nets: list[Net], force_func: Callable[[Part, list[Part], ...], Vector], - **options + **options: Unpack[Options] ): """Place net terminals around already-placed parts. @@ -1087,7 +1087,7 @@ def evolution(net_terminals: list["NetTerminal"], placed_parts: list[Part], bbox if evolution_type == "all_at_once": evolve_placement( - placed_parts, net_terminals, nets, total_part_force, **options + placed_parts, net_terminals, nets, total_part_force, **options: Unpack[Options] ) elif evolution_type == "outer_to_inner": @@ -1126,7 +1126,7 @@ def dist_to_bbox_edge(term: "NetTerminal"): mobile_terminals[:-1], nets, force_func, - **options + **options: Unpack[Options] ) # Anchor the mobile terminals after their placement is done. anchored_parts.extend(mobile_terminals[:-1]) @@ -1137,7 +1137,7 @@ def dist_to_bbox_edge(term: "NetTerminal"): if mobile_terminals: # Evolve placement of any remaining terminals. evolve_placement( - anchored_parts, mobile_terminals, nets, total_part_force, **options + anchored_parts, mobile_terminals, nets, total_part_force, **options: Unpack[Options] ) bbox = get_enclosing_bbox(placed_parts) @@ -1152,7 +1152,7 @@ def dist_to_bbox_edge(term: "NetTerminal"): class Placer: """Mixin to add place function to Node class.""" - def group_parts(node: "SchNode", **options): + def group_parts(node: "SchNode", **options: Unpack[Options]): """Group parts in the Node that are connected by internal nets Args: @@ -1200,7 +1200,7 @@ def group_parts(node: "SchNode", **options): return connected_parts, internal_nets, floating_parts - def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], **options): + def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], **options: Unpack[Options]): """Place individual parts. Args: @@ -1215,10 +1215,10 @@ def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], * return # Add bboxes with surrounding area so parts are not butted against each other. - add_placement_bboxes(parts, **options) + add_placement_bboxes(parts, **options: Unpack[Options]) # Set anchor and pull pins that determine attractive forces between parts. - add_anchor_pull_pins(parts, nets, **options) + add_anchor_pull_pins(parts, nets, **options: Unpack[Options]) # Randomly place connected parts. random_placement(parts) @@ -1232,7 +1232,7 @@ def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], * ) if options.get("compress_before_place"): - central_placement(parts, **options) + central_placement(parts, **options: Unpack[Options]) # Do force-directed placement of the parts in the parts. @@ -1241,24 +1241,24 @@ def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], * real_parts = [part for part in parts if not is_net_terminal(part)] # Do the first trial placement. - evolve_placement([], real_parts, nets, total_part_force, **options) + evolve_placement([], real_parts, nets, total_part_force, **options: Unpack[Options]) if options.get("rotate_parts"): # Adjust part orientations after first trial placement is done. - if adjust_orientations(real_parts, **options): + if adjust_orientations(real_parts, **options: Unpack[Options]): # Some part orientations were changed, so re-do placement. - evolve_placement([], real_parts, nets, total_part_force, **options) + evolve_placement([], real_parts, nets, total_part_force, **options: Unpack[Options]) # Place NetTerminals after all the other parts. place_net_terminals( - net_terminals, real_parts, nets, total_part_force, **options + net_terminals, real_parts, nets, total_part_force, **options: Unpack[Options] ) if options.get("draw_placement"): # Pause to look at placement for debugging purposes. draw_pause() - def place_floating_parts(node: "SchNode", parts: list[Part], **options): + def place_floating_parts(node: "SchNode", parts: list[Part], **options: Unpack[Options]): """Place individual parts. Args: @@ -1275,7 +1275,7 @@ def place_floating_parts(node: "SchNode", parts: list[Part], **options): add_placement_bboxes(parts) # Set anchor and pull pins that determine attractive forces between similar parts. - add_anchor_pull_pins(parts, [], **options) + add_anchor_pull_pins(parts, [], **options: Unpack[Options]) # Randomly place the floating parts. random_placement(parts) @@ -1310,10 +1310,10 @@ def place_floating_parts(node: "SchNode", parts: list[Part], **options): if options.get("compress_before_place"): # Compress all floating parts together. - central_placement(parts, **options) + central_placement(parts, **options: Unpack[Options]) # Do force-directed placement of the parts in the group. - evolve_placement([], parts, [], force_func, **options) + evolve_placement([], parts, [], force_func, **options: Unpack[Options]) if options.get("draw_placement"): # Pause to look at placement for debugging purposes. @@ -1324,7 +1324,7 @@ def place_blocks( connected_parts: list[list[Part]], floating_parts: list[Part], children: list["SchNode"], - **options + **options: Unpack[Options] ): """Place blocks of parts and hierarchical sheets. @@ -1438,7 +1438,7 @@ def place_blocks( # Arrange the part blocks with similarity force-directed placement. force_func = functools.partial(total_similarity_force, similarity=blk_attr) - evolve_placement([], part_blocks, [], force_func, **options) + evolve_placement([], part_blocks, [], force_func, **options: Unpack[Options]) if options.get("draw_placement"): # Pause to look at placement for debugging purposes. @@ -1486,7 +1486,7 @@ def rmv_placement_stuff(node: "SchNode"): ) rmv_attr(node.get_internal_nets(), ("parts",)) - def place(node: "SchNode", **options): + def place(node: "SchNode", **options: Unpack[Options]): """Place the parts and children in this node. Args: @@ -1503,22 +1503,22 @@ def place(node: "SchNode", **options): # First, recursively place children of this node. # TODO: Child nodes are independent, so can they be processed in parallel? for child in node.children.values(): - child.place(**options) + child.place(**options: Unpack[Options]) # Group parts into those that are connected by explicit nets and # those that float freely connected only by stub nets. - connected_parts, internal_nets, floating_parts = node.group_parts(**options) + connected_parts, internal_nets, floating_parts = node.group_parts(**options: Unpack[Options]) # Place each group of connected parts. for group in connected_parts: - node.place_connected_parts(list(group), internal_nets, **options) + node.place_connected_parts(list(group), internal_nets, **options: Unpack[Options]) # Place the floating parts that have no connections to anything else. - node.place_floating_parts(list(floating_parts), **options) + node.place_floating_parts(list(floating_parts), **options: Unpack[Options]) # Now arrange all the blocks of placed parts and the child nodes within this node. node.place_blocks( - connected_parts, floating_parts, node.children.values(), **options + connected_parts, floating_parts, node.children.values(), **options: Unpack[Options] ) # Remove any stuff leftover from this place & route run. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 1a59067f..355bf4a7 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -1,6 +1,6 @@ """Replace common skidl functions with our own""" -from typing import Any, Iterator +from typing import Any, Iterator, TypedDict from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector @@ -86,10 +86,34 @@ class Net: # We need to assign these name: str netio: str # whether input or output - - # TODO: where are these expected to be assigned? - pins: list[Pin] # TODO: - parts: set[Part] # TODO: + pins: list[Pin] + parts: set[Part] def __iter__(self) -> Iterator[Pin]: yield from self.pins + + +class Options(TypedDict): + allow_routing_failure: bool + compress_before_place: bool + dont_rotate_pin_count: int + draw_assigned_terminals: bool + draw_font: str + draw_global_routing: bool + draw_placement: bool + draw_routing_channels: bool + draw_routing: bool + draw_scr: bool + draw_switchbox_boundary: bool + draw_switchbox_routing: bool + draw_tx: Tx + expansion_factor: float + graphics_only: bool + net_normalize: bool + pin_normalize: bool + pt_to_pt_mult: float + rotate_parts: bool + seed: int + show_capacities: bool + terminal_evolution: str + use_push_pull: bool From acd9581881ebb25eef3e365c6c0e0ba75a2b9d2a Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 17:47:29 +0200 Subject: [PATCH 20/85] Continue typing, over on route --- .../exporters/schematic/kicad/skidl/route.py | 94 ++++++++++--------- 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index d0daa1f4..d7360401 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -12,6 +12,7 @@ from collections import Counter, defaultdict from enum import Enum from itertools import chain, zip_longest +from typing import TYPE_CHECKING, Unpack from .constants import DRAWING_BOX_RESIZE, GRID from .debug_draw import ( @@ -22,8 +23,11 @@ draw_start, draw_text, ) -from .geometry import BBox, Point, Segment, Vector -from .shims import Part, rmv_attr +from .geometry import BBox, Point, Segment, Tx, Vector +from .shims import Net, Options, Part, Pin, rmv_attr + +if TYPE_CHECKING: + from pygame import Surface # skidl calls this Screen ################################################################### # @@ -132,11 +136,11 @@ class Boundary: boundary = Boundary() # Absolute coords of all part pins. Used when trimming stub nets. -pin_pts = [] +pin_pts: list[Point] = [] class Terminal: - def __init__(self, net, face, coord): + def __init__(self, net: Net, face: "Face", coord: float): """Terminal on a Face from which a net is routed within a SwitchBox. Args: @@ -164,7 +168,7 @@ def route_pt(self): else: return Point(track.coord, self.coord) - def get_next_terminal(self, next_face): + def get_next_terminal(self, next_face: "Face"): """Get the terminal on the next face that lies on the same net as this terminal. This method assumes the terminal's face and the next face are faces of the @@ -230,7 +234,7 @@ def get_next_terminal(self, next_face): # Well, something went wrong. Should have found *something*! raise GlobalRoutingFailure - def draw(self, scr, tx, **options): + def draw(self, scr: "Surface", tx: Tx, **options: Unpack[Options]): """Draw a Terminal for debugging purposes. Args: @@ -246,7 +250,7 @@ def draw(self, scr, tx, **options): class Interval(object): - def __init__(self, beg, end): + def __init__(self, beg: "GlobalTrack", end: "GlobalTrack"): """Define an interval with a beginning and an end. Args: @@ -280,7 +284,7 @@ def intersects(self, other): """Return True if the intervals overlap (even if only at one point).""" return not ((self.beg > other.end) or (self.end < other.beg)) - def interval_intersection(self, other): + def interval_intersection(self, other: "Interval") -> "Interval | None": """Return intersection of two intervals as an interval, otherwise None.""" if self.intersects(other): beg = max(self.beg, other.beg) @@ -290,7 +294,7 @@ def interval_intersection(self, other): return Interval(beg, end) return None - def merge(self, other): + def merge(self, other: "Interval") -> "Interval | None": """Return a merged interval if the given intervals intersect, otherwise return None.""" if Interval.intersects(self, other): return Interval(min(self.beg, other.beg), max(self.end, other.end)) @@ -298,7 +302,7 @@ def merge(self, other): class NetInterval(Interval): - def __init__(self, net, beg, end): + def __init__(self, net: Net, beg: "GlobalTrack", end: "GlobalTrack"): """Define an Interval with an associated net (useful for wire traces in a switchbox). Args: @@ -309,11 +313,11 @@ def __init__(self, net, beg, end): super().__init__(beg, end) self.net = net - def obstructs(self, other): + def obstructs(self, other: "NetInterval") -> bool: """Return True if the intervals intersect and have different nets.""" return super().intersects(other) and (self.net is not other.net) - def merge(self, other): + def merge(self, other: "NetInterval") -> "NetInterval | None": """Return a merged interval if the given intervals intersect and are on the same net, otherwise return None.""" if self.net is other.net: merged_intvl = super().merge(other) @@ -356,7 +360,13 @@ def __init__(self, from_face, to_face): class Face(Interval): """A side of a rectangle bounding a routing switchbox.""" - def __init__(self, part, track, beg, end): + def __init__( + self, + part: set[Part | Boundary] | Part | Boundary, + track: "GlobalTrack", + beg: "GlobalTrack", + end: "GlobalTrack", + ): """One side of a routing switchbox. Args: @@ -375,20 +385,20 @@ def __init__(self, part, track, beg, end): super().__init__(beg, end) # Store Part/Boundary the Face is part of, if any. - self.part = set() + self.part: set[Part | Boundary] = set() if isinstance(part, set): self.part.update(part) elif part is not None: self.part.add(part) # Storage for any part pins that lie along this Face. - self.pins = [] + self.pins: list[Pin] = [] # Storage for routing terminals along this face. - self.terminals = [] + self.terminals: list[Terminal] = [] # Set of Faces adjacent to this one. (Starts empty.) - self.adjacent = set() + self.adjacent: set[Face] = set() # Add this new face to the track it belongs to so it isn't lost. self.track = track @@ -677,7 +687,7 @@ def seg(self): return Segment(p1, p2) def draw( - self, scr, tx, font, color=(128, 128, 128), thickness=2, dot_radius=0, **options + self, scr, tx, font, color=(128, 128, 128), thickness=2, dot_radius=0, **options: Unpack[Options] ): """Draw a Face in the drawing area. @@ -698,7 +708,7 @@ def draw( # Draw the terminals on the Face. for terminal in self.terminals: - terminal.draw(scr, tx, **options) + terminal.draw(scr, tx, **options: Unpack[Options]) if options.get("show_capacities"): # Show the wiring capacity at the midpoint of the Face. @@ -761,7 +771,7 @@ def cvt_faces_to_terminals(self): # be the current element on the next iteration. self[i + 1] = self[i].get_next_terminal(self[i + 1]) - def draw(self, scr, tx, color=(0, 0, 0), thickness=1, dot_radius=10, **options): + def draw(self, scr, tx, color=(0, 0, 0), thickness=1, dot_radius=10, **options: Unpack[Options]): """Draw a global wire from Face-to-Face in the drawing area. Args: @@ -815,7 +825,7 @@ def cvt_faces_to_terminals(self): wire.cvt_faces_to_terminals() def draw( - self, scr, tx, font, color=(0, 0, 0), thickness=1, dot_radius=10, **options + self, scr, tx, font, color=(0, 0, 0), thickness=1, dot_radius=10, **options: Unpack[Options] ): """Draw the GlobalWires of this route in the drawing area. @@ -833,7 +843,7 @@ def draw( """ for wire in self: - wire.draw(scr, tx, color, thickness, dot_radius, **options) + wire.draw(scr, tx, color, thickness, dot_radius, **options: Unpack[Options]) class GlobalTrack(list): @@ -974,7 +984,7 @@ def audit(self): if first_face.has_overlap(second_face): raise AssertionError - def draw(self, scr, tx, font, **options): + def draw(self, scr, tx, font, **options: Unpack[Options]): """Draw the Faces in a track. Args: @@ -984,7 +994,7 @@ def draw(self, scr, tx, font, **options): options (dict, optional): Dictionary of options and values. Defaults to {}. """ for face in self: - face.draw(scr, tx, font, **options) + face.draw(scr, tx, font, **options: Unpack[Options]) class Target: @@ -1380,7 +1390,7 @@ def has_nets(self): or self.right_face.has_nets() ) - def route(self, **options): + def route(self, **options: Unpack[Options]): """Route wires between terminals on the switchbox faces. Args: @@ -1943,7 +1953,7 @@ def trim_column_intervals(column, track_nets, next_track_nets): return self.segments def draw( - self, scr=None, tx=None, font=None, color=(128, 0, 128), thickness=2, **options + self, scr=None, tx=None, font=None, color=(128, 0, 128), thickness=2, **options: Unpack[Options] ): """Draw a switchbox and its routing for debugging purposes. @@ -1968,10 +1978,10 @@ def draw( if options.get("draw_switchbox_boundary"): # Draw switchbox boundary. - self.top_face.draw(scr, tx, font, color, thickness, **options) - self.bottom_face.draw(scr, tx, font, color, thickness, **options) - self.left_face.draw(scr, tx, font, color, thickness, **options) - self.right_face.draw(scr, tx, font, color, thickness, **options) + self.top_face.draw(scr, tx, font, color, thickness, **options: Unpack[Options]) + self.bottom_face.draw(scr, tx, font, color, thickness, **options: Unpack[Options]) + self.left_face.draw(scr, tx, font, color, thickness, **options: Unpack[Options]) + self.right_face.draw(scr, tx, font, color, thickness, **options: Unpack[Options]) if options.get("draw_switchbox_routing"): # Draw routed wire segments. @@ -2356,7 +2366,7 @@ def rank_net(net): return global_routes - def create_switchboxes(node, h_tracks, v_tracks, **options): + def create_switchboxes(node, h_tracks, v_tracks, **options: Unpack[Options]): """Create routing switchboxes from the faces in the horz/vert tracks. Args: @@ -2406,7 +2416,7 @@ def create_switchboxes(node, h_tracks, v_tracks, **options): return switchboxes - def switchbox_router(node, switchboxes, **options): + def switchbox_router(node, switchboxes, **options: Unpack[Options]): """Create detailed wiring between the terminals along the sides of each switchbox. Args: @@ -2422,13 +2432,13 @@ def switchbox_router(node, switchboxes, **options): for swbx in switchboxes: try: # Try routing switchbox from left-to-right. - swbx.route(**options) + swbx.route(**options: Unpack[Options]) except RoutingFailure: # Routing failed, so try routing top-to-bottom instead. swbx.flip_xy() # If this fails, then a routing exception will terminate the whole routing process. - swbx.route(**options) + swbx.route(**options: Unpack[Options]) swbx.flip_xy() # Add switchbox routes to existing node wiring. @@ -3103,7 +3113,7 @@ def rmv_routing_stuff(node): for part in node.parts: rmv_attr(part.pins, ("route_pt", "face")) - def route(node, tool=None, **options): + def route(node, tool=None, **options: Unpack[Options]): """Route the wires between part pins in this node and its children. Steps: @@ -3135,7 +3145,7 @@ def route(node, tool=None, **options): # First, recursively route any children of this node. # TODO: Child nodes are independent so could they be processed in parallel? for child in node.children.values(): - child.route(tool=tool, **options) + child.route(tool=tool, **options: Unpack[Options]) # Exit if no parts to route in this node. if not node.parts: @@ -3167,7 +3177,7 @@ def route(node, tool=None, **options): # Draw part outlines, routing tracks and terminals. if options.get("draw_routing_channels"): draw_routing( - node, routing_bbox, node.parts, h_tracks, v_tracks, **options + node, routing_bbox, node.parts, h_tracks, v_tracks, **options: Unpack[Options] ) # Do global routing of nets internal to the node. @@ -3186,7 +3196,7 @@ def route(node, tool=None, **options): h_tracks, v_tracks, global_routes, - **options + **options: Unpack[Options] ) # Create detailed wiring using switchbox routing for the global routes. @@ -3200,10 +3210,10 @@ def route(node, tool=None, **options): node.parts, switchboxes, global_routes, - **options + **options: Unpack[Options] ) - node.switchbox_router(switchboxes, **options) + node.switchbox_router(switchboxes, **options: Unpack[Options]) # If enabled, draw the global and detailed routing for debug purposes. if options.get("draw_switchbox_routing"): @@ -3213,7 +3223,7 @@ def route(node, tool=None, **options): node.parts, global_routes, switchboxes, - **options + **options: Unpack[Options] ) # Now clean-up the wires and add junctions. @@ -3222,7 +3232,7 @@ def route(node, tool=None, **options): # If enabled, draw the global and detailed routing for debug purposes. if options.get("draw_switchbox_routing"): - draw_routing(node, routing_bbox, node.parts, **options) + draw_routing(node, routing_bbox, node.parts, **options: Unpack[Options]) # Remove any stuff leftover from this place & route run. node.rmv_routing_stuff() From 2f58c97d921555ce0aa57671b19ba0ad66cab550 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 18:05:10 +0200 Subject: [PATCH 21/85] Typing.... --- .../exporters/schematic/kicad/skidl/route.py | 103 +++++++++++------- .../exporters/schematic/kicad/skidl/shims.py | 12 +- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index d7360401..45589927 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: from pygame import Surface # skidl calls this Screen + from .node import SchNode ################################################################### # @@ -250,6 +251,10 @@ def draw(self, scr: "Surface", tx: Tx, **options: Unpack[Options]): class Interval(object): + # type hints for the derived classes + beg: "GlobalTrack" + end: "GlobalTrack" + def __init__(self, beg: "GlobalTrack", end: "GlobalTrack"): """Define an interval with a beginning and an end. @@ -267,20 +272,20 @@ def __init__(self, beg: "GlobalTrack", end: "GlobalTrack"): self.beg = beg self.end = end - def __bool__(self): + def __bool__(self) -> bool: """An Interval object always returns True.""" return True @property - def len(self): + def len(self) -> float: """Return the length of the interval.""" return self.end - self.beg - def __len__(self): + def __len__(self) -> float: """Return the length of the interval.""" return self.len - def intersects(self, other): + def intersects(self, other: "Interval") -> bool: """Return True if the intervals overlap (even if only at one point).""" return not ((self.beg > other.end) or (self.end < other.beg)) @@ -328,7 +333,7 @@ def merge(self, other: "NetInterval") -> "NetInterval | None": class Adjacency: - def __init__(self, from_face, to_face): + def __init__(self, from_face: "Face", to_face: "Face"): """Define an adjacency between two Faces. Args: @@ -407,7 +412,7 @@ def __init__( # Storage for switchboxes this face is part of. self.switchboxes = set() - def combine(self, other): + def combine(self, other: "Face"): """Combine information from other face into this one. Args: @@ -424,12 +429,12 @@ def combine(self, other): self.switchboxes.update(other.switchboxes) @property - def length(self): + def length(self) -> float: """Return the length of the face.""" return self.end.coord - self.beg.coord @property - def bbox(self): + def bbox(self) -> BBox: """Return the bounding box of the 1-D face segment.""" bbox = BBox() @@ -444,7 +449,7 @@ def bbox(self): return bbox - def add_terminal(self, net, coord): + def add_terminal(self, net: Net, coord: float) -> None: """Create a Terminal on the Face. Args: @@ -480,7 +485,7 @@ def add_terminal(self, net, coord): # Create a new Terminal and add it to the list of terminals for this face. self.terminals.append(Terminal(net, self, coord)) - def trim_repeated_terminals(self): + def trim_repeated_terminals(self) -> None: """Remove all but one terminal of each individual net from the face. Notes: @@ -489,7 +494,7 @@ def trim_repeated_terminals(self): """ # Find the intersection of every non-part face in the track with this one. - intersections = [] + intersections: list[Interval] = [] for face in self.track: if not face.part: intersection = self.interval_intersection(face) @@ -527,7 +532,7 @@ def trim_repeated_terminals(self): for terminal in terminals[1:]: # Keep only the 1st terminal. self.track.remove_terminal(terminal) - def create_nonpin_terminals(self): + def create_nonpin_terminals(self) -> None: """Create unassigned terminals along a non-part Face with GRID spacing. These terminals will be used during global routing of nets from @@ -553,7 +558,7 @@ def create_nonpin_terminals(self): for coord in range(beg, end, GRID): self.add_terminal(None, coord) - def set_capacity(self): + def set_capacity(self) -> None: """Set the wire routing capacity of a Face.""" if self.part: @@ -563,11 +568,11 @@ def set_capacity(self): # Wire routing capacity for other faces is the number of terminals they have. self.capacity = len(self.terminals) - def has_nets(self): + def has_nets(self) -> bool: """Return True if any Terminal on the Face is attached to a net.""" return any((terminal.net for terminal in self.terminals)) - def add_adjacencies(self): + def add_adjacencies(self) -> None: """Add adjacent faces of the switchbox having this face as the top face.""" # Create a temporary switchbox. @@ -577,7 +582,7 @@ def add_adjacencies(self): # This face doesn't belong to a valid switchbox. return - def add_adjacency(from_, to): + def add_adjacency(from_: Face, to: Face) -> None: # Faces on the boundary can never accept wires so they are never # adjacent to any other face. if boundary in from_.part or boundary in to.part: @@ -604,7 +609,7 @@ def add_adjacency(from_, to): # Get rid of the temporary switchbox. del swbx - def extend(self, orthogonal_tracks): + def extend(self, orthogonal_tracks: list["GlobalTrack"]) -> None: """Extend a Face along its track until it is blocked by an orthogonal face. This is used to create Faces that form the irregular grid of switchboxes. @@ -652,7 +657,7 @@ def extend(self, orthogonal_tracks): if blocked: break - def split(self, trk): + def split(self, trk: "GlobalTrack") -> None: """If a track intersects in the middle of a face, split the face into two faces.""" if self.beg < trk < self.end: @@ -661,20 +666,20 @@ def split(self, trk): # Move the beginning of the original Face to trk. self.beg = trk - def coincides_with(self, other_face): + def coincides_with(self, other_face: "Face") -> bool: """Return True if both faces have the same beginning and ending point on the same track.""" return (self.beg, self.end) == (other_face.beg, other_face.end) - def has_overlap(self, other_face): + def has_overlap(self, other_face: "Face") -> bool: """Return True if the two faces overlap.""" return self.beg < other_face.end and self.end > other_face.beg - def audit(self): + def audit(self) -> None: """Raise exception if face is malformed.""" assert len(self.switchboxes) <= 2 @property - def seg(self): + def seg(self) -> Segment: """Return a Segment that coincides with the Face.""" if self.track.orientation == Orientation.VERT: @@ -808,7 +813,7 @@ def draw(self, scr, tx, color=(0, 0, 0), thickness=1, dot_radius=10, **options: ) -class GlobalRoute(list): +class GlobalRoute(list[GlobalWire]): def __init__(self, *args, **kwargs): """A list containing GlobalWires that form an entire routing of a net. @@ -825,7 +830,14 @@ def cvt_faces_to_terminals(self): wire.cvt_faces_to_terminals() def draw( - self, scr, tx, font, color=(0, 0, 0), thickness=1, dot_radius=10, **options: Unpack[Options] + self, + scr, + tx, + font, + color=(0, 0, 0), + thickness=1, + dot_radius=10, + **options: Unpack[Options], ): """Draw the GlobalWires of this route in the drawing area. @@ -843,11 +855,18 @@ def draw( """ for wire in self: - wire.draw(scr, tx, color, thickness, dot_radius, **options: Unpack[Options]) + wire.draw(scr, tx, color, thickness, dot_radius, **options) -class GlobalTrack(list): - def __init__(self, orientation=Orientation.HORZ, coord=0, idx=None, *args, **kwargs): +class GlobalTrack(list[Face]): + def __init__( + self, + orientation: Orientation = Orientation.HORZ, + coord: int = 0, + idx: int = None, + *args, + **kwargs, + ): """A horizontal/vertical track holding zero or more faces all having the same Y/X coordinate. These global tracks are made by extending the edges of part bounding boxes to @@ -994,7 +1013,7 @@ def draw(self, scr, tx, font, **options: Unpack[Options]): options (dict, optional): Dictionary of options and values. Defaults to {}. """ for face in self: - face.draw(scr, tx, font, **options: Unpack[Options]) + face.draw(scr, tx, font, **options) class Target: @@ -1978,10 +1997,10 @@ def draw( if options.get("draw_switchbox_boundary"): # Draw switchbox boundary. - self.top_face.draw(scr, tx, font, color, thickness, **options: Unpack[Options]) - self.bottom_face.draw(scr, tx, font, color, thickness, **options: Unpack[Options]) - self.left_face.draw(scr, tx, font, color, thickness, **options: Unpack[Options]) - self.right_face.draw(scr, tx, font, color, thickness, **options: Unpack[Options]) + self.top_face.draw(scr, tx, font, color, thickness, **options) + self.bottom_face.draw(scr, tx, font, color, thickness, **options) + self.left_face.draw(scr, tx, font, color, thickness, **options) + self.right_face.draw(scr, tx, font, color, thickness, **options) if options.get("draw_switchbox_routing"): # Draw routed wire segments. @@ -2100,7 +2119,7 @@ def create_routing_tracks(node, routing_bbox): for idx, coord in enumerate(h_track_coord) ] - def bbox_to_faces(part, bbox): + def bbox_to_faces(part: Part, bbox: BBox): left_track = v_tracks[v_track_coord.index(bbox.min.x)] right_track = v_tracks[v_track_coord.index(bbox.max.x)] bottom_track = h_tracks[h_track_coord.index(bbox.min.y)] @@ -2432,13 +2451,13 @@ def switchbox_router(node, switchboxes, **options: Unpack[Options]): for swbx in switchboxes: try: # Try routing switchbox from left-to-right. - swbx.route(**options: Unpack[Options]) + swbx.route(**options) except RoutingFailure: # Routing failed, so try routing top-to-bottom instead. swbx.flip_xy() # If this fails, then a routing exception will terminate the whole routing process. - swbx.route(**options: Unpack[Options]) + swbx.route(**options) swbx.flip_xy() # Add switchbox routes to existing node wiring. @@ -3145,7 +3164,7 @@ def route(node, tool=None, **options: Unpack[Options]): # First, recursively route any children of this node. # TODO: Child nodes are independent so could they be processed in parallel? for child in node.children.values(): - child.route(tool=tool, **options: Unpack[Options]) + child.route(tool=tool, **options) # Exit if no parts to route in this node. if not node.parts: @@ -3177,7 +3196,7 @@ def route(node, tool=None, **options: Unpack[Options]): # Draw part outlines, routing tracks and terminals. if options.get("draw_routing_channels"): draw_routing( - node, routing_bbox, node.parts, h_tracks, v_tracks, **options: Unpack[Options] + node, routing_bbox, node.parts, h_tracks, v_tracks, **options ) # Do global routing of nets internal to the node. @@ -3196,7 +3215,7 @@ def route(node, tool=None, **options: Unpack[Options]): h_tracks, v_tracks, global_routes, - **options: Unpack[Options] + **options ) # Create detailed wiring using switchbox routing for the global routes. @@ -3210,10 +3229,10 @@ def route(node, tool=None, **options: Unpack[Options]): node.parts, switchboxes, global_routes, - **options: Unpack[Options] + **options ) - node.switchbox_router(switchboxes, **options: Unpack[Options]) + node.switchbox_router(switchboxes, **options) # If enabled, draw the global and detailed routing for debug purposes. if options.get("draw_switchbox_routing"): @@ -3223,7 +3242,7 @@ def route(node, tool=None, **options: Unpack[Options]): node.parts, global_routes, switchboxes, - **options: Unpack[Options] + **options ) # Now clean-up the wires and add junctions. @@ -3232,7 +3251,7 @@ def route(node, tool=None, **options: Unpack[Options]): # If enabled, draw the global and detailed routing for debug purposes. if options.get("draw_switchbox_routing"): - draw_routing(node, routing_bbox, node.parts, **options: Unpack[Options]) + draw_routing(node, routing_bbox, node.parts, **options) # Remove any stuff leftover from this place & route run. node.rmv_routing_stuff() diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 355bf4a7..f1b108a7 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -1,9 +1,12 @@ """Replace common skidl functions with our own""" -from typing import Any, Iterator, TypedDict +from typing import TYPE_CHECKING, Any, Iterator, TypedDict from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector +if TYPE_CHECKING: + from .route import GlobalTrack + def get_script_name(): # TODO: @@ -51,6 +54,10 @@ class Part: original_tx: Tx # internal use force: Vector # used for debugging mv: Vector # internal use + left_track: "GlobalTrack" + right_track: "GlobalTrack" + top_track: "GlobalTrack" + bottom_track: "GlobalTrack" def __iter__(self) -> Iterator["Pin"]: # TODO: @@ -89,8 +96,9 @@ class Net: pins: list[Pin] parts: set[Part] - def __iter__(self) -> Iterator[Pin]: + def __iter__(self) -> Iterator[Pin | Part]: yield from self.pins + yield from self.parts class Options(TypedDict): From ce2f3410e2ab764924d74c564168f30057e1a112 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 18:18:28 +0200 Subject: [PATCH 22/85] Finish module-level typing route.py --- .../exporters/schematic/kicad/skidl/route.py | 189 ++++++++++++------ .../exporters/schematic/kicad/skidl/shims.py | 10 +- 2 files changed, 135 insertions(+), 64 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index 45589927..8e13c1a8 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -692,8 +692,15 @@ def seg(self) -> Segment: return Segment(p1, p2) def draw( - self, scr, tx, font, color=(128, 128, 128), thickness=2, dot_radius=0, **options: Unpack[Options] - ): + self, + scr: Surface, + tx: Tx, + font, + color: tuple[int, int, int] = (128, 128, 128), + thickness=2, + dot_radius=0, + **options: Unpack[Options], + ) -> None: """Draw a Face in the drawing area. Args: @@ -713,7 +720,7 @@ def draw( # Draw the terminals on the Face. for terminal in self.terminals: - terminal.draw(scr, tx, **options: Unpack[Options]) + terminal.draw(scr, tx, **options) if options.get("show_capacities"): # Show the wiring capacity at the midpoint of the Face. @@ -721,8 +728,8 @@ def draw( draw_text(str(self.capacity), mid_pt, scr, tx, font=font, color=color) -class GlobalWire(list): - def __init__(self, net, *args, **kwargs): +class GlobalWire(list[Face | Terminal]): + def __init__(self, net: Net, *args, **kwargs): """A list connecting switchbox faces and terminals. Global routes start off as a sequence of switchbox faces that the route @@ -737,7 +744,7 @@ def __init__(self, net, *args, **kwargs): self.net = net super().__init__(*args, **kwargs) - def cvt_faces_to_terminals(self): + def cvt_faces_to_terminals(self) -> None: """Convert global face-to-face route to switchbox terminal-to-terminal route.""" if not self: @@ -776,7 +783,15 @@ def cvt_faces_to_terminals(self): # be the current element on the next iteration. self[i + 1] = self[i].get_next_terminal(self[i + 1]) - def draw(self, scr, tx, color=(0, 0, 0), thickness=1, dot_radius=10, **options: Unpack[Options]): + def draw( + self, + scr: Surface, + tx: Tx, + color: tuple[int, int, int] = (0, 0, 0), + thickness: int = 1, + dot_radius: int = 10, + **options: Unpack[Options], + ) -> None: """Draw a global wire from Face-to-Face in the drawing area. Args: @@ -824,21 +839,21 @@ def __init__(self, *args, **kwargs): """ super().__init__(*args, **kwargs) - def cvt_faces_to_terminals(self): + def cvt_faces_to_terminals(self) -> None: """Convert GlobalWires in route to switchbox terminal-to-terminal route.""" for wire in self: wire.cvt_faces_to_terminals() def draw( self, - scr, - tx, + scr: Surface, + tx: Tx, font, - color=(0, 0, 0), - thickness=1, - dot_radius=10, + color: tuple[int, int, int] = (0, 0, 0), + thickness: int = 1, + dot_radius: int = 10, **options: Unpack[Options], - ): + ) -> None: """Draw the GlobalWires of this route in the drawing area. Args: @@ -889,35 +904,35 @@ def __init__( # This stores the orthogonal tracks that intersect this one. self.splits = set() - def __eq__(self, track): + def __eq__(self, track: "GlobalTrack") -> bool: """Used for ordering tracks.""" return self.coord == track.coord - def __ne__(self, track): + def __ne__(self, track: "GlobalTrack") -> bool: """Used for ordering tracks.""" return self.coord != track.coord - def __lt__(self, track): + def __lt__(self, track: "GlobalTrack") -> bool: """Used for ordering tracks.""" return self.coord < track.coord - def __le__(self, track): + def __le__(self, track: "GlobalTrack") -> bool: """Used for ordering tracks.""" return self.coord <= track.coord - def __gt__(self, track): + def __gt__(self, track: "GlobalTrack") -> bool: """Used for ordering tracks.""" return self.coord > track.coord - def __ge__(self, track): + def __ge__(self, track: "GlobalTrack") -> bool: """Used for ordering tracks.""" return self.coord >= track.coord - def __sub__(self, other): + def __sub__(self, other: "GlobalTrack") -> int: """Subtract coords of two tracks.""" return self.coord - other.coord - def extend_faces(self, orthogonal_tracks): + def extend_faces(self, orthogonal_tracks: list["GlobalTrack"]) -> None: """Extend the faces in a track. This is part of forming the irregular grid of switchboxes. @@ -929,15 +944,15 @@ def extend_faces(self, orthogonal_tracks): for face in self[:]: face.extend(orthogonal_tracks) - def __hash__(self): + def __hash__(self) -> int: """This method lets a track be inserted into a set of splits.""" return self.idx - def add_split(self, orthogonal_track): + def add_split(self, orthogonal_track: "GlobalTrack") -> None: """Store the orthogonal track that intersects this one.""" self.splits.add(orthogonal_track) - def add_face(self, face): + def add_face(self, face: Face) -> None: """Add a face to a track. Args: @@ -950,7 +965,7 @@ def add_face(self, face): self.add_split(face.beg) self.add_split(face.end) - def split_faces(self): + def split_faces(self) -> None: """Split track faces by any intersecting orthogonal tracks.""" for split in self.splits: @@ -960,11 +975,11 @@ def split_faces(self): # to the track this face is on. face.split(split) - def remove_duplicate_faces(self): + def remove_duplicate_faces(self) -> None: """Remove faces from the track having the same endpoints.""" # Create lists of faces having the same endpoints. - dup_faces_dict = defaultdict(list) + dup_faces_dict: defaultdict[tuple[Face, Face], list[Face]] = defaultdict(list) for face in self: key = (face.beg, face.end) dup_faces_dict[key].append(face) @@ -977,7 +992,7 @@ def remove_duplicate_faces(self): retained_face.combine(dup_face) self.remove(dup_face) - def remove_terminal(self, terminal): + def remove_terminal(self, terminal: "Terminal") -> None: """Remove a terminal from any non-part Faces in the track.""" coord = terminal.coord @@ -988,13 +1003,13 @@ def remove_terminal(self, terminal): if term.coord == coord: face.terminals.remove(term) - def add_adjacencies(self): + def add_adjacencies(self) -> None: """Add adjacent switchbox faces to each face in a track.""" for top_face in self: top_face.add_adjacencies() - def audit(self): + def audit(self) -> None: """Raise exception if track is malformed.""" for i, first_face in enumerate(self): @@ -1003,7 +1018,13 @@ def audit(self): if first_face.has_overlap(second_face): raise AssertionError - def draw(self, scr, tx, font, **options: Unpack[Options]): + def draw( + self, + scr: Surface, + tx: Tx, + font, + **options: Unpack[Options], + ) -> None: """Draw the Faces in a track. Args: @@ -1017,7 +1038,7 @@ def draw(self, scr, tx, font, **options: Unpack[Options]): class Target: - def __init__(self, net, row, col): + def __init__(self, net: "Net", row: int, col: int) -> None: """A point on a switchbox face that switchbox router has not yet reached. Targets are used to direct the switchbox router towards terminals that @@ -1035,7 +1056,7 @@ def __init__(self, net, row, col): self.col = col self.net = net - def __lt__(self, other): + def __lt__(self, other: "Target") -> bool: """Used for ordering Targets in terms of priority.""" # Targets in the left-most columns are given priority since they will be reached @@ -1051,7 +1072,13 @@ class SwitchBox: # Indices for faces of the switchbox. TOP, LEFT, BOTTOM, RIGHT = 0, 1, 2, 3 - def __init__(self, top_face, left_face=None, bottom_face=None, right_face=None): + def __init__( + self, + top_face: Face, + left_face: Face | None = None, + bottom_face: Face | None = None, + right_face: Face | None = None, + ) -> None: """Routing switchbox. A switchbox is a rectangular region through which wires are routed. @@ -1069,7 +1096,7 @@ def __init__(self, top_face, left_face=None, bottom_face=None, right_face=None): """ # Find the left face in the left track that bounds the top face. - if left_face == None: + if left_face is None: left_track = top_face.beg for face in left_track: # The left face will end at the track for the top face. @@ -1080,7 +1107,7 @@ def __init__(self, top_face, left_face=None, bottom_face=None, right_face=None): raise NoSwitchBox("Unroutable switchbox (left)!") # Find the right face in the right track that bounds the top face. - if right_face == None: + if right_face is None: right_track = top_face.end for face in right_track: # The right face will end at the track for the top face. @@ -1098,7 +1125,7 @@ def __init__(self, top_face, left_face=None, bottom_face=None, right_face=None): raise NoSwitchBox("Unroutable switchbox (left-right)!") # Find the bottom face in the track where the left/right faces begin. - if bottom_face == None: + if bottom_face is None: bottom_track = left_face.beg for face in bottom_track: # The bottom face should begin/end in the same places as the top face. @@ -1128,7 +1155,11 @@ def __init__(self, top_face, left_face=None, bottom_face=None, right_face=None): self.left_face.switchboxes.add(self) self.right_face.switchboxes.add(self) - def find_terminal_net(terminals, terminal_coords, coord): + def find_terminal_net( + terminals: list["Terminal"], + terminal_coords: list[int], + coord: int, + ) -> "Net | None": """Return the net attached to a terminal at the given coordinate. Args: @@ -1207,9 +1238,9 @@ def find_terminal_net(terminals, terminal_coords, coord): self.move_corner_nets() # Storage for detailed routing. - self.segments = defaultdict(list) + self.segments: dict[tuple[int, int], list[Segment]] = defaultdict(list) - def audit(self): + def audit(self) -> None: """Raise exception if switchbox is malformed.""" for face in self.face_list: @@ -1222,7 +1253,7 @@ def audit(self): assert len(self.left_nets) == len(self.right_nets) @property - def face_list(self): + def face_list(self) -> list[Face]: """Return list of switchbox faces in CCW order, starting from top face.""" flst = [None] * 4 flst[self.TOP] = self.top_face @@ -1231,7 +1262,7 @@ def face_list(self): flst[self.RIGHT] = self.right_face return flst - def move_corner_nets(self): + def move_corner_nets(self) -> None: """ Move any nets at the edges of the left/right faces (i.e., the corners) to the edges of the top/bottom faces. @@ -1259,7 +1290,7 @@ def move_corner_nets(self): self.top_nets[-1] = self.right_nets[-1] self.right_nets[-1] = None - def flip_xy(self): + def flip_xy(self) -> None: """Flip X-Y of switchbox to route from top-to-bottom instead of left-to-right.""" # Flip coords of tracks and columns. @@ -1281,7 +1312,7 @@ def flip_xy(self): for seg in segments: seg.flip_xy() - def coalesce(self, switchboxes): + def coalesce(self, switchboxes: list["SwitchBox"]) -> "SwitchBox" | None: """Group switchboxes around a seed switchbox into a larger switchbox. Args: @@ -1390,17 +1421,17 @@ def coalesce(self, switchboxes): # Return the coalesced switchbox created from the new faces. return SwitchBox(*total_faces) - def trim_repeated_terminals(self): + def trim_repeated_terminals(self) -> None: """Trim terminals on each face.""" for face in self.face_list: face.trim_repeated_terminals() @property - def bbox(self): + def bbox(self) -> BBox: """Return bounding box for a switchbox.""" return BBox().add(self.top_face.bbox).add(self.left_face.bbox) - def has_nets(self): + def has_nets(self) -> bool: """Return True if switchbox has any terminals on any face with nets attached.""" return ( self.top_face.has_nets() @@ -1409,7 +1440,7 @@ def has_nets(self): or self.right_face.has_nets() ) - def route(self, **options: Unpack[Options]): + def route(self, **options: Unpack[Options]) -> None: """Route wires between terminals on the switchbox faces. Args: @@ -1427,7 +1458,11 @@ def route(self, **options: Unpack[Options]): assert not self.segments.keys() return self.segments - def collect_targets(top_nets, bottom_nets, right_nets): + def collect_targets( + top_nets: list["Net"], + bottom_nets: list["Net"], + right_nets: list["Net"], + ) -> list["Target"]: """Collect target nets along top, bottom, right faces of switchbox.""" min_row = 1 @@ -1453,10 +1488,12 @@ def collect_targets(top_nets, bottom_nets, right_nets): return targets - def connect_top_btm(track_nets): + def connect_top_btm(track_nets: list["Net"]) -> list["NetInterval"]: """Connect nets from top/bottom terminals in a column to nets in horizontal tracks of the switchbox.""" - def find_connection(net, tracks, direction): + def find_connection( + net: "Net", tracks: list["Net"], direction: int + ) -> list[int]: """ Searches for the closest track with the same net followed by the closest empty track. The indices of these tracks are returned. @@ -1556,11 +1593,15 @@ def find_connection(net, tracks, direction): # Return connection segments. return column_intvls - def prune_targets(targets, current_col): + def prune_targets( + targets: list["Target"], current_col: int + ) -> list["Target"]: """Remove targets in columns to the left of the current left-to-right routing column""" return [target for target in targets if target.col > current_col] - def insert_column_nets(track_nets, column_intvls): + def insert_column_nets( + track_nets: list["Net"], column_intvls: list["NetInterval"] + ) -> list["Net"]: """Return the active nets with the added nets of the column's vertical intervals.""" nets = track_nets[:] @@ -1569,7 +1610,9 @@ def insert_column_nets(track_nets, column_intvls): nets[intvl.end] = intvl.net return nets - def net_search(net, start, track_nets): + def net_search( + net: "Net", start: int, track_nets: list["Net"] + ) -> int: """Search for the closest points for the net before and after the start point.""" # illegal offset past the end of the list of track nets. @@ -1594,7 +1637,11 @@ def net_search(net, start, track_nets): else: return -down - def insert_target_nets(track_nets, targets, right_nets): + def insert_target_nets( + track_nets: list["Net"], + targets: list["Target"], + right_nets: list["Net"], + ) -> list["Net"]: """Return copy of active track nets with additional prioritized targets from the top, bottom, right faces.""" # Allocate storage for potential target nets to be added to the list of active track nets. @@ -1645,7 +1692,9 @@ def insert_target_nets(track_nets, targets, right_nets): ) ] - def connect_splits(track_nets, column): + def connect_splits( + track_nets: list["Net"], column: list["NetInterval"] + ) -> list["NetInterval"]: """Vertically connect nets on multiple tracks.""" # Make a copy so the original isn't disturbed. @@ -1658,7 +1707,7 @@ def connect_splits(track_nets, column): multi_nets.discard(None) # Ignore empty tracks. # Find possible intervals for multi-track nets. - net_intervals = [] + net_intervals: list["NetInterval"] = [] for net in multi_nets: net_trk_idxs = [idx for idx, nt in enumerate(track_nets) if nt is net] for index, trk1 in enumerate(net_trk_idxs[:-1], 1): @@ -1716,7 +1765,11 @@ def connect_splits(track_nets, column): return column - def extend_tracks(track_nets, column, targets): + def extend_tracks( + track_nets: list["Net"], + column: list["NetInterval"], + targets: list["Target"], + ) -> list["Net"]: """Extend track nets into the next column.""" # These are nets to the right of the current column. @@ -1846,7 +1899,11 @@ def extend_tracks(track_nets, column, targets): return next_track_nets - def trim_column_intervals(column, track_nets, next_track_nets): + def trim_column_intervals( + column: list["NetInterval"], + track_nets: list["Net"], + next_track_nets: list["Net"], + ) -> None: """Trim stubs from column intervals.""" # All nets entering and exiting the column. @@ -1972,8 +2029,14 @@ def trim_column_intervals(column, track_nets, next_track_nets): return self.segments def draw( - self, scr=None, tx=None, font=None, color=(128, 0, 128), thickness=2, **options: Unpack[Options] - ): + self, + scr: Surface | None = None, + tx: Tx | None = None, + font = None, + color: tuple[int, int, int] = (128, 0, 128), + thickness: int = 2, + **options: Unpack[Options], + ) -> None: """Draw a switchbox and its routing for debugging purposes. Args: @@ -2014,7 +2077,7 @@ def draw( if options.get("draw_routing_channels"): # Draw routing channels from midpoint of one switchbox face to midpoint of another. - def draw_channel(face1, face2): + def draw_channel(face1: Face, face2: Face) -> None: seg1 = face1.seg seg2 = face2.seg p1 = (seg1.p1 + seg1.p2) / 2 diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index f1b108a7..b791acbe 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -5,7 +5,7 @@ from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector if TYPE_CHECKING: - from .route import GlobalTrack + from .route import Face, GlobalTrack def get_script_name(): @@ -84,6 +84,10 @@ class Pin: x: float y: float + # internal use + route_pt: Point + face: "Face" + def is_connected(self) -> bool: # TODO: raise NotImplementedError @@ -96,6 +100,10 @@ class Net: pins: list[Pin] parts: set[Part] + def __bool__(self) -> bool: + """TODO: does this need to be false if no parts or pins?""" + raise NotImplementedError + def __iter__(self) -> Iterator[Pin | Part]: yield from self.pins yield from self.parts From ec5bd105e0f128aacdebe423c3423031f4e00e21 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 18:40:29 +0200 Subject: [PATCH 23/85] Typing on route done too! --- .../schematic/kicad/skidl/geometry.py | 2 +- .../exporters/schematic/kicad/skidl/node.py | 8 +- .../exporters/schematic/kicad/skidl/place.py | 72 +++++----- .../exporters/schematic/kicad/skidl/route.py | 133 ++++++++++-------- .../exporters/schematic/kicad/skidl/shims.py | 2 + 5 files changed, 119 insertions(+), 98 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index f8faa62e..6ea80129 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -390,7 +390,7 @@ def __str__(self): class Segment: - def __init__(self, p1, p2): + def __init__(self, p1: Point, p2: Point): "Create a line segment between two points." self.p1 = copy(p1) self.p2 = copy(p2) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 5275b015..e1d442a9 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -6,10 +6,10 @@ from collections import defaultdict from itertools import chain -from .geometry import BBox, Point, Tx, Vector +from .geometry import BBox, Point, Segment, Tx, Vector from .place import Placer from .route import Router -from .shims import Net, Part +from .shims import Net, Part, Pin """ Node class for storing circuit hierarchy. @@ -44,7 +44,7 @@ def __init__( self.flattened = False self.tool_module = tool_module # Backend tool. self.parts: list["Part"] = [] - self.wires = defaultdict(list) + self.wires: dict[Net, list[Segment]] = defaultdict(list) self.junctions = defaultdict(list) self.tx = Tx() self.bbox = BBox() @@ -313,7 +313,7 @@ def get_internal_nets(self) -> list[Net]: return internal_nets - def get_internal_pins(self, net): + def get_internal_pins(self, net: Net) -> list[Pin]: """Return the pins on the net that are on parts in the node. Args: diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 5f0387bb..d8ac2d7e 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -23,7 +23,7 @@ draw_text, ) from .geometry import BBox, Point, Tx, Vector -from .shims import Net, Part, Pin, rmv_attr, Options +from .shims import Net, Options, Part, Pin, rmv_attr if TYPE_CHECKING: from .net_terminal import NetTerminal @@ -326,12 +326,12 @@ def find_best_orientation(part: Part) -> None: if calc_starting_cost: # Calculate the cost of the starting orientation before any changes in orientation. - starting_cost = net_tension(part, **options: Unpack[Options]) + starting_cost = net_tension(part, **options) # Skip the starting orientation but set flag to process the others. calc_starting_cost = False else: # Calculate the cost of the current orientation. - delta_cost = net_tension(part, **options: Unpack[Options]) - starting_cost + delta_cost = net_tension(part, **options) - starting_cost if delta_cost < best_delta_cost: # Save the largest decrease in cost and the associated orientation. best_delta_cost = delta_cost @@ -683,15 +683,15 @@ def scale_attractive_repulsive_forces( part.original_tx = copy(part.tx) # Find attractive forces when they are maximized by random part placement. - random_placement(parts, **options: Unpack[Options]) + random_placement(parts, **options) attractive_forces_sum = sum( - force_func(p, parts, alpha=0, scale=1, **options: Unpack[Options]).magnitude for p in parts + force_func(p, parts, alpha=0, scale=1, **options).magnitude for p in parts ) # Find repulsive forces when they are maximized by compacted part placement. - central_placement(parts, **options: Unpack[Options]) + central_placement(parts, **options) repulsive_forces_sum = sum( - force_func(p, parts, alpha=1, scale=1, **options: Unpack[Options]).magnitude for p in parts + force_func(p, parts, alpha=1, scale=1, **options).magnitude for p in parts ) # Restore original part placement. @@ -723,8 +723,8 @@ def total_part_force( Vector: Weighted total of net attractive and overlap repulsion forces. """ force = scale * (1 - alpha) * attractive_force( - part, **options: Unpack[Options] - ) + alpha * repulsive_force(part, parts, **options: Unpack[Options]) + part, **options + ) + alpha * repulsive_force(part, parts, **options) part.force = force # For debug drawing. return force @@ -773,8 +773,8 @@ def total_similarity_force( Vector: Weighted total of net attractive and overlap repulsion forces. """ force = scale * (1 - alpha) * similarity_force( - part, parts, similarity, **options: Unpack[Options] - ) + alpha * repulsive_force(part, parts, **options: Unpack[Options]) + part, parts, similarity, **options + ) + alpha * repulsive_force(part, parts, **options) part.force = force # For debug drawing. return force @@ -819,7 +819,7 @@ def random_placement(parts: list[Part], **options: Unpack[Options]): """ # Compute appropriate size to hold the parts based on their areas. - bbox = define_placement_bbox(parts, **options: Unpack[Options]) + bbox = define_placement_bbox(parts, **options) # Place parts randomly within area. for part in parts: @@ -855,7 +855,7 @@ def push_and_pull( def cost(parts: list[Part], alpha: float) -> float: """Cost function for use in debugging. Should decrease as parts move.""" for part in parts: - part.force = force_func(part, parts, scale=scale, alpha=alpha, **options: Unpack[Options]) + part.force = force_func(part, parts, scale=scale, alpha=alpha, **options) return sum((part.force.magnitude for part in parts)) # Get PyGame screen, real-to-screen coord Tx matrix, font for debug drawing. @@ -874,7 +874,7 @@ def cost(parts: list[Part], alpha: float) -> float: rmv_drift = not anchored_parts # Set scale factor between attractive net forces and repulsive part overlap forces. - scale = scale_attractive_repulsive_forces(parts, force_func, **options: Unpack[Options]) + scale = scale_attractive_repulsive_forces(parts, force_func, **options) # Setup the schedule for adjusting the alpha coefficient that weights the # combination of the attractive net forces and the repulsive part overlap forces. @@ -918,7 +918,7 @@ def cost(parts: list[Part], alpha: float) -> float: sum_of_forces = 0 for part in mobile_parts: part.force = force_func( - part, parts, scale=scale, alpha=alpha, **options: Unpack[Options] + part, parts, scale=scale, alpha=alpha, **options ) # Mask X or Y component of force during part alignment. part.force = part.force.mask(force_mask) @@ -989,7 +989,7 @@ def evolve_placement( parts = anchored_parts + mobile_parts # Force-directed placement. - push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options: Unpack[Options]) + push_and_pull(anchored_parts, mobile_parts, nets, force_func, **options) # Snap parts to grid. for part in parts: @@ -1087,7 +1087,7 @@ def evolution(net_terminals: list["NetTerminal"], placed_parts: list[Part], bbox if evolution_type == "all_at_once": evolve_placement( - placed_parts, net_terminals, nets, total_part_force, **options: Unpack[Options] + placed_parts, net_terminals, nets, total_part_force, **options ) elif evolution_type == "outer_to_inner": @@ -1126,7 +1126,7 @@ def dist_to_bbox_edge(term: "NetTerminal"): mobile_terminals[:-1], nets, force_func, - **options: Unpack[Options] + **options ) # Anchor the mobile terminals after their placement is done. anchored_parts.extend(mobile_terminals[:-1]) @@ -1137,7 +1137,7 @@ def dist_to_bbox_edge(term: "NetTerminal"): if mobile_terminals: # Evolve placement of any remaining terminals. evolve_placement( - anchored_parts, mobile_terminals, nets, total_part_force, **options: Unpack[Options] + anchored_parts, mobile_terminals, nets, total_part_force, **options ) bbox = get_enclosing_bbox(placed_parts) @@ -1200,7 +1200,7 @@ def group_parts(node: "SchNode", **options: Unpack[Options]): return connected_parts, internal_nets, floating_parts - def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], **options: Unpack[Options]): + def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], **options): """Place individual parts. Args: @@ -1215,10 +1215,10 @@ def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], * return # Add bboxes with surrounding area so parts are not butted against each other. - add_placement_bboxes(parts, **options: Unpack[Options]) + add_placement_bboxes(parts, **options) # Set anchor and pull pins that determine attractive forces between parts. - add_anchor_pull_pins(parts, nets, **options: Unpack[Options]) + add_anchor_pull_pins(parts, nets, **options) # Randomly place connected parts. random_placement(parts) @@ -1232,7 +1232,7 @@ def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], * ) if options.get("compress_before_place"): - central_placement(parts, **options: Unpack[Options]) + central_placement(parts, **options) # Do force-directed placement of the parts in the parts. @@ -1241,17 +1241,17 @@ def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], * real_parts = [part for part in parts if not is_net_terminal(part)] # Do the first trial placement. - evolve_placement([], real_parts, nets, total_part_force, **options: Unpack[Options]) + evolve_placement([], real_parts, nets, total_part_force, **options) if options.get("rotate_parts"): # Adjust part orientations after first trial placement is done. - if adjust_orientations(real_parts, **options: Unpack[Options]): + if adjust_orientations(real_parts, **options): # Some part orientations were changed, so re-do placement. - evolve_placement([], real_parts, nets, total_part_force, **options: Unpack[Options]) + evolve_placement([], real_parts, nets, total_part_force, **options) # Place NetTerminals after all the other parts. place_net_terminals( - net_terminals, real_parts, nets, total_part_force, **options: Unpack[Options] + net_terminals, real_parts, nets, total_part_force, **options ) if options.get("draw_placement"): @@ -1275,7 +1275,7 @@ def place_floating_parts(node: "SchNode", parts: list[Part], **options: Unpack[O add_placement_bboxes(parts) # Set anchor and pull pins that determine attractive forces between similar parts. - add_anchor_pull_pins(parts, [], **options: Unpack[Options]) + add_anchor_pull_pins(parts, [], **options) # Randomly place the floating parts. random_placement(parts) @@ -1310,10 +1310,10 @@ def place_floating_parts(node: "SchNode", parts: list[Part], **options: Unpack[O if options.get("compress_before_place"): # Compress all floating parts together. - central_placement(parts, **options: Unpack[Options]) + central_placement(parts, **options) # Do force-directed placement of the parts in the group. - evolve_placement([], parts, [], force_func, **options: Unpack[Options]) + evolve_placement([], parts, [], force_func, **options) if options.get("draw_placement"): # Pause to look at placement for debugging purposes. @@ -1438,7 +1438,7 @@ def place_blocks( # Arrange the part blocks with similarity force-directed placement. force_func = functools.partial(total_similarity_force, similarity=blk_attr) - evolve_placement([], part_blocks, [], force_func, **options: Unpack[Options]) + evolve_placement([], part_blocks, [], force_func, **options) if options.get("draw_placement"): # Pause to look at placement for debugging purposes. @@ -1503,22 +1503,22 @@ def place(node: "SchNode", **options: Unpack[Options]): # First, recursively place children of this node. # TODO: Child nodes are independent, so can they be processed in parallel? for child in node.children.values(): - child.place(**options: Unpack[Options]) + child.place(**options) # Group parts into those that are connected by explicit nets and # those that float freely connected only by stub nets. - connected_parts, internal_nets, floating_parts = node.group_parts(**options: Unpack[Options]) + connected_parts, internal_nets, floating_parts = node.group_parts(**options) # Place each group of connected parts. for group in connected_parts: - node.place_connected_parts(list(group), internal_nets, **options: Unpack[Options]) + node.place_connected_parts(list(group), internal_nets, **options) # Place the floating parts that have no connections to anything else. - node.place_floating_parts(list(floating_parts), **options: Unpack[Options]) + node.place_floating_parts(list(floating_parts), **options) # Now arrange all the blocks of placed parts and the child nodes within this node. node.place_blocks( - connected_parts, floating_parts, node.children.values(), **options: Unpack[Options] + connected_parts, floating_parts, node.children.values(), **options ) # Remove any stuff leftover from this place & route run. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index 8e13c1a8..d42e06c9 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -12,7 +12,7 @@ from collections import Counter, defaultdict from enum import Enum from itertools import chain, zip_longest -from typing import TYPE_CHECKING, Unpack +from typing import TYPE_CHECKING, Iterator, Unpack from .constants import DRAWING_BOX_RESIZE, GRID from .debug_draw import ( @@ -403,7 +403,7 @@ def __init__( self.terminals: list[Terminal] = [] # Set of Faces adjacent to this one. (Starts empty.) - self.adjacent: set[Face] = set() + self.adjacent: set[Adjacency] = set() # Add this new face to the track it belongs to so it isn't lost. self.track = track @@ -2099,14 +2099,14 @@ def draw_channel(face1: Face, face2: Face) -> None: class Router: """Mixin to add routing function to Node class.""" - def add_routing_points(node, nets): + def add_routing_points(node: "SchNode", nets: list[Net]): """Add routing points by extending wires from pins out to the edge of the part bounding box. Args: nets (list): List of nets to be routed. """ - def add_routing_pt(pin): + def add_routing_pt(pin: Pin) -> None: """Add the point for a pin on the boundary of a part.""" bbox = pin.part.lbl_bbox @@ -2143,7 +2143,9 @@ def add_routing_pt(pin): seg = Segment(pin.pt, pin.route_pt) * pin.part.tx node.wires[pin.net].append(seg) - def create_routing_tracks(node, routing_bbox): + def create_routing_tracks( + node: "SchNode", routing_bbox: BBox + ) -> tuple[list[GlobalTrack], list[GlobalTrack]]: """Create horizontal & vertical global routing tracks.""" # Find the coords of the horiz/vert tracks that will hold the H/V faces of the routing switchboxes. @@ -2222,7 +2224,12 @@ def bbox_to_faces(part: Part, bbox: BBox): return h_tracks, v_tracks - def create_terminals(node, internal_nets, h_tracks, v_tracks): + def create_terminals( + node: "SchNode", + internal_nets: list[Net], + h_tracks: list[GlobalTrack], + v_tracks: list[GlobalTrack], + ) -> None: """Create terminals on the faces in the routing tracks.""" # Add terminals to all non-part/non-boundary faces. @@ -2274,7 +2281,7 @@ def create_terminals(node, internal_nets, h_tracks, v_tracks): for face in track: face.set_capacity() - def global_router(node, nets): + def global_router(node: "SchNode", nets: list[Net]) -> list[GlobalRoute]: """Globally route a list of nets from face to face. Args: @@ -2303,7 +2310,9 @@ def global_router(node, nets): # d. Add the faces on the new route to the stop_faces list. # Core routing function. - def rt_srch(start_face, stop_faces): + def rt_srch( + start_face: Face, stop_faces: list[Face] + ) -> GlobalWire: """Return a minimal-distance path from the start face to one of the stop faces. Args: @@ -2373,9 +2382,7 @@ def rt_srch(start_face, stop_faces): if not closest_face: # Exception raised if couldn't find a path from start to stop faces. raise GlobalRoutingFailure( - "Global routing failure: {net.name} {net} {start_face.pins}".format( - **locals() - ) + f"Global routing failure: {net.name} {net} {start_face.pins}" ) # Add the closest adjacent face to the list of visited faces. @@ -2400,7 +2407,7 @@ def rt_srch(start_face, stop_faces): return GlobalWire(net, reversed(face_path)) # Key function for setting the order in which nets will be globally routed. - def rank_net(net): + def rank_net(net: Net) -> tuple[float, int]: """Rank net based on W/H of bounding box of pins and the # of pins.""" # Nets with a small bounding box probably have fewer routing resources @@ -2415,7 +2422,7 @@ def rank_net(net): nets.sort(key=rank_net) # Globally route each net. - global_routes = [] + global_routes: list[GlobalRoute] = [] for net in nets: # List for storing GlobalWires connecting pins on net. @@ -2448,7 +2455,12 @@ def rank_net(net): return global_routes - def create_switchboxes(node, h_tracks, v_tracks, **options: Unpack[Options]): + def create_switchboxes( + node: "SchNode", + h_tracks: list[GlobalTrack], + v_tracks: list[GlobalTrack], + **options: Unpack[Options], + ) -> list[SwitchBox]: """Create routing switchboxes from the faces in the horz/vert tracks. Args: @@ -2466,7 +2478,7 @@ def create_switchboxes(node, h_tracks, v_tracks, **options: Unpack[Options]): face.switchboxes.clear() # For each horizontal Face, create a switchbox where it is the top face of the box. - switchboxes = [] + switchboxes: list[SwitchBox] = [] for h_track in h_tracks[1:]: for face in h_track: try: @@ -2498,7 +2510,11 @@ def create_switchboxes(node, h_tracks, v_tracks, **options: Unpack[Options]): return switchboxes - def switchbox_router(node, switchboxes, **options: Unpack[Options]): + def switchbox_router( + node: "SchNode", + switchboxes: list[SwitchBox], + **options: Unpack[Options], + ): """Create detailed wiring between the terminals along the sides of each switchbox. Args: @@ -2527,28 +2543,28 @@ def switchbox_router(node, switchboxes, **options: Unpack[Options]): for net, segments in swbx.segments.items(): node.wires[net].extend(segments) - def cleanup_wires(node): + def cleanup_wires(node: "SchNode"): """Try to make wire segments look prettier.""" - def order_seg_points(segments): + def order_seg_points(segments: list[Segment]): """Order endpoints in a horizontal or vertical segment.""" for seg in segments: if seg.p2 < seg.p1: seg.p1, seg.p2 = seg.p2, seg.p1 - def segments_bbox(segments): + def segments_bbox(segments: list[Segment]): """Return bounding box containing the given list of segments.""" seg_pts = list(chain(*((s.p1, s.p2) for s in segments))) return BBox(*seg_pts) - def extract_horz_vert_segs(segments): + def extract_horz_vert_segs(segments: list[Segment]): """Separate segments and return lists of horizontal & vertical segments.""" horz_segs = [seg for seg in segments if seg.p1.y == seg.p2.y] vert_segs = [seg for seg in segments if seg.p1.x == seg.p2.x] assert len(horz_segs) + len(vert_segs) == len(segments) return horz_segs, vert_segs - def split_segments(segments, net_pin_pts): + def split_segments(segments: list[Segment], net_pin_pts: list[Point]): """Return list of net segments split into the smallest intervals without intersections with other segments.""" # Check each horizontal segment against each vertical segment and split each one if they intersect. @@ -2602,16 +2618,16 @@ def split_segments(segments, net_pin_pts): return horz_segs + vert_segs - def merge_segments(segments): + def merge_segments(segments: list[Segment]) -> list[Segment]: """Return segments after merging those that run the same direction and overlap.""" # Preprocess the segments. horz_segs, vert_segs = extract_horz_vert_segs(segments) - merged_segs = [] + merged_segs: list[Segment] = [] # Separate horizontal segments having the same Y coord. - horz_segs_v = defaultdict(list) + horz_segs_v: defaultdict[float, list[Segment]] = defaultdict(list) for seg in horz_segs: horz_segs_v[seg.p1.y].append(seg) @@ -2632,7 +2648,7 @@ def merge_segments(segments): merged_segs.append(seg) # Separate vertical segments having the same X coord. - vert_segs_h = defaultdict(list) + vert_segs_h: defaultdict[float, list[Segment]] = defaultdict(list) for seg in vert_segs: vert_segs_h[seg.p1.x].append(seg) @@ -2654,18 +2670,18 @@ def merge_segments(segments): return merged_segs - def break_cycles(segments): + def break_cycles(segments: list[Segment]) -> list[Segment]: """Remove segments to break any cycles of a net's segments.""" # Create a dict storing set of segments adjacent to each endpoint. - adj_segs = defaultdict(set) + adj_segs: defaultdict[Point, set[Segment]] = defaultdict(set) for seg in segments: # Add segment to set for each endpoint. adj_segs[seg.p1].add(seg) adj_segs[seg.p2].add(seg) # Create a dict storing the list of endpoints adjacent to each endpoint. - adj_pts = dict() + adj_pts: dict[Point, list[Point]] = {} for pt, segs in adj_segs.items(): # Store endpoints of all segments adjacent to endpoint, then remove the endpoint. adj_pts[pt] = list({p for seg in segs for p in (seg.p1, seg.p2)}) @@ -2673,7 +2689,7 @@ def break_cycles(segments): # Start at any endpoint and visit adjacent endpoints until all have been visited. # If an endpoint is seen more than once, then a cycle exists. Remove the segment forming the cycle. - visited_pts = [] # List of visited endpoints. + visited_pts: list[Point] = [] # List of visited endpoints. frontier_pts = list(adj_pts.keys())[:1] # Arbitrary starting point. while frontier_pts: # Visit a point on the frontier. @@ -2699,18 +2715,18 @@ def break_cycles(segments): return segments - def is_pin_pt(pt): + def is_pin_pt(pt: Point) -> bool: """Return True if the point is on one of the part pins.""" return pt in pin_pts - def contains_pt(seg, pt): + def contains_pt(seg: Segment, pt: Point) -> bool: """Return True if the point is contained within the horz/vert segment.""" return seg.p1.x <= pt.x <= seg.p2.x and seg.p1.y <= pt.y <= seg.p2.y - def trim_stubs(segments): + def trim_stubs(segments: list[Segment]) -> list[Segment]: """Return segments after removing stubs that have an unconnected endpoint.""" - def get_stubs(segments): + def get_stubs(segments: list[Segment]) -> set[Segment]: """Return set of stub segments.""" # For end point, the dict entry contains a list of the segments that meet there. @@ -2737,7 +2753,13 @@ def get_stubs(segments): stubs = get_stubs(trimmed_segments) return list(trimmed_segments) - def remove_jogs_old(net, segments, wires, net_bboxes, part_bboxes): + def remove_jogs_old( + net: Net, + segments: list[Segment], + wires: dict[Net, list[Segment]], + net_bboxes: dict[Net, BBox], + part_bboxes: list[BBox], + ) -> tuple[list[Segment], bool]: """Remove jogs in wiring segments. Args: @@ -2748,7 +2770,7 @@ def remove_jogs_old(net, segments, wires, net_bboxes, part_bboxes): part_bboxes (list): List of BBoxes for the placed parts. """ - def get_touching_segs(seg, ortho_segs): + def get_touching_segs(seg: Segment, ortho_segs: list[Segment]) -> list[Segment]: """Return list of orthogonal segments that touch the given segment.""" touch_segs = set() for oseg in ortho_segs: @@ -2760,7 +2782,7 @@ def get_touching_segs(seg, ortho_segs): touch_segs.add(oseg) return list(touch_segs) # Convert to list with no dups. - def get_overlap(*segs): + def get_overlap(*segs: Segment) -> tuple[float, float]: """Find extent of overlap of parallel horz/vert segments and return as (min, max) tuple.""" ov1 = float("-inf") ov2 = float("inf") @@ -2776,7 +2798,7 @@ def get_overlap(*segs): # assert ov1 <= ov2 return ov1, ov2 - def obstructed(segment): + def obstructed(segment: Segment) -> bool: """Return true if segment obstructed by parts or segments of other nets.""" # Obstructed if segment bbox intersects one of the part bboxes. @@ -2902,7 +2924,13 @@ def obstructed(segment): # Return updated segments. If no segments for this net were updated, then stop is True. return segments, stop - def remove_jogs(net, segments, wires, net_bboxes, part_bboxes): + def remove_jogs( + net: Net, + segments: list[Segment], + wires: dict[Net, list[Segment]], + net_bboxes: dict[Net, BBox], + part_bboxes: list[BBox], + ) -> tuple[list[Segment], bool]: """Remove jogs and staircases in wiring segments. Args: @@ -2913,7 +2941,7 @@ def remove_jogs(net, segments, wires, net_bboxes, part_bboxes): part_bboxes (list): List of BBoxes for the placed parts. """ - def obstructed(segment): + def obstructed(segment: Segment) -> bool: """Return true if segment obstructed by parts or segments of other nets.""" # Obstructed if segment bbox intersects one of the part bboxes. @@ -2955,7 +2983,7 @@ def obstructed(segment): # No obstructions found, so return False. return False - def get_corners(segments): + def get_corners(segments: list[Segment]) -> dict[Point, list[Segment]]: """Return dictionary of right-angle corner points and lists of associated segments.""" # For each corner point, the dict entry contains a list of the segments that meet there. @@ -2980,7 +3008,7 @@ def get_corners(segments): } return corners - def get_jogs(segments): + def get_jogs(segments: list[Segment]) -> Iterator[tuple[list[Segment], list[Point]]]: """Yield the three segments and starting and end points of a staircase or tophat jog.""" # Get dict of right-angle corners formed by segments. @@ -2991,14 +3019,14 @@ def get_jogs(segments): for segment in segments: if segment.p1 in corners and segment.p2 in corners: # Get the three segments in the jog. - jog_segs = set() + jog_segs: set[Segment] = set() jog_segs.add(corners[segment.p1][0]) jog_segs.add(corners[segment.p1][1]) jog_segs.add(corners[segment.p2][0]) jog_segs.add(corners[segment.p2][1]) # Get the points where the three-segment jog starts and stops. - start_stop_pts = set() + start_stop_pts: set[Point] = set() for seg in jog_segs: start_stop_pts.add(seg.p1) start_stop_pts.add(seg.p2) @@ -3140,10 +3168,10 @@ def get_jogs(segments): # Update the node net's wire with the cleaned version. node.wires[net] = segments - def add_junctions(node): + def add_junctions(node: SchNode): """Add X & T-junctions where wire segments in the same net meet.""" - def find_junctions(route): + def find_junctions(route: list[Segment]) -> list[Point]: """Find junctions where segments of a net intersect. Args: @@ -3188,14 +3216,14 @@ def find_junctions(route): junctions = find_junctions(segments) node.junctions[net].extend(junctions) - def rmv_routing_stuff(node): + def rmv_routing_stuff(node: SchNode): """Remove attributes added to parts/pins during routing.""" rmv_attr(node.parts, ("left_track", "right_track", "top_track", "bottom_track")) for part in node.parts: rmv_attr(part.pins, ("route_pt", "face")) - def route(node, tool=None, **options: Unpack[Options]): + def route(node: SchNode, **options: Unpack[Options]): """Route the wires between part pins in this node and its children. Steps: @@ -3210,15 +3238,6 @@ def route(node, tool=None, **options: Unpack[Options]): "allow_routing_failure", "draw", "draw_all_terminals", "show_capacities", "draw_switchbox", "draw_routing", "draw_channels" """ - - # Inject the constants for the backend tool into this module. - import skidl - from skidl.tools import tool_modules - - tool = tool or skidl.config.tool - this_module = sys.modules[__name__] - this_module.__dict__.update(tool_modules[tool].constants.__dict__) - random.seed(options.get("seed")) # Remove any stuff leftover from a previous place & route run. @@ -3227,7 +3246,7 @@ def route(node, tool=None, **options: Unpack[Options]): # First, recursively route any children of this node. # TODO: Child nodes are independent so could they be processed in parallel? for child in node.children.values(): - child.route(tool=tool, **options) + child.route(**options) # Exit if no parts to route in this node. if not node.parts: diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index b791acbe..4fd788dd 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -68,6 +68,7 @@ class Pin: # We need to assign these num: str name: str + net: "Net" # TODO: where are these expected to be assigned? part: Part # TODO: @@ -78,6 +79,7 @@ class Pin: route_pt: Point # TODO: place_pt: Point # TODO: orientation: str # TODO: + bbox: BBox # TODO: # Assigned in NetTerminal, but it's unclear # whether this is typically something that comes from the user From 08c6c888cd0d92bc4e0c2c5421b0672d654016de Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 18:42:56 +0200 Subject: [PATCH 24/85] Type geometry --- .../schematic/kicad/skidl/geometry.py | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index 6ea80129..0908ca93 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -29,7 +29,7 @@ def to_mms(mils): class Tx: - def __init__(self, a=1, b=0, c=0, d=1, dx=0, dy=0): + def __init__(self, a: float = 1, b: float = 0, c: float = 0, d: float = 1, dx: float = 0, dy: float = 0): """Create a transformation matrix. tx = [ a b 0 @@ -47,7 +47,7 @@ def __init__(self, a=1, b=0, c=0, d=1, dx=0, dy=0): self.dy = dy @classmethod - def from_symtx(cls, symtx): + def from_symtx(cls, symtx: str) -> "Tx": """Return a Tx() object that implements the "HVLR" geometric operation sequence. Args: @@ -68,17 +68,17 @@ def from_symtx(cls, symtx): tx *= op_dict[op] return tx - def __repr__(self): + def __repr__(self) -> str: return "{self.__class__}({self.a}, {self.b}, {self.c}, {self.d}, {self.dx}, {self.dy})".format( self=self ) - def __str__(self): + def __str__(self) -> str: return "[{self.a}, {self.b}, {self.c}, {self.d}, {self.dx}, {self.dy}]".format( self=self ) - def __mul__(self, m): + def __mul__(self, m: "Tx | float") -> "Tx": """Return the product of two transformation matrices.""" if isinstance(m, Tx): tx = m @@ -95,7 +95,7 @@ def __mul__(self, m): ) @property - def origin(self): + def origin(self) -> Point: """Return the (dx, dy) translation as a Point.""" return Point(self.dx, self.dy) @@ -106,32 +106,32 @@ def origin(self): # self.dx, self.dy = pt.x, pt.y @property - def scale(self): + def scale(self) -> float: """Return the scaling factor.""" return (Point(1, 0) * self - Point(0, 0) * self).magnitude - def move(self, vec): + def move(self, vec: Point) -> "Tx": """Return Tx with movement vector applied.""" return self * Tx(dx=vec.x, dy=vec.y) - def rot_90cw(self): + def rot_90cw(self) -> "Tx": """Return Tx with 90-deg clock-wise rotation around (0, 0).""" return self * Tx(a=0, b=1, c=-1, d=0) - def rot(self, degs): + def rot(self, degs: float) -> "Tx": """Return Tx rotated by the given angle (in degrees).""" rads = radians(degs) return self * Tx(a=cos(rads), b=sin(rads), c=-sin(rads), d=cos(rads)) - def flip_x(self): + def flip_x(self) -> "Tx": """Return Tx with X coords flipped around (0, 0).""" return self * Tx(a=-1) - def flip_y(self): + def flip_y(self) -> "Tx": """Return Tx with Y coords flipped around (0, 0).""" return self * Tx(d=-1) - def no_translate(self): + def no_translate(self) -> "Tx": """Return Tx with translation set to (0,0).""" return Tx(a=self.a, b=self.b, c=self.c, d=self.d) @@ -148,40 +148,40 @@ def no_translate(self): class Point: - def __init__(self, x, y): + def __init__(self, x: float, y: float): """Create a Point with coords x,y.""" self.x = x self.y = y - def __hash__(self): + def __hash__(self) -> int: """Return hash of X,Y tuple.""" return hash((self.x, self.y)) - def __eq__(self, other): + def __eq__(self, other: "Point") -> bool: """Return true if (x,y) tuples of self and other are the same.""" return (self.x, self.y) == (other.x, other.y) - def __lt__(self, other): + def __lt__(self, other: "Point") -> bool: """Return true if (x,y) tuple of self compares as less than (x,y) tuple of other.""" return (self.x, self.y) < (other.x, other.y) - def __ne__(self, other): + def __ne__(self, other: "Point") -> bool: """Return true if (x,y) tuples of self and other differ.""" return not (self == other) - def __add__(self, pt): + def __add__(self, pt: "Point | float") -> "Point": """Add the x,y coords of pt to self and return the resulting Point.""" if not isinstance(pt, Point): pt = Point(pt, pt) return Point(self.x + pt.x, self.y + pt.y) - def __sub__(self, pt): + def __sub__(self, pt: "Point | float") -> "Point": """Subtract the x,y coords of pt from self and return the resulting Point.""" if not isinstance(pt, Point): pt = Point(pt, pt) return Point(self.x - pt.x, self.y - pt.y) - def __mul__(self, m): + def __mul__(self, m: "Tx | Point") -> "Point": """Apply transformation matrix or scale factor to a point and return a point.""" if isinstance(m, Tx): return Point( @@ -192,72 +192,72 @@ def __mul__(self, m): else: return Point(m * self.x, m * self.y) - def __rmul__(self, m): + def __rmul__(self, m: "Tx | Point") -> "Point": if isinstance(m, Tx): raise ValueError else: return self * m - def xprod(self, pt): + def xprod(self, pt: "Point") -> float: """Cross-product of two 2D vectors returns scalar in Z coord.""" return self.x * pt.y - self.y * pt.x - def mask(self, msk): + def mask(self, msk: list[float]) -> "Point": """Multiply the X & Y coords by the elements of msk.""" return Point(self.x * msk[0], self.y * msk[1]) - def __neg__(self): + def __neg__(self) -> "Point": """Negate both coords.""" return Point(-self.x, -self.y) - def __truediv__(self, d): + def __truediv__(self, d: float) -> "Point": """Divide the x,y coords by d.""" return Point(self.x / d, self.y / d) - def __div__(self, d): + def __div__(self, d: float) -> "Point": """Divide the x,y coords by d.""" return Point(self.x / d, self.y / d) - def round(self): + def round(self) -> "Point": return Point(int(round(self.x)), int(round(self.y))) - def __str__(self): + def __str__(self) -> str: return "{} {}".format(self.x, self.y) - def snap(self, grid_spacing): + def snap(self, grid_spacing: float) -> "Point": """Snap point x,y coords to the given grid spacing.""" snap_func = lambda x: int(grid_spacing * round(x / grid_spacing)) return Point(snap_func(self.x), snap_func(self.y)) - def min(self, pt): + def min(self, pt: "Point") -> "Point": """Return a Point with coords that are the min x,y of both points.""" return Point(min(self.x, pt.x), min(self.y, pt.y)) - def max(self, pt): + def max(self, pt: "Point") -> "Point": """Return a Point with coords that are the max x,y of both points.""" return Point(max(self.x, pt.x), max(self.y, pt.y)) @property - def magnitude(self): + def magnitude(self) -> float: """Get the distance of the point from the origin.""" return sqrt(self.x**2 + self.y**2) @property - def norm(self): + def norm(self) -> "Point": """Return a unit vector pointing from the origin to the point.""" try: return self / self.magnitude except ZeroDivisionError: return Point(0, 0) - def flip_xy(self): + def flip_xy(self) -> None: """Flip X-Y coordinates of point.""" self.x, self.y = self.y, self.x - def __repr__(self): + def __repr__(self) -> str: return "{self.__class__}({self.x}, {self.y})".format(self=self) - def __str__(self): + def __str__(self) -> str: return "({}, {})".format(self.x, self.y) @@ -265,14 +265,14 @@ def __str__(self): class BBox: - def __init__(self, *pts): + def __init__(self, *pts: Point): """Create a bounding box surrounding the given points.""" inf = float("inf") self.min = Point(inf, inf) self.max = Point(-inf, -inf) self.add(*pts) - def __add__(self, obj): + def __add__(self, obj: "Point | BBox") -> "BBox": """Return the merged BBox of two BBoxes or a BBox and a Point.""" sum_ = BBox() if isinstance(obj, Point): @@ -285,30 +285,30 @@ def __add__(self, obj): raise NotImplementedError return sum_ - def __iadd__(self, obj): + def __iadd__(self, obj: "Point | BBox") -> "BBox": """Update BBox bt adding another Point or BBox""" sum_ = self + obj self.min = sum_.min self.max = sum_.max return self - def add(self, *objs): + def add(self, *objs: "Point | BBox") -> "BBox": """Update the bounding box size by adding Point/BBox objects.""" for obj in objs: self += obj return self - def __mul__(self, m): + def __mul__(self, m: "Tx") -> "BBox": return BBox(self.min * m, self.max * m) - def round(self): + def round(self) -> "BBox": return BBox(self.min.round(), self.max.round()) - def is_inside(self, pt): + def is_inside(self, pt: "Point") -> bool: """Return True if point is inside bounding box.""" return (self.min.x <= pt.x <= self.max.x) and (self.min.y <= pt.y <= self.max.y) - def intersects(self, bbox): + def intersects(self, bbox: "BBox") -> bool: """Return True if the two bounding boxes intersect.""" return ( (self.min.x < bbox.max.x) @@ -317,7 +317,7 @@ def intersects(self, bbox): and (self.max.y > bbox.min.y) ) - def intersection(self, bbox): + def intersection(self, bbox: "BBox") -> "BBox | None": """Return the bounding box of the intersection between the two bounding boxes.""" if not self.intersects(bbox): return None @@ -325,11 +325,11 @@ def intersection(self, bbox): corner2 = self.max.min(bbox.max) return BBox(corner1, corner2) - def resize(self, vector): + def resize(self, vector: "Point") -> "BBox": """Expand/contract the bounding box by applying vector to its corner points.""" return BBox(self.min - vector, self.max + vector) - def snap_resize(self, grid_spacing): + def snap_resize(self, grid_spacing: float) -> "BBox": """Resize bbox so max and min points are on grid. Args: @@ -341,42 +341,42 @@ def snap_resize(self, grid_spacing): return bbox @property - def area(self): + def area(self) -> float: """Return area of bounding box.""" return self.w * self.h @property - def w(self): + def w(self) -> float: """Return the bounding box width.""" return abs(self.max.x - self.min.x) @property - def h(self): + def h(self) -> float: """Return the bounding box height.""" return abs(self.max.y - self.min.y) @property - def ctr(self): + def ctr(self) -> Point: """Return center point of bounding box.""" return (self.max + self.min) / 2 @property - def ll(self): + def ll(self) -> Point: """Return lower-left point of bounding box.""" return Point(self.min.x, self.min.y) @property - def lr(self): + def lr(self) -> Point: """Return lower-right point of bounding box.""" return Point(self.max.x, self.min.y) @property - def ul(self): + def ul(self) -> Point: """Return upper-left point of bounding box.""" return Point(self.min.x, self.max.y) @property - def ur(self): + def ur(self) -> Point: """Return upper-right point of bounding box.""" return Point(self.max.x, self.max.y) @@ -395,22 +395,22 @@ def __init__(self, p1: Point, p2: Point): self.p1 = copy(p1) self.p2 = copy(p2) - def __mul__(self, m): + def __mul__(self, m: "Tx") -> "Segment": """Apply transformation matrix to a segment and return a segment.""" return Segment(self.p1 * m, self.p2 * m) - def round(self): + def round(self) -> "Segment": return Segment(self.p1.round(), self.p2.round()) def __str__(self): return "{} {}".format(str(self.p1), str(self.p2)) - def flip_xy(self): + def flip_xy(self) -> None: """Flip the X-Y coordinates of the segment.""" self.p1.flip_xy() self.p2.flip_xy() - def intersects(self, other): + def intersects(self, other: "Segment") -> bool: """Return true if the segments intersect.""" # FIXME: This fails if the segments are parallel! @@ -441,7 +441,7 @@ def intersects(self, other): return (0 <= t1 <= 1) and (0 <= t2 <= 1) - def shadows(self, other): + def shadows(self, other: "Segment") -> bool: """Return true if two segments overlap each other even if they aren't on the same horiz or vertical track.""" if self.p1.x == self.p2.x and other.p1.x == other.p2.x: From 8e07468d7463297b687b33b337dcd1bf6eeaa0b1 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 18:54:17 +0200 Subject: [PATCH 25/85] NamedTuples -> dataclasses and largely type bbox. I reckon we might replace that one though so not gonna bother so much --- .../exporters/schematic/kicad/skidl/bboxes.py | 44 +++--- .../schematic/kicad/skidl/draw_objs.py | 134 +++++++++++++++--- .../exporters/schematic/kicad/skidl/shims.py | 6 + 3 files changed, 143 insertions(+), 41 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py index 3167e6bf..1d027a57 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py @@ -7,7 +7,8 @@ """ import logging -from collections import namedtuple +from dataclasses import dataclass +from typing import Literal, Unpack from .constants import HIER_TERM_SIZE, PIN_LABEL_FONT_SIZE from .draw_objs import ( @@ -25,17 +26,37 @@ BBox, Point, Tx, - Vector, tx_rot_0, tx_rot_90, tx_rot_180, tx_rot_270, ) +from .shims import Options, Part logger = logging.getLogger(__name__) - -def calc_symbol_bbox(part, **options): +@dataclass +class PinDir: + """Stores information about pins in each of four directions""" + # direction: The direction the pin line is drawn from start to end. + direction: Point + # side: The side of the symbol the pin is on. (Opposite of the direction.) + side: Literal["bottom", "top", "right", "left"] + # angle: The angle of the name/number text for the pin (usually 0, -90.). + angle: int + # num_justify: Text justification of the pin number. + num_justify: Literal["end", "start"] + # name_justify: Text justification of the pin name. + name_justify: Literal["start", "end"] + # num_offset: (x,y) offset of the pin number w.r.t. the end of the pin. + num_offset: Point + # name_offset: (x,y) offset of the pin name w.r.t. the end of the pin. + name_offset: Point + # fuck knows what this is + net_offset: Point + + +def calc_symbol_bbox(part: Part, **options: Unpack[Options]): """ Return the bounding box of the part symbol. @@ -51,7 +72,7 @@ def calc_symbol_bbox(part, **options): # Named tuples for part KiCad V5 DRAW primitives. - def make_pin_dir_tbl(abs_xoff=20): + def make_pin_dir_tbl(abs_xoff: int = 20) -> dict[str, "PinDir"]: # abs_xoff is the absolute distance of name/num from the end of the pin. rel_yoff_num = -0.15 # Relative distance of number above pin line. @@ -59,19 +80,6 @@ def make_pin_dir_tbl(abs_xoff=20): 0.2 # Relative distance that places name midline even with pin line. ) - # Tuple for storing information about pins in each of four directions: - # direction: The direction the pin line is drawn from start to end. - # side: The side of the symbol the pin is on. (Opposite of the direction.) - # angle: The angle of the name/number text for the pin (usually 0, -90.). - # num_justify: Text justification of the pin number. - # name_justify: Text justification of the pin name. - # num_offset: (x,y) offset of the pin number w.r.t. the end of the pin. - # name_offset: (x,y) offset of the pin name w.r.t. the end of the pin. - PinDir = namedtuple( - "PinDir", - "direction side angle num_justify name_justify num_offset name_offset net_offset", - ) - return { "U": PinDir( Point(0, 1), diff --git a/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py b/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py index 34aac990..4d11b3e0 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py @@ -5,36 +5,124 @@ """ KiCad 5 drawing objects. """ +from dataclasses import dataclass -from collections import namedtuple -DrawDef = namedtuple( - "DrawDef", - "name ref zero name_offset show_nums show_names num_units lock_units power_symbol", -) +@dataclass +class DrawDef: + name: str + ref: str + zero: str + name_offset: int + show_nums: bool + show_names: bool + num_units: int + lock_units: bool + power_symbol: bool -DrawF0 = namedtuple("DrawF0", "ref x y size orientation visibility halign valign") -DrawF1 = namedtuple( - "DrawF1", "name x y size orientation visibility halign valign fieldname" -) +@dataclass +class DrawF0: + ref: str + x: float + y: float + size: float + orientation: str + visibility: str + halign: str + valign: str -DrawArc = namedtuple( - "DrawArc", - "cx cy radius start_angle end_angle unit dmg thickness fill startx starty endx endy", -) -DrawCircle = namedtuple("DrawCircle", "cx cy radius unit dmg thickness fill") +@dataclass +class DrawF1: + name: str + x: float + y: float + size: float + orientation: str + visibility: str + halign: str + valign: str + fieldname: str -DrawPoly = namedtuple("DrawPoly", "point_count unit dmg thickness points fill") -DrawRect = namedtuple("DrawRect", "x1 y1 x2 y2 unit dmg thickness fill") +@dataclass +class DrawArc: + cx: float + cy: float + radius: float + start_angle: float + end_angle: float + unit: int + dmg: int + thickness: float + fill: str + startx: float + starty: float + endx: float + endy: float -DrawText = namedtuple( - "DrawText", "angle x y size hidden unit dmg text italic bold halign valign" -) -DrawPin = namedtuple( - "DrawPin", - "name num x y length orientation num_size name_size unit dmg electrical_type shape", -) +@dataclass +class DrawCircle: + cx: float + cy: float + radius: float + unit: int + dmg: int + thickness: float + fill: str + + +@dataclass +class DrawPoly: + point_count: int + unit: int + dmg: int + thickness: float + points: list + fill: str + + +@dataclass +class DrawRect: + x1: float + y1: float + x2: float + y2: float + unit: int + dmg: int + thickness: float + fill: str + + +@dataclass +class DrawText: + angle: float + x: float + y: float + size: float + hidden: bool + unit: int + dmg: int + text: str + italic: bool + bold: bool + halign: str + valign: str + + +@dataclass +class DrawPin: + name: str + num: str + x: float + y: float + length: float + orientation: str + num_size: float + name_size: float + unit: int + dmg: int + electrical_type: str + shape: str diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 4fd788dd..a2012434 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -34,6 +34,7 @@ def rmv_attr(objs, attrs): class Part: # We need to assign these + ref: str # TODO: where are these expected to be assigned? pins: list["Pin"] # TODO: source @@ -63,6 +64,11 @@ def __iter__(self) -> Iterator["Pin"]: # TODO: raise NotImplementedError + @property + def draw(self): + # TODO: + raise NotImplementedError + class Pin: # We need to assign these From bd5d5667ee69c8b566e440185f24652b41dfb263 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 19:15:01 +0200 Subject: [PATCH 26/85] Add circuit and types to node.py --- .../schematic/kicad/skidl/net_terminal.py | 2 +- .../exporters/schematic/kicad/skidl/node.py | 47 ++++++++----------- .../exporters/schematic/kicad/skidl/shims.py | 15 +++++- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py index 9fb16ad0..3a4b240e 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py @@ -14,7 +14,7 @@ class NetTerminal(Part): pull_pins: dict[Net, list[Pin]] - def __init__(self, net: Net, tool_module): + def __init__(self, net: Net): """Specialized Part with a single pin attached to a net. This is intended for attaching to nets to label them, typically when diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index e1d442a9..86a2016e 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -5,16 +5,19 @@ import re from collections import defaultdict from itertools import chain +from typing import Unpack from .geometry import BBox, Point, Segment, Tx, Vector from .place import Placer from .route import Router -from .shims import Net, Part, Pin +from .shims import Circuit, Net, Options, Part, Pin """ Node class for storing circuit hierarchy. """ +HIER_SEP = "." + class SchNode(Placer, Router): """Data structure for holding information about a node in the circuit hierarchy.""" @@ -25,7 +28,6 @@ class SchNode(Placer, Router): def __init__( self, circuit=None, - tool_module=None, filepath=".", top_name="", title="", @@ -33,7 +35,7 @@ def __init__( ): self.parent = None self.children = defaultdict( - lambda: SchNode(None, tool_module, filepath, top_name, title, flatness) + lambda: SchNode(None, filepath, top_name, title, flatness) ) self.filepath = filepath self.top_name = top_name @@ -42,7 +44,6 @@ def __init__( self.title = title self.flatness = flatness self.flattened = False - self.tool_module = tool_module # Backend tool. self.parts: list["Part"] = [] self.wires: dict[Net, list[Segment]] = defaultdict(list) self.junctions = defaultdict(list) @@ -52,7 +53,7 @@ def __init__( if circuit: self.add_circuit(circuit) - def find_node_with_part(self, part): + def find_node_with_part(self, part: Part): """Find the node that contains the part based on its hierarchy. Args: @@ -61,9 +62,6 @@ def find_node_with_part(self, part): Returns: Node: The Node object containing the part. """ - - from skidl.circuit import HIER_SEP - level_names = part.hierarchy.split(HIER_SEP) node = self for lvl_nm in level_names[1:]: @@ -71,7 +69,8 @@ def find_node_with_part(self, part): assert part in node.parts return node - def add_circuit(self, circuit): + # TODO: what's a circuit to us? + def add_circuit(self, circuit: Circuit): """Add parts in circuit to node and its children. Args: @@ -127,16 +126,13 @@ def add_circuit(self, circuit): # Flatten the hierarchy as specified by the flatness parameter. self.flatten(self.flatness) - def add_part(self, part, level=0): + def add_part(self, part: Part, level: int = 0): """Add a part to the node at the appropriate level of the hierarchy. Args: part (Part): Part to be added to this node or one of its children. level (int, optional): The current level (depth) of the node in the hierarchy. Defaults to 0. """ - - from skidl.circuit import HIER_SEP - # Get list of names of hierarchical levels (in order) leading to this part. level_names = part.hierarchy.split(HIER_SEP) @@ -172,21 +168,18 @@ def add_part(self, part, level=0): # Add part to the child node (or one of its children). child_node.add_part(part, level + 1) - def add_terminal(self, net): + def add_terminal(self, net: Net): """Add a terminal for this net to the node. Args: net (Net): The net to be added to this node. """ - - from skidl.circuit import HIER_SEP - from .net_terminal import NetTerminal - nt = NetTerminal(net, self.tool_module) + nt = NetTerminal(net) self.parts.append(nt) - def external_bbox(self): + def external_bbox(self) -> BBox: """Return the bounding box of a hierarchical sheet as seen by its parent node.""" bbox = BBox(Point(0, 0), Point(500, 500)) bbox.add(Point(len("File: " + self.sheet_filename) * self.filename_sz, 0)) @@ -197,7 +190,7 @@ def external_bbox(self): return bbox - def internal_bbox(self): + def internal_bbox(self) -> BBox: """Return the bounding box for the circuitry contained within this node.""" # The bounding box is determined by the arrangement of the node's parts and child nodes. @@ -211,7 +204,7 @@ def internal_bbox(self): return bbox - def calc_bbox(self): + def calc_bbox(self) -> BBox: """Compute the bounding box for the node in the circuit hierarchy.""" if self.flattened: @@ -222,7 +215,7 @@ def calc_bbox(self): return self.bbox - def flatten(self, flatness=0.0): + def flatten(self, flatness: float = 0.0) -> None: """Flatten node hierarchy according to flatness parameter. Args: @@ -251,17 +244,17 @@ def flatten(self, flatness=0.0): slack = child_complexity * flatness # Group the children according to what types of modules they are by removing trailing instance ids. - child_types = defaultdict(list) + child_types: dict[str, list[SchNode]] = defaultdict(list) for child_id, child in self.children.items(): child_types[re.sub(r"\d+$", "", child_id)].append(child) # Compute the total size of each type of children. - child_type_sizes = dict() + child_type_sizes: dict[str, int] = {} for child_type, children in child_types.items(): child_type_sizes[child_type] = sum((child.complexity for child in children)) # Sort the groups from smallest total size to largest. - sorted_child_type_sizes = sorted( + sorted_child_type_sizes: list[tuple[str, int]] = sorted( child_type_sizes.items(), key=lambda item: item[1] ) @@ -329,10 +322,10 @@ def get_internal_pins(self, net: Net) -> list[Pin]: return [pin for pin in net.pins if pin.stub is False and pin.part in self.parts] - def collect_stats(self, **options): + def collect_stats(self, **options: Unpack[Options]) -> str: """Return comma-separated string with place & route statistics of a schematic.""" - def get_wire_length(node): + def get_wire_length(node: SchNode) -> int: """Return the sum of the wire segment lengths between parts in a routed node.""" wire_length = 0 diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index a2012434..2aa7d413 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -35,6 +35,8 @@ def rmv_attr(objs, attrs): class Part: # We need to assign these ref: str + hierarchy: str # dot-separated string of part names + unit: dict[str, "Part"] # units within the part, empty is this is all it is # TODO: where are these expected to be assigned? pins: list["Pin"] # TODO: source @@ -75,12 +77,12 @@ class Pin: num: str name: str net: "Net" + stub: bool # whether to stub the pin or not # TODO: where are these expected to be assigned? part: Part # TODO: place_pt: Point # TODO: pt: Point # TODO: - stub: bool # whether to stub the pin or not orientation: str # TODO: route_pt: Point # TODO: place_pt: Point # TODO: @@ -107,6 +109,7 @@ class Net: netio: str # whether input or output pins: list[Pin] parts: set[Part] + stub: bool # whether to stub the pin or not def __bool__(self) -> bool: """TODO: does this need to be false if no parts or pins?""" @@ -116,6 +119,16 @@ def __iter__(self) -> Iterator[Pin | Part]: yield from self.pins yield from self.parts + def is_implicit(self) -> bool: + # TODO: + # Hint; The net has a user-assigned name, so add a terminal to it below. + raise NotImplementedError + + +class Circuit: + parts: list[Part] + nets: list[Net] + class Options(TypedDict): allow_routing_failure: bool From c7b39b70add9c8ee419878f170749665f9cecb2b Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 24 Sep 2024 19:22:56 +0200 Subject: [PATCH 27/85] Add noqa for line-length --- .../schematic/kicad/skidl/debug_draw.py | 12 ++++++++---- .../schematic/kicad/skidl/gen_schematic.py | 16 +++++++--------- .../exporters/schematic/kicad/skidl/geometry.py | 1 + .../exporters/schematic/kicad/skidl/node.py | 1 + .../exporters/schematic/kicad/skidl/route.py | 3 ++- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index 9f3f1656..8ea3255b 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 imported from another project # -*- coding: utf-8 -*- # The MIT License (MIT) - Copyright (c) Dave Vandenbout. @@ -8,8 +9,6 @@ from collections import defaultdict from random import randint -import pygame - from .geometry import BBox, Point, Segment, Tx, Vector # Dictionary for storing colors to visually distinguish routed nets. @@ -27,6 +26,7 @@ def draw_box(bbox, scr, tx, color=(192, 255, 192), thickness=0): Returns: None. """ + import pygame bbox = bbox * tx corners = ( @@ -47,6 +47,7 @@ def draw_endpoint(pt, scr, tx, color=(100, 100, 100), dot_radius=10): color (tuple, optional): Segment color. Defaults to (192, 255, 192). dot_Radius (int, optional): Endpoint dot radius. Defaults to 3. """ + import pygame pt = pt * tx # Convert to drawing coords. @@ -75,6 +76,7 @@ def draw_seg(seg, scr, tx, color=(100, 100, 100), thickness=5, dot_radius=10): seg_thickness (int, optional): Segment line thickness. Defaults to 5. dot_Radius (int, optional): Endpoint dot radius. Defaults to 3. """ + import pygame # Use net color if object has a net. Otherwise use input color. try: @@ -106,7 +108,6 @@ def draw_text(txt, pt, scr, tx, font, color=(100, 100, 100), real=True): color (tuple, optional): Segment color. Defaults to (100,100,100). real (Boolean): If true, transform real pt to screen coords. Otherwise, pt is screen coords. """ - # Transform real text starting point to screen coords. if real: pt = pt * tx @@ -254,6 +255,7 @@ def draw_start(bbox): tx: Matrix to transform from real coords to screen coords. font: PyGame font for rendering text. """ + import pygame # Screen drawing area. scr_bbox = BBox(Point(0, 0), Point(2000, 1500)) @@ -294,10 +296,12 @@ def draw_start(bbox): def draw_redraw(): """Redraw the PyGame display.""" + import pygame pygame.display.flip() def draw_pause(): """Pause drawing and then resume after button press.""" + import pygame # Display drawing. draw_redraw() @@ -311,6 +315,6 @@ def draw_pause(): def draw_end(): """Display drawing and wait for user to close PyGame window.""" - + import pygame draw_pause() pygame.quit() diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index b78dfc84..875c4121 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 imported from another project """ Functions for generating a KiCad EESCHEMA schematic. """ @@ -20,7 +21,7 @@ from .shims import get_script_name, rmv_attr -def bbox_to_eeschema(bbox, tx, name=None): +def bbox_to_eeschema(bbox: BBox, tx: Tx, name=None): """Create a bounding box using EESCHEMA graphic lines.""" # Make sure the box corners are integers. @@ -289,7 +290,7 @@ def power_part_to_eeschema(part, tx=Tx()): A_sizes = OrderedDict(A_sizes_list) -def get_A_size(bbox): +def get_A_size(bbox: BBox) -> str: """Return the A-size page needed to fit the given bounding box.""" width = bbox.w @@ -339,7 +340,6 @@ def calc_pin_dir(pin): }[pin_vector] -@export_to_all def pin_label_to_eeschema(pin, tx): """Create EESCHEMA text of net label attached to a pin.""" @@ -692,11 +692,9 @@ def gen_schematic( options (dict, optional): Dict of options and values, usually for drawing/debugging. """ - from skidl import KICAD - from skidl.schematics.node import Node - from skidl.schematics.place import PlacementFailure - from skidl.schematics.route import RoutingFailure - from skidl.tools import tool_modules + from .node import SchNode + from .place import PlacementFailure + from .route import RoutingFailure # Part placement options that should always be turned on. options["use_push_pull"] = True @@ -711,7 +709,7 @@ def gen_schematic( for _ in range(retries): preprocess_circuit(circuit, **options) - node = Node(circuit, tool_modules[KICAD], filepath, top_name, title, flatness) + node = SchNode(circuit, filepath, top_name, title, flatness) try: # Place parts. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index 0908ca93..902d5756 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 imported from another project # -*- coding: utf-8 -*- # The MIT License (MIT) - Copyright (c) Dave Vandenbout. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 86a2016e..9a47bd81 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 imported from another project # -*- coding: utf-8 -*- # The MIT License (MIT) - Copyright (c) Dave Vandenbout. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index d42e06c9..a26daf02 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 imported from another project # -*- coding: utf-8 -*- # The MIT License (MIT) - Copyright (c) Dave Vandenbout. @@ -8,7 +9,6 @@ import copy import random -import sys from collections import Counter, defaultdict from enum import Enum from itertools import chain, zip_longest @@ -28,6 +28,7 @@ if TYPE_CHECKING: from pygame import Surface # skidl calls this Screen + from .node import SchNode ################################################################### From d93285472fb8f3e18da12165eceb1010d00927d3 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 25 Sep 2024 09:43:54 +0200 Subject: [PATCH 28/85] Add typing to gen_schematic and pre-empt modifying functions to pull Faebryk info --- .../schematic/kicad/skidl/gen_schematic.py | 42 +++++++++---------- .../exporters/schematic/kicad/skidl/shims.py | 31 +++++++++++++- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index 875c4121..965e6f1f 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -13,12 +13,14 @@ import re import time from collections import Counter, OrderedDict +from typing import Unpack from .bboxes import calc_hier_label_bbox, calc_symbol_bbox from .constants import BLK_INT_PAD, BOX_LABEL_FONT_SIZE, GRID, PIN_LABEL_FONT_SIZE from .geometry import BBox, Point, Tx, Vector from .net_terminal import NetTerminal -from .shims import get_script_name, rmv_attr +from .node import HIER_SEP, SchNode +from .shims import Circuit, Options, Part, PartUnit, get_script_name, rmv_attr def bbox_to_eeschema(bbox: BBox, tx: Tx, name=None): @@ -421,7 +423,7 @@ def create_eeschema_file( ) -def node_to_eeschema(node, sheet_tx=Tx()): +def node_to_eeschema(node: SchNode, sheet_tx: Tx = Tx()) -> str: """Convert node circuitry to an EESCHEMA sheet. Args: @@ -430,9 +432,6 @@ def node_to_eeschema(node, sheet_tx=Tx()): Returns: str: EESCHEMA text for the node circuitry. """ - - from skidl import HIER_SEP - # List to hold all the EESCHEMA code for this node. eeschema_code = [] @@ -519,16 +518,16 @@ def node_to_eeschema(node, sheet_tx=Tx()): # TODO: Handle symio attribute. -def preprocess_circuit(circuit, **options): +def preprocess_circuit(circuit: Circuit, **options: Unpack[Options]): """Add stuff to parts & nets for doing placement and routing of schematics.""" - def units(part): + def units(part: Part) -> list[PartUnit | Part]: if len(part.unit) == 0: return [part] else: return part.unit.values() - def initialize(part): + def initialize(part: Part): """Initialize part or its part units.""" # Initialize the units of the part, or the part itself if it has no units. @@ -554,7 +553,7 @@ def initialize(part): pin.pt = Point(pin.x, pin.y) pin.routed = False - def rotate_power_pins(part): + def rotate_power_pins(part: Part): """Rotate a part based on the direction of its power pins. This function is to make sure that voltage sources face up and gnd pins @@ -612,7 +611,7 @@ def is_gnd(net): for _ in range(int(round(rotation / 90))): part_unit.tx = part_unit.tx * tx_cw_90 - def calc_part_bbox(part): + def calc_part_bbox(part: Part): """Calculate the labeled bounding boxes and store it in the part.""" # Find part/unit bounding boxes excluding any net labels on pins. @@ -621,6 +620,9 @@ def calc_part_bbox(part): bare_bboxes = calc_symbol_bbox(part)[1:] for part_unit, bare_bbox in zip(units(part), bare_bboxes): + assert isinstance(part_unit, (PartUnit, Part)) + assert isinstance(bare_bbox, BBox) + # Expand the bounding box if it's too small in either dimension. resize_wh = Vector(0, 0) if bare_bbox.w < 100: @@ -656,12 +658,11 @@ def calc_part_bbox(part): calc_part_bbox(part) -def finalize_parts_and_nets(circuit, **options): +def finalize_parts_and_nets(circuit: Circuit, **options: Unpack[Options]): """Restore parts and nets after place & route is done.""" # Remove any NetTerminals that were added. - net_terminals = (p for p in circuit.parts if isinstance(p, NetTerminal)) - circuit.rmv_parts(*net_terminals) + circuit.parts = [p for p in circuit.parts if not isinstance(p, NetTerminal)] # Return pins from the part units to their parent part. for part in circuit.parts: @@ -672,13 +673,13 @@ def finalize_parts_and_nets(circuit, **options): def gen_schematic( - circuit, - filepath=".", - top_name=get_script_name(), + circuit: Circuit, + filepath: str = ".", + top_name: str = get_script_name(), title="SKiDL-Generated Schematic", - flatness=0.0, - retries=2, - **options + flatness: float = 0.0, + retries: int = 2, + **options: Unpack[Options], ): """Create a schematic file from a Circuit object. @@ -692,7 +693,6 @@ def gen_schematic( options (dict, optional): Dict of options and values, usually for drawing/debugging. """ - from .node import SchNode from .place import PlacementFailure from .route import RoutingFailure @@ -733,7 +733,7 @@ def gen_schematic( continue # Generate EESCHEMA code for the schematic. - # TODO: + # TODO: extract into for generating schematic objects # node_to_eeschema(node) # Clean up. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 2aa7d413..3a73f9be 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Iterator, TypedDict from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector +from dataclasses import dataclass if TYPE_CHECKING: from .route import Face, GlobalTrack @@ -32,14 +33,15 @@ def rmv_attr(objs, attrs): pass + class Part: # We need to assign these ref: str hierarchy: str # dot-separated string of part names - unit: dict[str, "Part"] # units within the part, empty is this is all it is + unit: dict[str, "PartUnit"] # units within the part, empty is this is all it is + pins: list["Pin"] # TODO: source # TODO: where are these expected to be assigned? - pins: list["Pin"] # TODO: source place_bbox: BBox # TODO: lbl_bbox: BBox # TODO: tx: Tx # transformation matrix of the part's position @@ -71,6 +73,30 @@ def draw(self): # TODO: raise NotImplementedError + def grab_pins(self) -> None: + """Grab pin from Part and assign to PartUnit.""" + + for pin in self.pins: + pin.part = self + + +class PartUnit(Part): + # TODO: represent these in Faebryk + + parent: Part + + def grab_pins(self) -> None: + """Grab pin from Part and assign to PartUnit.""" + + for pin in self.pins: + pin.part = self + + def release_pins(self) -> None: + """Return PartUnit pins to parent Part.""" + + for pin in self.pins: + pin.part = self.parent + class Pin: # We need to assign these @@ -95,6 +121,7 @@ class Pin: y: float # internal use + routed: bool route_pt: Point face: "Face" From 335106938bf42ac0ffcab678a441973b3486388b Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 25 Sep 2024 10:11:45 +0200 Subject: [PATCH 29/85] Sort shim attrs and add add attrs from `getattr` --- .../exporters/schematic/kicad/skidl/shims.py | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 3a73f9be..5cf10f69 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -1,9 +1,15 @@ -"""Replace common skidl functions with our own""" +""" +Replace common skidl functions with our own + +Design notes: +- Skidl's original code uses hasattr to check if an attribute is present. + I don't want to refactor all this, so we're assigning attributes as Skidl does +- We're not using dataclasses because typing doesn't pass through InitVar properly +""" from typing import TYPE_CHECKING, Any, Iterator, TypedDict from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector -from dataclasses import dataclass if TYPE_CHECKING: from .route import Face, GlobalTrack @@ -39,12 +45,11 @@ class Part: ref: str hierarchy: str # dot-separated string of part names unit: dict[str, "PartUnit"] # units within the part, empty is this is all it is - pins: list["Pin"] # TODO: source + pins: list["Pin"] + # A string of H, V, L, R operations that are applied in sequence left-to-right. + symtx: str # TODO: where are these expected to be assigned? - place_bbox: BBox # TODO: - lbl_bbox: BBox # TODO: - tx: Tx # transformation matrix of the part's position prev_tx: Tx # previous transformation matrix of the part's position anchor_pins: dict[Any, list["Pin"]] # TODO: better types, what is this? pull_pins: dict[Any, list["Pin"]] # TODO: better types, what is this? @@ -55,7 +60,10 @@ class Part: delta_cost_tx: Tx # transformation matrix associated with delta_cost orientation_locked: bool # whether the part's orientation is locked - # attr assigned, eg. I don't need to care about it + # internal use + tx: Tx # transformation matrix of the part's position + lbl_bbox: BBox + place_bbox: BBox original_tx: Tx # internal use force: Vector # used for debugging mv: Vector # internal use @@ -83,6 +91,7 @@ def grab_pins(self) -> None: class PartUnit(Part): # TODO: represent these in Faebryk + num: int # which unit does this represent parent: Part def grab_pins(self) -> None: @@ -104,29 +113,23 @@ class Pin: name: str net: "Net" stub: bool # whether to stub the pin or not - - # TODO: where are these expected to be assigned? - part: Part # TODO: - place_pt: Point # TODO: - pt: Point # TODO: - orientation: str # TODO: - route_pt: Point # TODO: - place_pt: Point # TODO: - orientation: str # TODO: - bbox: BBox # TODO: - - # Assigned in NetTerminal, but it's unclear - # whether this is typically something that comes from the user + part: Part # to which part does this pin belong? + orientation: str # "U"/"D"/"L"/"R" for the pin's location + # position of the pin + # - relative to the part? x: float y: float # internal use + pt: Point + place_pt: Point + bbox: BBox routed: bool route_pt: Point face: "Face" def is_connected(self) -> bool: - # TODO: + """Whether the pin is connected to anything""" raise NotImplementedError From eb4f9d77d6ddedab383d3272d304681b57d2a9a4 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 25 Sep 2024 10:18:48 +0200 Subject: [PATCH 30/85] Organise shim attributes into required and not --- .../exporters/schematic/kicad/skidl/shims.py | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 5cf10f69..b00e956f 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -3,7 +3,7 @@ Design notes: - Skidl's original code uses hasattr to check if an attribute is present. - I don't want to refactor all this, so we're assigning attributes as Skidl does + I don't want to refactor all this, so we're assigning attributes as Skidl does - We're not using dataclasses because typing doesn't pass through InitVar properly """ @@ -42,39 +42,37 @@ def rmv_attr(objs, attrs): class Part: # We need to assign these - ref: str hierarchy: str # dot-separated string of part names - unit: dict[str, "PartUnit"] # units within the part, empty is this is all it is pins: list["Pin"] + ref: str # A string of H, V, L, R operations that are applied in sequence left-to-right. symtx: str - - # TODO: where are these expected to be assigned? - prev_tx: Tx # previous transformation matrix of the part's position - anchor_pins: dict[Any, list["Pin"]] # TODO: better types, what is this? - pull_pins: dict[Any, list["Pin"]] # TODO: better types, what is this? - pin_ctrs: dict # TODO: better types, what is this? - saved_anchor_pins: dict[Any, list["Pin"]] # copy of anchor_pins - saved_pull_pins: dict[Any, list["Pin"]] # copy of pull_pins - delta_cost: float # the largest decrease in cost and the associated orientation. - delta_cost_tx: Tx # transformation matrix associated with delta_cost - orientation_locked: bool # whether the part's orientation is locked + unit: dict[str, "PartUnit"] # units within the part, empty is this is all it is # internal use - tx: Tx # transformation matrix of the part's position - lbl_bbox: BBox - place_bbox: BBox - original_tx: Tx # internal use + anchor_pins: dict[Any, list["Pin"]] + bottom_track: "GlobalTrack" + delta_cost_tx: Tx # transformation matrix associated with delta_cost + delta_cost: float # the largest decrease in cost and the associated orientation. force: Vector # used for debugging - mv: Vector # internal use + lbl_bbox: BBox left_track: "GlobalTrack" + mv: Vector # internal use + # whether the part's orientation is locked, based on symtx or pin count + orientation_locked: bool + original_tx: Tx # internal use + pin_ctrs: dict["Net", Point] + place_bbox: BBox + prev_tx: Tx # previous transformation matrix of the part's position + pull_pins: dict[Any, list["Pin"]] right_track: "GlobalTrack" + saved_anchor_pins: dict[Any, list["Pin"]] # copy of anchor_pins + saved_pull_pins: dict[Any, list["Pin"]] # copy of pull_pins top_track: "GlobalTrack" - bottom_track: "GlobalTrack" + tx: Tx # transformation matrix of the part's position def __iter__(self) -> Iterator["Pin"]: - # TODO: - raise NotImplementedError + yield from self.pins @property def draw(self): @@ -109,24 +107,24 @@ def release_pins(self) -> None: class Pin: # We need to assign these - num: str name: str net: "Net" - stub: bool # whether to stub the pin or not - part: Part # to which part does this pin belong? + num: str orientation: str # "U"/"D"/"L"/"R" for the pin's location + part: Part # to which part does this pin belong? + stub: bool # whether to stub the pin or not # position of the pin # - relative to the part? x: float y: float # internal use - pt: Point - place_pt: Point bbox: BBox - routed: bool - route_pt: Point face: "Face" + place_pt: Point + pt: Point + route_pt: Point + routed: bool def is_connected(self) -> bool: """Whether the pin is connected to anything""" @@ -137,8 +135,8 @@ class Net: # We need to assign these name: str netio: str # whether input or output - pins: list[Pin] parts: set[Part] + pins: list[Pin] stub: bool # whether to stub the pin or not def __bool__(self) -> bool: @@ -149,15 +147,18 @@ def __iter__(self) -> Iterator[Pin | Part]: yield from self.pins yield from self.parts + def __hash__(self) -> int: + """Nets are sometimes used as keys in dicts, so they must be hashable""" + return id(self) + def is_implicit(self) -> bool: - # TODO: - # Hint; The net has a user-assigned name, so add a terminal to it below. + """Whether the net has a user-assigned name""" raise NotImplementedError class Circuit: - parts: list[Part] nets: list[Net] + parts: list[Part] class Options(TypedDict): From 5cf78b3bc50a2efe2b2fb307e7b21057f1c2ff7b Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 25 Sep 2024 12:14:53 +0200 Subject: [PATCH 31/85] First information across boundary --- .../exporters/schematic/kicad/skidl/bboxes.py | 2 +- .../schematic/kicad/skidl/gen_schematic.py | 15 +++--- .../exporters/schematic/kicad/skidl/shims.py | 54 ++++++++++++++++++- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py index 1d027a57..3d405abe 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py @@ -312,7 +312,7 @@ def make_pin_dir_tbl(abs_xoff: int = 20) -> dict[str, "PinDir"]: return unit_bboxes -def calc_hier_label_bbox(label, dir): +def calc_hier_label_bbox(label: str, dir: str) -> BBox: """Calculate the bounding box for a hierarchical label. Args: diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index 965e6f1f..575046ec 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -20,7 +20,7 @@ from .geometry import BBox, Point, Tx, Vector from .net_terminal import NetTerminal from .node import HIER_SEP, SchNode -from .shims import Circuit, Options, Part, PartUnit, get_script_name, rmv_attr +from .shims import Circuit, Options, Part, PartUnit, Pin, get_script_name, rmv_attr def bbox_to_eeschema(bbox: BBox, tx: Tx, name=None): @@ -564,11 +564,11 @@ def rotate_power_pins(part: Part): if not getattr(part, "symtx", ""): return - def is_pwr(net): - return net_name.startswith("+") + def is_pwr(pin: Pin) -> bool: + return pin.fab_is_pwr - def is_gnd(net): - return "gnd" in net_name.lower() + def is_gnd(pin: Pin) -> bool: + return pin.fab_is_gnd dont_rotate_pin_cnt = options.get("dont_rotate_pin_count", 10000) @@ -580,8 +580,7 @@ def is_gnd(net): # Tally what rotation would make each pwr/gnd pin point up or down. rotation_tally = Counter() for pin in part_unit: - net_name = getattr(pin.net, "name", "").lower() - if is_gnd(net_name): + if is_gnd(pin): if pin.orientation == "U": rotation_tally[0] += 1 if pin.orientation == "D": @@ -590,7 +589,7 @@ def is_gnd(net): rotation_tally[90] += 1 if pin.orientation == "R": rotation_tally[270] += 1 - elif is_pwr(net_name): + elif is_pwr(pin): if pin.orientation == "D": rotation_tally[0] += 1 if pin.orientation == "U": diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index b00e956f..13207d65 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Iterator, TypedDict +import faebryk.library._F as F from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector if TYPE_CHECKING: @@ -39,7 +40,6 @@ def rmv_attr(objs, attrs): pass - class Part: # We need to assign these hierarchy: str # dot-separated string of part names @@ -49,6 +49,15 @@ class Part: symtx: str unit: dict[str, "PartUnit"] # units within the part, empty is this is all it is + # things we've added to make life easier + fab_symbol: F.Symbol + + def audit(self) -> None: + """Ensure mandatory attributes are set""" + for attr in ["hierarchy", "pins", "ref", "symtx", "unit", "fab_symbol"]: + if not hasattr(self, attr): + raise ValueError(f"Missing attribute: {attr}") + # internal use anchor_pins: dict[Any, list["Pin"]] bottom_track: "GlobalTrack" @@ -92,6 +101,13 @@ class PartUnit(Part): num: int # which unit does this represent parent: Part + def audit(self) -> None: + """Ensure mandatory attributes are set""" + super().audit() + for attr in ["num", "parent"]: + if not hasattr(self, attr): + raise ValueError(f"Missing attribute: {attr}") + def grab_pins(self) -> None: """Grab pin from Part and assign to PartUnit.""" @@ -118,6 +134,29 @@ class Pin: x: float y: float + # things we've added to make life easier + fab_pin: F.Symbol.Pin + fab_is_gnd: bool + fab_is_pwr: bool + + def audit(self) -> None: + """Ensure mandatory attributes are set""" + for attr in [ + "name", + "net", + "num", + "orientation", + "part", + "stub", + "x", + "y", + "fab_pin", + "fab_is_gnd", + "fab_is_pwr", + ]: + if not hasattr(self, attr): + raise ValueError(f"Missing attribute: {attr}") + # internal use bbox: BBox face: "Face" @@ -139,11 +178,18 @@ class Net: pins: list[Pin] stub: bool # whether to stub the pin or not + def audit(self) -> None: + """Ensure mandatory attributes are set""" + for attr in ["name", "netio", "parts", "pins", "stub"]: + if not hasattr(self, attr): + raise ValueError(f"Missing attribute: {attr}") + def __bool__(self) -> bool: """TODO: does this need to be false if no parts or pins?""" raise NotImplementedError def __iter__(self) -> Iterator[Pin | Part]: + raise NotImplementedError # not sure what to output here yield from self.pins yield from self.parts @@ -160,6 +206,12 @@ class Circuit: nets: list[Net] parts: list[Part] + def audit(self) -> None: + """Ensure mandatory attributes are set""" + for attr in ["nets", "parts"]: + if not hasattr(self, attr): + raise ValueError(f"Missing attribute: {attr}") + class Options(TypedDict): allow_routing_failure: bool From 4411daeeb7614522012974adec878fbb92cb4f8c Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 25 Sep 2024 15:17:34 +0200 Subject: [PATCH 32/85] Type debug_draw --- .../schematic/kicad/skidl/debug_draw.py | 85 ++++++++++++++++--- 1 file changed, 74 insertions(+), 11 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index 8ea3255b..bb61b90b 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -8,13 +8,25 @@ """ from collections import defaultdict from random import randint +from typing import TYPE_CHECKING from .geometry import BBox, Point, Segment, Tx, Vector +if TYPE_CHECKING: + import pygame + import pygame.font + + from faebryk.exporters.schematic.kicad.skidl.node import SchNode + from faebryk.exporters.schematic.kicad.skidl.route import Interval, NetInterval + from faebryk.exporters.schematic.kicad.skidl.shims import Net, Part + # Dictionary for storing colors to visually distinguish routed nets. net_colors = defaultdict(lambda: (randint(0, 200), randint(0, 200), randint(0, 200))) -def draw_box(bbox, scr, tx, color=(192, 255, 192), thickness=0): + +def draw_box( + bbox: BBox, scr: "pygame.Surface", tx: Tx, color=(192, 255, 192), thickness=0 +): """Draw a box in the drawing area. Args: @@ -37,7 +49,8 @@ def draw_box(bbox, scr, tx, color=(192, 255, 192), thickness=0): ) pygame.draw.polygon(scr, color, corners, thickness) -def draw_endpoint(pt, scr, tx, color=(100, 100, 100), dot_radius=10): + +def draw_endpoint(pt: Point, scr: "pygame.Surface", tx: Tx, color=(100, 100, 100), dot_radius=10): """Draw a line segment endpoint in the drawing area. Args: @@ -65,7 +78,14 @@ def draw_endpoint(pt, scr, tx, color=(100, 100, 100), dot_radius=10): radius = dot_radius * tx.a pygame.draw.circle(scr, color, (pt.x, pt.y), radius) -def draw_seg(seg, scr, tx, color=(100, 100, 100), thickness=5, dot_radius=10): +def draw_seg( + seg: Segment | "Interval" | "NetInterval", + scr: "pygame.Surface", + tx: Tx, + color=(100, 100, 100), + thickness=5, + dot_radius=10, +): """Draw a line segment in the drawing area. Args: @@ -96,7 +116,15 @@ def draw_seg(seg, scr, tx, color=(100, 100, 100), thickness=5, dot_radius=10): scr, color, (seg.p1.x, seg.p1.y), (seg.p2.x, seg.p2.y), width=thickness ) -def draw_text(txt, pt, scr, tx, font, color=(100, 100, 100), real=True): +def draw_text( + txt: str, + pt: Point, + scr: "pygame.Surface", + tx: Tx, + font: "pygame.font.Font", + color=(100, 100, 100), + real=True, +): """Render text in drawing area. Args: @@ -113,9 +141,11 @@ def draw_text(txt, pt, scr, tx, font, color=(100, 100, 100), real=True): pt = pt * tx # Render text. + # TODO: pygame version may've varied because + # syntax highlighting doesn't recognise this font.render_to(scr, (pt.x, pt.y), txt, color) -def draw_part(part, scr, tx, font): +def draw_part(part: Part, scr: "pygame.Surface", tx: Tx, font: "pygame.font.Font"): """Draw part bounding box. Args: @@ -139,7 +169,16 @@ def draw_part(part, scr, tx, font): # Probably trying to draw a block of parts which has no pins and can't iterate thru them. pass -def draw_net(net, parts, scr, tx, font, color=(0, 0, 0), thickness=2, dot_radius=5): +def draw_net( + net: "Net", + parts: list["Part"], + scr: "pygame.Surface", + tx: Tx, + font: "pygame.font.Font", + color=(0, 0, 0), + thickness=2, + dot_radius=5, +): """Draw net and connected terminals. Args: @@ -168,7 +207,14 @@ def draw_net(net, parts, scr, tx, font, color=(0, 0, 0), thickness=2, dot_radius dot_radius=dot_radius, ) -def draw_force(part, force, scr, tx, font, color=(128, 0, 0)): +def draw_force( + part: Part, + force: Vector, + scr: "pygame.Surface", + tx: Tx, + font: "pygame.font.Font", + color=(128, 0, 0), +): """Draw force vector affecting a part. Args: @@ -185,7 +231,13 @@ def draw_force(part, force, scr, tx, font, color=(128, 0, 0)): Segment(anchor, anchor + force), scr, tx, color=color, thickness=5, dot_radius=5 ) -def draw_placement(parts, nets, scr, tx, font): +def draw_placement( + parts: list["Part"], + nets: list["Net"], + scr: "pygame.Surface", + tx: Tx, + font: "pygame.font.Font", +): """Draw placement of parts and interconnecting nets. Args: @@ -203,7 +255,13 @@ def draw_placement(parts, nets, scr, tx, font): draw_net(net, parts, scr, tx, font) draw_redraw() -def draw_routing(node, bbox, parts, *other_stuff, **options): +def draw_routing( + node: "SchNode", + bbox: BBox, + parts: list["Part"], + *other_stuff, + **options, +): """Draw routing for debugging purposes. Args: @@ -234,7 +292,10 @@ def draw_routing(node, bbox, parts, *other_stuff, **options): draw_end() -def draw_clear(scr, color=(255, 255, 255)): +def draw_clear( + scr: "pygame.Surface", + color: tuple[int, int, int] = (255, 255, 255), +): """Clear drawing area. Args: @@ -243,7 +304,9 @@ def draw_clear(scr, color=(255, 255, 255)): """ scr.fill(color) -def draw_start(bbox): +def draw_start( + bbox: BBox, +): """ Initialize PyGame drawing area. From dcf198340b1427cdbab9036aa8213a8620b3d5eb Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 25 Sep 2024 16:37:03 +0200 Subject: [PATCH 33/85] Add property markers to transformer --- .../schematic/kicad/skidl/gen_schematic.py | 13 +- .../exporters/schematic/kicad/transformer.py | 174 +++++++++++------- 2 files changed, 118 insertions(+), 69 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index 575046ec..c1442de7 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -15,7 +15,7 @@ from collections import Counter, OrderedDict from typing import Unpack -from .bboxes import calc_hier_label_bbox, calc_symbol_bbox +from .bboxes import calc_hier_label_bbox from .constants import BLK_INT_PAD, BOX_LABEL_FONT_SIZE, GRID, PIN_LABEL_FONT_SIZE from .geometry import BBox, Point, Tx, Vector from .net_terminal import NetTerminal @@ -612,11 +612,14 @@ def is_gnd(pin: Pin) -> bool: def calc_part_bbox(part: Part): """Calculate the labeled bounding boxes and store it in the part.""" + from faebryk.exporters.schematic.kicad.transformer import SchTransformer - # Find part/unit bounding boxes excluding any net labels on pins. - # TODO: part.lbl_bbox could be substituted for part.bbox. - # TODO: Part ref and value should be updated before calculating bounding box. - bare_bboxes = calc_symbol_bbox(part)[1:] + f_symbol = part.fab_symbol.get_trait(SchTransformer.has_linked_sch_symbol).symbol + bare_bboxes = BBox(Point(*pts) for pts in SchTransformer.get_bbox(f_symbol)) + + # # Find part/unit bounding boxes excluding any net labels on pins. + # # TODO: part.lbl_bbox could be substituted for part.bbox. + # # TODO: Part ref and value should be updated before calculating bounding box. for part_unit, bare_bbox in zip(units(part), bare_bboxes): assert isinstance(part_unit, (PartUnit, Part)) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 8b0fe9d0..eff20b92 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -1,6 +1,7 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT +import hashlib import logging import pprint from copy import deepcopy @@ -8,14 +9,17 @@ from itertools import chain, groupby from os import PathLike from pathlib import Path -from typing import Any, List, Protocol +from typing import Any, List, Protocol, Unpack -# import numpy as np -# from shapely import Polygon +from faebryk.exporters.schematic.kicad.skidl.shims import Options import faebryk.library._F as F from faebryk.core.graphinterface import Graph from faebryk.core.module import Module from faebryk.core.node import Node + +# import numpy as np +# from shapely import Polygon +from faebryk.exporters.pcb.kicad.transformer import is_marked from faebryk.libs.exceptions import FaebrykException from faebryk.libs.geometry.basic import Geometry from faebryk.libs.kicad.fileformats import ( @@ -62,19 +66,14 @@ Alignment_Default = (Justify.center_horizontal, Justify.center_vertical, Justify.normal) -def gen_uuid(mark: str = "") -> UUID: - return _gen_uuid(mark) - - -def is_marked(uuid: UUID, mark: str): - suffix = mark.encode().hex() - return uuid.replace("-", "").endswith(suffix) - - class _HasUUID(Protocol): uuid: UUID +class _HasPropertys(Protocol): + propertys: dict[str, C_property] + + # TODO: consider common transformer base class SchTransformer: @@ -99,7 +98,7 @@ def __init__( self.app = app self._symbol_files_index: dict[str, Path] = {} - self.missing_symbols: list[SCH.C_lib_symbols.C_symbol] = [] + self.missing_symbols: list[F.Symbol] = [] self.dimensions = None @@ -146,16 +145,7 @@ def attach(self): ) } logger.debug(f"Attached: {pprint.pformat(attached)}") - - if self.missing_symbols: - # TODO: just go look for the symbols instead - raise ExceptionGroup( - "Missing lib symbols", - [ - f"Symbol {sym.name} not found in symbols dictionary" - for sym in self.missing_symbols - ], - ) + logger.debug(f"Missing: {pprint.pformat(self.missing_symbols)}") def attach_symbol(self, f_symbol: F.Symbol, sch_symbol: SCH.C_symbol_instance): """Bind the module and symbol together on the graph""" @@ -165,21 +155,22 @@ def attach_symbol(self, f_symbol: F.Symbol, sch_symbol: SCH.C_symbol_instance): for pin_name, pins in groupby(sch_symbol.pins, key=lambda p: p.name): f_symbol.pins[pin_name].add(SchTransformer.has_linked_sch_pins(pins)) - def cleanup(self): - """Delete faebryk-created objects in schematic.""" + # TODO: remove cleanup, it shouldn't really be required if we're marking propertys + # def cleanup(self): + # """Delete faebryk-created objects in schematic.""" - # find all objects with path_len 2 (direct children of a list in pcb) - candidates = [o for o in dataclass_dfs(self.sch) if len(o[1]) == 2] - for obj, path, _ in candidates: - if not self.is_marked(obj): - continue + # # find all objects with path_len 2 (direct children of a list in pcb) + # candidates = [o for o in dataclass_dfs(self.sch) if len(o[1]) == 2] + # for obj, path, _ in candidates: + # if not self.check_mark(obj): + # continue - # delete object by removing it from the container they are in - holder = path[-1] - if isinstance(holder, list): - holder.remove(obj) - elif isinstance(holder, dict): - del holder[get_key(obj, holder)] + # # delete object by removing it from the container they are in + # holder = path[-1] + # if isinstance(holder, list): + # holder.remove(obj) + # elif isinstance(holder, dict): + # del holder[get_key(obj, holder)] def index_symbol_files( self, fp_lib_tables: PathLike | list[PathLike], load_globals: bool = True @@ -208,16 +199,6 @@ def index_symbol_files( def flipped[T](input_list: list[tuple[T, int]]) -> list[tuple[T, int]]: return [(x, (y + 180) % 360) for x, y in reversed(input_list)] - @staticmethod - def gen_uuid(mark: bool = False): - return gen_uuid(mark="FBRK" if mark else "") - - @staticmethod - def is_marked(obj) -> bool: - if not hasattr(obj, "uuid"): - return False - return is_marked(obj.uuid, "FBRK") - # Getter --------------------------------------------------------------------------- @staticmethod def get_symbol(cmp: Node) -> F.Symbol: @@ -294,31 +275,82 @@ def _name_filter(sch_pin: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin): ) return lib_pin - # Insert --------------------------------------------------------------------------- + # Marking ------------------------------------------------------------------------- + """ + There are two methods to mark objects in the schematic: + 1. For items with propertys, add a property with a hash of the contents of + itself, minus the mark property. This is used to detect changes to things + such as position that the user may have nudged externally. + 2. For items without propertys, generate the uuid with the mark. + + Anything generated by this transformer is marked. + """ + @staticmethod - def mark[R: _HasUUID](node: R) -> R: - if hasattr(node, "uuid"): - node.uuid = SchTransformer.gen_uuid(mark=True) # type: ignore + def gen_uuid(mark: bool = False) -> UUID: + return _gen_uuid(mark="FBRK" if mark else "") - return node + @staticmethod + def is_uuid_marked(obj) -> bool: + if not hasattr(obj, "uuid"): + return False + assert isinstance(obj.uuid, str) + suffix = "FBRK".encode().hex() + return obj.uuid.replace("-", "").endswith(suffix) - def _get_list_field[R](self, node: R, prefix: str = "") -> list[R]: - root = self.sch - key = prefix + type(node).__name__.removeprefix("C_") + "s" + @staticmethod + def hash_contents(obj) -> str: + """Hash the contents of an object, minus the mark""" + # filter out mark properties + def _filter(k: tuple[Any, list[Any], list[str]]) -> bool: + obj, _, _ = k + if isinstance(obj, C_property): + if obj.name == "faebryk_mark": + return False + return True + + hasher = hashlib.blake2b() + for obj, _, name_path in filter(_filter, dataclass_dfs(obj)): + hasher.update(f"{name_path}={obj}".encode()) + + return hasher.hexdigest() - assert hasattr(root, key) + @staticmethod + def check_mark(obj) -> bool: + """Return True if an object is validly marked""" + if hasattr(obj, "propertys"): + if "faebryk_mark" in obj.propertys: + prop = obj.propertys["faebryk_mark"] + assert isinstance(prop, C_property) + return prop.value == SchTransformer.hash_contents(obj) + else: + # items that have the capacity to be marked + # via propertys are only considered marked + # if they have the property and it's valid, + # despite their uuid + return False - target = getattr(root, key) - assert isinstance(target, list) - assert all(isinstance(x, type(node)) for x in target) - return target + return SchTransformer.is_uuid_marked(obj) - def _insert(self, obj: Any, prefix: str = ""): - obj = SchTransformer.mark(obj) - self._get_list_field(obj, prefix=prefix).append(obj) + @staticmethod + def mark[R: _HasUUID | _HasPropertys](obj: R) -> R: + """Mark the property if possible, otherwise ensure the uuid is marked""" + if hasattr(obj, "propertys"): + obj.propertys["faebryk_mark"] = C_property( + name="faebryk_mark", + value=SchTransformer.hash_contents(obj), + ) + + else: + if not hasattr(obj, "uuid"): + raise TypeError(f"Object {obj} has no propertys or uuid") - def _delete(self, obj: Any, prefix: str = ""): - self._get_list_field(obj, prefix=prefix).remove(obj) + if not is_marked(obj): + obj.uuid = SchTransformer.gen_uuid(mark=True) + + return obj + + # Insert --------------------------------------------------------------------------- def insert_wire( self, @@ -419,6 +451,9 @@ def insert_symbol( uuid=self.gen_uuid(mark=True), ) + # It's one of ours, until it's modified in KiCAD + SchTransformer.mark(unit_instance) + # Add a C_property for the reference based on the override name if reference_name := module.get_trait(F.has_overriden_name).get_name(): unit_instance.propertys["Reference"] = C_property( @@ -507,3 +542,14 @@ def _(self, symbol: SCH.C_symbol_instance) -> BoundingBox: # FIXME: this requires context to get the lib symbol, # which means it must be called with self return Geometry.abs_pos(self.get_bbox(self.get_lib_symbol(symbol))) + + def generate_schematic(self, **options: Unpack[Options]): + """Does what it says on the tin.""" + # 1. add missing symbols + for f_symbol in self.missing_symbols: + self.insert_symbol(f_symbol) + self.missing_symbols = [] + + # 2. create shim objects + # 3. run skidl schematic generation + # 4. transform sch according to skidl From e15d7c336e8b154d492664371b515717de6ee09d Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 25 Sep 2024 18:13:04 +0200 Subject: [PATCH 34/85] Add bare_bbox attr to shim --- .../schematic/kicad/skidl/gen_schematic.py | 27 ++++++++----------- .../exporters/schematic/kicad/skidl/shims.py | 11 +++++++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index c1442de7..e18ae0e5 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -612,30 +612,25 @@ def is_gnd(pin: Pin) -> bool: def calc_part_bbox(part: Part): """Calculate the labeled bounding boxes and store it in the part.""" - from faebryk.exporters.schematic.kicad.transformer import SchTransformer + # Find part/unit bounding boxes excluding any net labels on pins. + # TODO: part.lbl_bbox could be substituted for part.bbox. + # TODO: Part ref and value should be updated before calculating bounding box. - f_symbol = part.fab_symbol.get_trait(SchTransformer.has_linked_sch_symbol).symbol - bare_bboxes = BBox(Point(*pts) for pts in SchTransformer.get_bbox(f_symbol)) - - # # Find part/unit bounding boxes excluding any net labels on pins. - # # TODO: part.lbl_bbox could be substituted for part.bbox. - # # TODO: Part ref and value should be updated before calculating bounding box. - - for part_unit, bare_bbox in zip(units(part), bare_bboxes): + for part_unit in units(part): assert isinstance(part_unit, (PartUnit, Part)) - assert isinstance(bare_bbox, BBox) + assert isinstance(part_unit.bare_bbox, BBox) # Expand the bounding box if it's too small in either dimension. resize_wh = Vector(0, 0) - if bare_bbox.w < 100: - resize_wh.x = (100 - bare_bbox.w) / 2 - if bare_bbox.h < 100: - resize_wh.y = (100 - bare_bbox.h) / 2 - bare_bbox = bare_bbox.resize(resize_wh) + if part_unit.bare_bbox.w < 100: + resize_wh.x = (100 - part_unit.bare_bbox.w) / 2 + if part_unit.bare_bbox.h < 100: + resize_wh.y = (100 - part_unit.bare_bbox.h) / 2 + part_unit.bare_bbox = part_unit.bare_bbox.resize(resize_wh) # Find expanded bounding box that includes any hier labels attached to pins. part_unit.lbl_bbox = BBox() - part_unit.lbl_bbox.add(bare_bbox) + part_unit.lbl_bbox.add(part_unit.bare_bbox) for pin in part_unit: if pin.stub: # Find bounding box for net stub label attached to pin. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 13207d65..9ad16448 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -51,10 +51,19 @@ class Part: # things we've added to make life easier fab_symbol: F.Symbol + bare_bbox: BBox def audit(self) -> None: """Ensure mandatory attributes are set""" - for attr in ["hierarchy", "pins", "ref", "symtx", "unit", "fab_symbol"]: + for attr in [ + "hierarchy", + "pins", + "ref", + "symtx", + "unit", + "fab_symbol", + "bare_bbox", + ]: if not hasattr(self, attr): raise ValueError(f"Missing attribute: {attr}") From 59085e3ed62d12666ebb401e999d674db40b77c7 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 26 Sep 2024 12:16:55 +0200 Subject: [PATCH 35/85] Add shim builders! --- .../exporters/schematic/kicad/skidl/shims.py | 40 ++++- .../exporters/schematic/kicad/transformer.py | 137 ++++++++++++++++-- .../schematic/kicad/test_transformer.py | 14 +- 3 files changed, 166 insertions(+), 25 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 9ad16448..2a98deba 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -7,12 +7,15 @@ - We're not using dataclasses because typing doesn't pass through InitVar properly """ +from itertools import chain from typing import TYPE_CHECKING, Any, Iterator, TypedDict import faebryk.library._F as F from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector if TYPE_CHECKING: + from faebryk.libs.kicad import fileformats_sch + from .route import Face, GlobalTrack @@ -40,6 +43,21 @@ def rmv_attr(objs, attrs): pass + +def angle_to_orientation(angle: float) -> str: + """Convert an angle to an orientation""" + if angle == 0: + return "U" + elif angle == 90: + return "R" + elif angle == 180: + return "D" + elif angle == 270: + return "L" + else: + raise ValueError(f"Invalid angle: {angle}") + + class Part: # We need to assign these hierarchy: str # dot-separated string of part names @@ -50,7 +68,8 @@ class Part: unit: dict[str, "PartUnit"] # units within the part, empty is this is all it is # things we've added to make life easier - fab_symbol: F.Symbol + sch_symbol: fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance + fab_symbol: F.Symbol | None bare_bbox: BBox def audit(self) -> None: @@ -67,6 +86,10 @@ def audit(self) -> None: if not hasattr(self, attr): raise ValueError(f"Missing attribute: {attr}") + # don't audit pins, they're handled through nets instead + for unit in self.unit.values(): + unit.audit() + # internal use anchor_pins: dict[Any, list["Pin"]] bottom_track: "GlobalTrack" @@ -132,8 +155,8 @@ def release_pins(self) -> None: class Pin: # We need to assign these - name: str net: "Net" + name: str num: str orientation: str # "U"/"D"/"L"/"R" for the pin's location part: Part # to which part does this pin belong? @@ -144,6 +167,7 @@ class Pin: y: float # things we've added to make life easier + sch_pin: fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance.C_pin fab_pin: F.Symbol.Pin fab_is_gnd: bool fab_is_pwr: bool @@ -183,16 +207,21 @@ class Net: # We need to assign these name: str netio: str # whether input or output - parts: set[Part] pins: list[Pin] stub: bool # whether to stub the pin or not + # internal use + parts: set[Part] + def audit(self) -> None: """Ensure mandatory attributes are set""" - for attr in ["name", "netio", "parts", "pins", "stub"]: + for attr in ["name", "netio", "pins", "stub"]: if not hasattr(self, attr): raise ValueError(f"Missing attribute: {attr}") + for pin in self.pins: + pin.audit() + def __bool__(self) -> bool: """TODO: does this need to be false if no parts or pins?""" raise NotImplementedError @@ -221,6 +250,9 @@ def audit(self) -> None: if not hasattr(self, attr): raise ValueError(f"Missing attribute: {attr}") + for obj in chain(self.nets, self.parts): + obj.audit() + class Options(TypedDict): allow_routing_failure: bool diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index eff20b92..aba4fd73 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -9,9 +9,9 @@ from itertools import chain, groupby from os import PathLike from pathlib import Path +from random import randint from typing import Any, List, Protocol, Unpack -from faebryk.exporters.schematic.kicad.skidl.shims import Options import faebryk.library._F as F from faebryk.core.graphinterface import Graph from faebryk.core.module import Module @@ -20,6 +20,8 @@ # import numpy as np # from shapely import Polygon from faebryk.exporters.pcb.kicad.transformer import is_marked +from faebryk.exporters.schematic.kicad.skidl import shims +from faebryk.exporters.schematic.kicad.skidl.geometry import BBox from faebryk.libs.exceptions import FaebrykException from faebryk.libs.geometry.basic import Geometry from faebryk.libs.kicad.fileformats import ( @@ -43,6 +45,7 @@ from faebryk.libs.kicad.paths import GLOBAL_FP_DIR_PATH, GLOBAL_FP_LIB_PATH from faebryk.libs.sexp.dataclass_sexp import dataclass_dfs from faebryk.libs.util import ( + FuncDict, cast_assert, find, get_key, @@ -75,9 +78,9 @@ class _HasPropertys(Protocol): # TODO: consider common transformer base -class SchTransformer: +class Transformer: - class has_linked_sch_symbol(Module.TraitT.decless()): + class has_linked_sch_symbol(F.Symbol.TraitT.decless()): def __init__(self, symbol: SCH.C_symbol_instance) -> None: super().__init__() self.symbol = symbol @@ -86,8 +89,10 @@ class has_linked_sch_pins(F.Symbol.Pin.TraitT.decless()): def __init__( self, pins: list[SCH.C_symbol_instance.C_pin], + symbol: SCH.C_symbol_instance, ) -> None: super().__init__() + self.symbol = symbol self.pins = pins def __init__( @@ -141,7 +146,7 @@ def attach(self): attached = { n: t.symbol for n, t in self.graph.nodes_with_trait( - SchTransformer.has_linked_sch_symbol + Transformer.has_linked_sch_symbol ) } logger.debug(f"Attached: {pprint.pformat(attached)}") @@ -153,7 +158,9 @@ def attach_symbol(self, f_symbol: F.Symbol, sch_symbol: SCH.C_symbol_instance): # Attach the pins on the symbol to the module interface for pin_name, pins in groupby(sch_symbol.pins, key=lambda p: p.name): - f_symbol.pins[pin_name].add(SchTransformer.has_linked_sch_pins(pins)) + f_symbol.pins[pin_name].add( + Transformer.has_linked_sch_pins(pins, sch_symbol) + ) # TODO: remove cleanup, it shouldn't really be required if we're marking propertys # def cleanup(self): @@ -202,13 +209,13 @@ def flipped[T](input_list: list[tuple[T, int]]) -> list[tuple[T, int]]: # Getter --------------------------------------------------------------------------- @staticmethod def get_symbol(cmp: Node) -> F.Symbol: - return not_none(cmp.get_trait(SchTransformer.has_linked_sch_symbol)).symbol + return not_none(cmp.get_trait(Transformer.has_linked_sch_symbol)).symbol def get_all_symbols(self) -> List[tuple[Module, F.Symbol]]: return [ (cast_assert(Module, cmp), t.symbol) for cmp, t in self.graph.nodes_with_trait( - SchTransformer.has_linked_sch_symbol + Transformer.has_linked_sch_symbol ) ] @@ -262,7 +269,7 @@ def _(self, pin: F.Symbol.Pin) -> SCH.C_lib_symbols.C_symbol.C_symbol.C_pin: assert isinstance(graph_symbol, Node) lib_sym = self.get_lib_symbol(graph_symbol) units = self.get_related_lib_sym_units(lib_sym) - sym = graph_symbol.get_trait(SchTransformer.has_linked_sch_symbol).symbol + sym = graph_symbol.get_trait(Transformer.has_linked_sch_symbol).symbol def _name_filter(sch_pin: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin): return sch_pin.name in { @@ -322,7 +329,7 @@ def check_mark(obj) -> bool: if "faebryk_mark" in obj.propertys: prop = obj.propertys["faebryk_mark"] assert isinstance(prop, C_property) - return prop.value == SchTransformer.hash_contents(obj) + return prop.value == Transformer.hash_contents(obj) else: # items that have the capacity to be marked # via propertys are only considered marked @@ -330,7 +337,7 @@ def check_mark(obj) -> bool: # despite their uuid return False - return SchTransformer.is_uuid_marked(obj) + return Transformer.is_uuid_marked(obj) @staticmethod def mark[R: _HasUUID | _HasPropertys](obj: R) -> R: @@ -338,7 +345,7 @@ def mark[R: _HasUUID | _HasPropertys](obj: R) -> R: if hasattr(obj, "propertys"): obj.propertys["faebryk_mark"] = C_property( name="faebryk_mark", - value=SchTransformer.hash_contents(obj), + value=Transformer.hash_contents(obj), ) else: @@ -346,7 +353,7 @@ def mark[R: _HasUUID | _HasPropertys](obj: R) -> R: raise TypeError(f"Object {obj} has no propertys or uuid") if not is_marked(obj): - obj.uuid = SchTransformer.gen_uuid(mark=True) + obj.uuid = Transformer.gen_uuid(mark=True) return obj @@ -452,7 +459,7 @@ def insert_symbol( ) # It's one of ours, until it's modified in KiCAD - SchTransformer.mark(unit_instance) + Transformer.mark(unit_instance) # Add a C_property for the reference based on the override name if reference_name := module.get_trait(F.has_overriden_name).get_name(): @@ -543,13 +550,115 @@ def _(self, symbol: SCH.C_symbol_instance) -> BoundingBox: # which means it must be called with self return Geometry.abs_pos(self.get_bbox(self.get_lib_symbol(symbol))) - def generate_schematic(self, **options: Unpack[Options]): + def generate_schematic(self, **options: Unpack[shims.Options]): """Does what it says on the tin.""" # 1. add missing symbols for f_symbol in self.missing_symbols: self.insert_symbol(f_symbol) self.missing_symbols = [] + # 1.1 create hollow circuits to append to + circuit = shims.Circuit() + + # 1.2 create maps to short-cut access between fab and sch + sch_to_fab_pin_map: FuncDict[SCH.C_symbol_instance.C_pin, F.Symbol.Pin | None] = FuncDict() + sch_to_fab_sym_map: FuncDict[SCH.C_symbol_instance, F.Symbol | None] = FuncDict() + # for each sch_symbol / (fab_symbol | None) pair, create a shim part + # we need to shim sym object which aren't even in the graph to avoid colliding + for _, f_sym_trait in self.graph.nodes_with_trait(F.Symbol.has_symbol): + if sch_sym_trait := f_sym_trait.reference.try_get_trait( + Transformer.has_linked_sch_symbol + ): + sch_to_fab_sym_map[sch_sym_trait.symbol] = f_sym_trait.reference + for sch_sym in self.sch.symbols: + for sch_pin in sch_sym.pins: + sch_to_fab_pin_map[sch_pin] = f_symbol.pins.get(sch_pin.name) + if sch_sym not in sch_to_fab_sym_map: + sch_to_fab_sym_map[sch_sym] = None + # 2. create shim objects + # 2.1 make nets + sch_to_shim_pin_map: FuncDict[SCH.C_symbol_instance.C_pin, shims.Pin] = FuncDict() + fab_nets = self.graph.nodes_of_type(F.Net) + for net in fab_nets: + shim_net = shims.Net() + shim_net.name = net.get_trait(F.has_overriden_name).get_name() + shim_net.netio = "" # TODO: + shim_net.stub = False # TODO: + + # make partial net-oriented pins + shim_net.pins = [] + for mif in net.get_connected_interfaces(): + if has_fab_pin := mif.try_get_trait(F.Symbol.Pin.has_pin): + if has_sch_pin := has_fab_pin.reference.try_get_trait( + Transformer.has_linked_sch_pins + ): + for sch_pin in has_sch_pin.pins: + shim_pin = shims.Pin() + shim_pin.net = shim_net + shim_net.pins.append(shim_pin) + sch_to_shim_pin_map[sch_pin] = shim_pin + + circuit.nets.append(shim_net) + + # 2.2 make parts + def _hierarchy(module: Module) -> str: + """ + Make a string representation of the module's hierarchy + using the best name for each part we have + """ + def _best_name(module: Module) -> str: + if name_trait := module.try_get_trait(F.has_overriden_name): + return name_trait.get_name() + return module.get_name() + + # skip the root module, because it's name is just "*" + hierarchy = [h[0] for h in module.get_hierarchy()][1:] + return ".".join(_best_name(n) for n in hierarchy) + + # for each sch_symbol, create a shim part + for sch_sym, f_symbol in sch_to_fab_sym_map.items(): + shim_part = shims.Part() + shim_part.hierarchy = _hierarchy(f_symbol.represents) + shim_part.ref = f_symbol.represents.get_trait(F.has_overriden_name).get_name() + # TODO: what's the ato analog? + # TODO: should this desc + shim_part.symtx = "" + shim_part.unit = {} # TODO: support units + shim_part.fab_symbol = f_symbol + shim_part.bare_bbox = BBox( + Point(*pts) for pts in Transformer.get_bbox(f_symbol) + ) + + # 2.3 finish making pins, this time from a part-orientation + sch_lib_symbol = self.get_lib_symbol(sch_sym) + all_sch_units = list(sch_lib_symbol.symbols.values()) + all_sch_lib_pins = [p for u in all_sch_units for p in u.pins] + for sch_pin in sch_sym.pins: + lib_sch_pin = find(all_sch_lib_pins, key=lambda x: x.name == sch_pin.name) + assert isinstance(lib_sch_pin, SCH.C_lib_symbols.C_symbol.C_symbol.C_pin) + shim_pin = sch_to_shim_pin_map.setdefault(sch_pin, shims.Pin()) + shim_pin.name = sch_pin.name + shim_pin.num = lib_sch_pin.number + shim_pin.orientation = shims.angle_to_orientation(lib_sch_pin.at.r) + shim_pin.part = shim_part + + # TODO: ideas: + # - stub powery things + # - override from symbol layout info trait + shim_pin.stub = False + + shim_pin.x = lib_sch_pin.at.x + shim_pin.y = lib_sch_pin.at.y + shim_pin.fab_pin = sch_to_fab_pin_map[sch_pin] + shim_pin.sch_pin = sch_pin + + shim_part.pins.append(shim_pin) + + circuit.parts.append(shim_part) + + # 2.-1 run audit on circuit + circuit.audit() + # 3. run skidl schematic generation # 4. transform sch according to skidl diff --git a/test/exporters/schematic/kicad/test_transformer.py b/test/exporters/schematic/kicad/test_transformer.py index 2b64f222..7b683ae8 100644 --- a/test/exporters/schematic/kicad/test_transformer.py +++ b/test/exporters/schematic/kicad/test_transformer.py @@ -4,7 +4,7 @@ import faebryk.library._F as F from faebryk.core.module import Module -from faebryk.exporters.schematic.kicad.transformer import SchTransformer +from faebryk.exporters.schematic.kicad.transformer import Transformer from faebryk.libs.exceptions import FaebrykException from faebryk.libs.kicad.fileformats_sch import C_kicad_sch_file from faebryk.libs.util import find @@ -31,10 +31,10 @@ def sch_file(test_dir: Path): @pytest.fixture def transformer(sch_file: C_kicad_sch_file): app = Module() - return SchTransformer(sch_file.kicad_sch, app.get_graph(), app) + return Transformer(sch_file.kicad_sch, app.get_graph(), app) -def test_wire_transformer(transformer: SchTransformer): +def test_wire_transformer(transformer: Transformer): start_wire_count = len(transformer.sch.wires) transformer.insert_wire( @@ -53,19 +53,19 @@ def test_wire_transformer(transformer: SchTransformer): ] -def test_index_symbol_files(transformer: SchTransformer, fp_lib_path_path: Path): +def test_index_symbol_files(transformer: Transformer, fp_lib_path_path: Path): assert len(transformer._symbol_files_index) == 0 transformer.index_symbol_files(fp_lib_path_path, load_globals=False) assert len(transformer._symbol_files_index) == 1 @pytest.fixture -def full_transformer(transformer: SchTransformer, fp_lib_path_path: Path): +def full_transformer(transformer: Transformer, fp_lib_path_path: Path): transformer.index_symbol_files(fp_lib_path_path, load_globals=False) return transformer -def test_get_symbol_file(full_transformer: SchTransformer): +def test_get_symbol_file(full_transformer: Transformer): with pytest.raises(FaebrykException): full_transformer.get_symbol_file("notta-lib") @@ -76,7 +76,7 @@ def test_get_symbol_file(full_transformer: SchTransformer): ) -def test_insert_symbol(full_transformer: SchTransformer, sch_file: C_kicad_sch_file): +def test_insert_symbol(full_transformer: Transformer, sch_file: C_kicad_sch_file): start_symbol_count = len(full_transformer.sch.symbols) # mimicing typically design/user-space From 0bf421a93315f056ca387592e16af04f587bd14a Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 26 Sep 2024 12:22:11 +0200 Subject: [PATCH 36/85] Cleanup so tests import --- src/faebryk/exporters/schematic/kicad/skidl/geometry.py | 4 ++-- src/faebryk/exporters/schematic/kicad/skidl/shims.py | 4 ++-- src/faebryk/exporters/schematic/kicad/transformer.py | 2 -- src/faebryk/library/_F.py | 1 - 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index 902d5756..a5fddbc0 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -96,7 +96,7 @@ def __mul__(self, m: "Tx | float") -> "Tx": ) @property - def origin(self) -> Point: + def origin(self) -> "Point": """Return the (dx, dy) translation as a Point.""" return Point(self.dx, self.dy) @@ -111,7 +111,7 @@ def scale(self) -> float: """Return the scaling factor.""" return (Point(1, 0) * self - Point(0, 0) * self).magnitude - def move(self, vec: Point) -> "Tx": + def move(self, vec: "Point") -> "Tx": """Return Tx with movement vector applied.""" return self * Tx(dx=vec.x, dy=vec.y) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 2a98deba..43ef8b7e 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -68,7 +68,7 @@ class Part: unit: dict[str, "PartUnit"] # units within the part, empty is this is all it is # things we've added to make life easier - sch_symbol: fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance + sch_symbol: "fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance" fab_symbol: F.Symbol | None bare_bbox: BBox @@ -167,7 +167,7 @@ class Pin: y: float # things we've added to make life easier - sch_pin: fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance.C_pin + sch_pin: "fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance.C_pin" fab_pin: F.Symbol.Pin fab_is_gnd: bool fab_is_pwr: bool diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index aba4fd73..5a849115 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -9,7 +9,6 @@ from itertools import chain, groupby from os import PathLike from pathlib import Path -from random import randint from typing import Any, List, Protocol, Unpack import faebryk.library._F as F @@ -48,7 +47,6 @@ FuncDict, cast_assert, find, - get_key, not_none, once, ) diff --git a/src/faebryk/library/_F.py b/src/faebryk/library/_F.py index 12340c67..8f056067 100644 --- a/src/faebryk/library/_F.py +++ b/src/faebryk/library/_F.py @@ -94,7 +94,6 @@ from faebryk.library.has_equal_pins import has_equal_pins from faebryk.library.has_kicad_manual_footprint import has_kicad_manual_footprint from faebryk.library.has_pcb_routing_strategy_greedy_direct_line import has_pcb_routing_strategy_greedy_direct_line -from faebryk.library.has_symbol_layout_defined import has_symbol_layout_defined from faebryk.library.BJT import BJT from faebryk.library.Diode import Diode from faebryk.library.MOSFET import MOSFET From 319ed70114669d907c29a62351ecbd09a89822e2 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Sat, 28 Sep 2024 10:49:38 +0200 Subject: [PATCH 37/85] We pass audit!!!! --- .../exporters/schematic/kicad/transformer.py | 282 ++++++++++++------ src/faebryk/libs/geometry/basic.py | 2 + 2 files changed, 192 insertions(+), 92 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 5a849115..4e43c628 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -11,6 +11,9 @@ from pathlib import Path from typing import Any, List, Protocol, Unpack +import rich +import rich.table + import faebryk.library._F as F from faebryk.core.graphinterface import Graph from faebryk.core.module import Module @@ -20,7 +23,6 @@ # from shapely import Polygon from faebryk.exporters.pcb.kicad.transformer import is_marked from faebryk.exporters.schematic.kicad.skidl import shims -from faebryk.exporters.schematic.kicad.skidl.geometry import BBox from faebryk.libs.exceptions import FaebrykException from faebryk.libs.geometry.basic import Geometry from faebryk.libs.kicad.fileformats import ( @@ -45,6 +47,7 @@ from faebryk.libs.sexp.dataclass_sexp import dataclass_dfs from faebryk.libs.util import ( FuncDict, + KeyErrorNotFound, cast_assert, find, not_none, @@ -150,14 +153,20 @@ def attach(self): logger.debug(f"Attached: {pprint.pformat(attached)}") logger.debug(f"Missing: {pprint.pformat(self.missing_symbols)}") - def attach_symbol(self, f_symbol: F.Symbol, sch_symbol: SCH.C_symbol_instance): + def attach_symbol(self, f_symbol: F.Symbol, sym_inst: SCH.C_symbol_instance): """Bind the module and symbol together on the graph""" - f_symbol.add(self.has_linked_sch_symbol(sch_symbol)) + f_symbol.add(self.has_linked_sch_symbol(sym_inst)) + + lib_sym = self._ensure_lib_symbol(sym_inst.lib_id) + lib_sym_units = self.get_sub_syms(lib_sym, sym_inst.unit) + lib_sym_pins = [p for u in lib_sym_units for p in u.pins] + pin_no_to_name = {str(pin.number.number): pin.name.name for pin in lib_sym_pins} # Attach the pins on the symbol to the module interface - for pin_name, pins in groupby(sch_symbol.pins, key=lambda p: p.name): + pin_by_name = groupby(sym_inst.pins, key=lambda p: pin_no_to_name[p.name]) + for pin_name, pins in pin_by_name: f_symbol.pins[pin_name].add( - Transformer.has_linked_sch_pins(pins, sch_symbol) + Transformer.has_linked_sch_pins(pins, sym_inst) ) # TODO: remove cleanup, it shouldn't really be required if we're marking propertys @@ -227,58 +236,72 @@ def get_symbol_file(self, lib_name: str) -> C_kicad_sym_file: return C_kicad_sym_file.loads(path) @staticmethod - def get_related_lib_sym_units( + def get_sub_syms( lib_sym: SCH.C_lib_symbols.C_symbol, - ) -> dict[int, list[SCH.C_lib_symbols.C_symbol.C_symbol]]: + unit: int | None, + body_style: int = 1, + ) -> list[SCH.C_lib_symbols.C_symbol.C_symbol]: """ - Figure out units. - This seems to be purely based on naming convention. - There are two suffixed numbers on the end eg. _0_0, _0_1 - They're in two sets of groups: - 1. subunit. used to represent graphical vs. pin objects within a unit - 2. unit. eg, a single op-amp in a package with 4 - We need to lump the subunits together for further processing. - - That is, we group them by the last number. + This is purely based on naming convention. + There are two suffixed numbers on the end: __, eg "LED_0_1" + The first number is the "unit" and the second is "body style" + Index 0 for either unit or body-style indicates "draw for all" + + References: + - ^1 Parser: + https://gitlab.com/kicad/code/kicad/-/blob/b043f334de6183595fda935175d2e2635daa379c/eeschema/sch_io/kicad_sexpr/sch_io_kicad_sexpr_parser.cpp#L455-476 + - ^2 Note on unit index meanings: + https://gitlab.com/kicad/code/kicad/-/blob/2c99bc6c6d0f548f590d4681e20868e8ddb5b9c7/eeschema/eeschema_jobs_handler.cpp#L702 """ - groups = groupby(lib_sym.symbols.items(), key=lambda item: int(item[0][-1])) - return {k: [v[1] for v in vs] for k, vs in groups} - @singledispatchmethod - def get_lib_symbol(self, sym) -> SCH.C_lib_symbols.C_symbol: - raise NotImplementedError(f"Don't know how to get lib symbol for {type(sym)}") + # kept body_style as an arg because I expect it will come up sooner than I like + # apparently body_style == 2 is comes from some option "de morgen?" + # don't need it now, but leaving this here for some poor sod later + if body_style != 1: + raise NotImplementedError("Only body style 1 is supported") - @get_lib_symbol.register - def _(self, sym: F.Symbol) -> SCH.C_lib_symbols.C_symbol: - lib_id = sym.get_trait(F.Symbol.has_kicad_symbol).symbol_name - return self._ensure_lib_symbol(lib_id) + sub_syms: list[SCH.C_lib_symbols.C_symbol.C_symbol] = [] + for name, sym in lib_sym.symbols.items(): + _, sub_sym_unit, sub_sym_body_style = name.split("_") + sub_sym_unit = int(sub_sym_unit) + sub_sym_body_style = int(sub_sym_body_style) - @get_lib_symbol.register - def _(self, sym: SCH.C_symbol_instance) -> SCH.C_lib_symbols.C_symbol: - return self.sch.lib_symbols.symbols[sym.lib_id] + if sub_sym_unit == unit or sub_sym_unit == 0 or unit is None: + if sub_sym_body_style == body_style or sub_sym_body_style == 0: + sub_syms.append(sym) - @singledispatchmethod - def get_lib_pin(self, pin) -> SCH.C_lib_symbols.C_symbol.C_symbol.C_pin: - raise NotImplementedError(f"Don't know how to get lib pin for {type(pin)}") - - @get_lib_pin.register - def _(self, pin: F.Symbol.Pin) -> SCH.C_lib_symbols.C_symbol.C_symbol.C_pin: - graph_symbol, _ = pin.get_parent() - assert isinstance(graph_symbol, Node) - lib_sym = self.get_lib_symbol(graph_symbol) - units = self.get_related_lib_sym_units(lib_sym) - sym = graph_symbol.get_trait(Transformer.has_linked_sch_symbol).symbol - - def _name_filter(sch_pin: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin): - return sch_pin.name in { - p.name for p in pin.get_trait(self.has_linked_sch_pins).pins - } - - lib_pin = find( - chain.from_iterable(u.pins for u in units[sym.unit]), - _name_filter, + return sub_syms + + @staticmethod + def get_unit_count(lib_sym: SCH.C_lib_symbols.C_symbol) -> int: + return max( + int(name.split("_")[1]) + for name in lib_sym.symbols.keys() ) - return lib_pin + + # TODO: remove + # @singledispatchmethod + # def get_lib_pin(self, pin) -> SCH.C_lib_symbols.C_symbol.C_symbol.C_pin: + # raise NotImplementedError(f"Don't know how to get lib pin for {type(pin)}") + + # @get_lib_pin.register + # def _(self, pin: F.Symbol.Pin) -> SCH.C_lib_symbols.C_symbol.C_symbol.C_pin: + # graph_symbol, _ = pin.get_parent() + # assert isinstance(graph_symbol, Node) + # lib_sym = self.get_lib_syms(graph_symbol) + # units = self.get_lib_syms(lib_sym) + # sym = graph_symbol.get_trait(Transformer.has_linked_sch_symbol).symbol + + # def _name_filter(sch_pin: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin): + # return sch_pin.name in { + # p.name for p in pin.get_trait(self.has_linked_sch_pins).pins + # } + + # lib_pin = find( + # chain.from_iterable(u.pins for u in units[sym.unit]), + # _name_filter, + # ) + # return lib_pin # Marking ------------------------------------------------------------------------- """ @@ -427,28 +450,29 @@ def insert_symbol( lib_sym = self._ensure_lib_symbol(lib_id) # insert all units - if len(self.get_related_lib_sym_units(lib_sym) > 1): + if self.get_unit_count(lib_sym) > 1: # problems today: # - F.Symbol -> Module mapping # - has_linked_sch_symbol mapping is currently 1:1 # - has_kicad_symbol mapping is currently 1:1 raise NotImplementedError("Multiple units not implemented") - for unit_key, unit_objs in self.get_related_lib_sym_units(lib_sym).items(): - pins = [] + for unit_key in range(self.get_unit_count(lib_sym)): + unit_objs = self.get_sub_syms(lib_sym, unit_key) + pins = [] for subunit in unit_objs: for pin in subunit.pins: pins.append( SCH.C_symbol_instance.C_pin( - name=pin.name.name, + name=pin.number.number, uuid=self.gen_uuid(mark=True), ) ) unit_instance = SCH.C_symbol_instance( lib_id=lib_id, - unit=unit_key + 1, # yes, these are indexed from 1... + unit=unit_key, at=C_xyr(at[0], at[1], rotation), in_bom=True, on_board=True, @@ -478,7 +502,7 @@ def insert_symbol( @singledispatchmethod @staticmethod - def get_bbox(obj) -> BoundingBox: + def get_bbox(obj) -> BoundingBox | None: """ Get the bounding box of the object in it's reference frame This means that for things like pins, which know their own position, @@ -490,15 +514,35 @@ def get_bbox(obj) -> BoundingBox: @staticmethod def _(obj: C_arc) -> BoundingBox: return Geometry.bbox( - Geometry.approximate_arc(obj.start, obj.mid, obj.end), + list(chain.from_iterable( + Geometry.approximate_arc( + (obj.start.x, obj.start.y), + (obj.mid.x, obj.mid.y), + (obj.end.x, obj.end.y), + ) + )), tolerance=obj.stroke.width, ) @get_bbox.register @staticmethod - def _(obj: C_polyline | C_rect) -> BoundingBox: + def _(obj: C_polyline) -> BoundingBox | None: + if len(obj.pts.xys) == 0: + return None + return Geometry.bbox( - ((pt.x, pt.y) for pt in obj.pts.xys), + [(pt.x, pt.y) for pt in obj.pts.xys], + tolerance=obj.stroke.width, + ) + + @get_bbox.register + @staticmethod + def _(obj: C_rect) -> BoundingBox | None: + return Geometry.bbox( + [ + (obj.start.x, obj.start.y), + (obj.end.x, obj.end.y), + ], tolerance=obj.stroke.width, ) @@ -513,43 +557,63 @@ def _(obj: C_circle) -> BoundingBox: ) @get_bbox.register - def _(self, pin: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin) -> BoundingBox: + @staticmethod + def _(obj: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin) -> BoundingBox: # TODO: include the name and number in the bbox - start = (pin.at.x, pin.at.y) - end = Geometry.rotate(start, [(pin.at.x + pin.length, pin.at.y)], pin.at.r)[0] + start = (obj.at.x, obj.at.y) + end = Geometry.rotate(start, [(obj.at.x + obj.length, obj.at.y)], obj.at.r)[0] return Geometry.bbox([start, end]) @get_bbox.register @classmethod - def _(cls, symbol: SCH.C_lib_symbols.C_symbol.C_symbol) -> BoundingBox: - return Geometry.bbox( - map( - cls.get_bbox, - chain( - symbol.arcs, - symbol.polylines, - symbol.circles, - symbol.rectangles, - symbol.pins, - ), - ) - ) + def _(cls, obj: SCH.C_lib_symbols.C_symbol.C_symbol) -> BoundingBox | None: + all_geos = list(chain( + obj.arcs, + obj.polylines, + obj.circles, + obj.rectangles, + obj.pins, + )) + + bboxes = [] + for geo in all_geos: + if (new_bboxes := cls.get_bbox(geo)) is not None: + bboxes.extend(new_bboxes) + + if len(bboxes) == 0: + return None + + return Geometry.bbox(bboxes) @get_bbox.register @classmethod - def _(cls, symbol: SCH.C_lib_symbols.C_symbol) -> BoundingBox: - return Geometry.bbox( - chain.from_iterable(cls.get_bbox(unit) for unit in symbol.symbols.values()) - ) + def _(cls, obj: SCH.C_lib_symbols.C_symbol) -> BoundingBox: + sub_points = list(chain.from_iterable( + bboxes + for unit in obj.symbols.values() + if (bboxes := cls.get_bbox(unit)) is not None + )) + assert len(sub_points) > 0 + return Geometry.bbox(sub_points) @get_bbox.register - def _(self, symbol: SCH.C_symbol_instance) -> BoundingBox: - # FIXME: this requires context to get the lib symbol, - # which means it must be called with self - return Geometry.abs_pos(self.get_bbox(self.get_lib_symbol(symbol))) + @classmethod + def _(cls, obj: list) -> BoundingBox: + return Geometry.bbox(list(chain.from_iterable(cls.get_bbox(item) for item in obj))) + + # TODO: remove + # @get_bbox.register + # def _(self, symbol: SCH.C_symbol_instance) -> BoundingBox: + # # FIXME: this requires context to get the lib symbol, + # # which means it must be called with self + # return Geometry.abs_pos( + # self.get_bbox(self.get_lib_syms(symbol)) + # ) def generate_schematic(self, **options: Unpack[shims.Options]): """Does what it says on the tin.""" + from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point + # 1. add missing symbols for f_symbol in self.missing_symbols: self.insert_symbol(f_symbol) @@ -557,6 +621,8 @@ def generate_schematic(self, **options: Unpack[shims.Options]): # 1.1 create hollow circuits to append to circuit = shims.Circuit() + circuit.parts = [] + circuit.nets = [] # 1.2 create maps to short-cut access between fab and sch sch_to_fab_pin_map: FuncDict[SCH.C_symbol_instance.C_pin, F.Symbol.Pin | None] = FuncDict() @@ -569,10 +635,11 @@ def generate_schematic(self, **options: Unpack[shims.Options]): ): sch_to_fab_sym_map[sch_sym_trait.symbol] = f_sym_trait.reference for sch_sym in self.sch.symbols: + f_symbol = sch_to_fab_sym_map.setdefault(sch_sym, None) for sch_pin in sch_sym.pins: - sch_to_fab_pin_map[sch_pin] = f_symbol.pins.get(sch_pin.name) - if sch_sym not in sch_to_fab_sym_map: - sch_to_fab_sym_map[sch_sym] = None + sch_to_fab_pin_map[sch_pin] = ( + f_symbol.pins.get(sch_pin.name) if f_symbol else None + ) # 2. create shim objects # 2.1 make nets @@ -616,24 +683,55 @@ def _best_name(module: Module) -> str: # for each sch_symbol, create a shim part for sch_sym, f_symbol in sch_to_fab_sym_map.items(): + lib_sym = self._ensure_lib_symbol(sch_sym.lib_id) + sch_lib_symbol_units = self.get_sub_syms(lib_sym, sch_sym.unit) shim_part = shims.Part() - shim_part.hierarchy = _hierarchy(f_symbol.represents) - shim_part.ref = f_symbol.represents.get_trait(F.has_overriden_name).get_name() + shim_part.ref = sch_sym.propertys["Reference"].value + # if we don't have a fab symbol, place the part at the top of the hierarchy + shim_part.hierarchy = _hierarchy(f_symbol.represents) if f_symbol else shim_part.ref # TODO: what's the ato analog? # TODO: should this desc shim_part.symtx = "" shim_part.unit = {} # TODO: support units shim_part.fab_symbol = f_symbol shim_part.bare_bbox = BBox( - Point(*pts) for pts in Transformer.get_bbox(f_symbol) + *[Point(*pts) for pts in Transformer.get_bbox(sch_lib_symbol_units)] ) + shim_part.pins = [] # 2.3 finish making pins, this time from a part-orientation - sch_lib_symbol = self.get_lib_symbol(sch_sym) - all_sch_units = list(sch_lib_symbol.symbols.values()) - all_sch_lib_pins = [p for u in all_sch_units for p in u.pins] + all_sch_lib_pins = [p for u in sch_lib_symbol_units for p in u.pins] + + # if logger.isEnabledFor(logging.DEBUG): # TODO: enable + rich.print(f"Symbol {sch_sym.propertys['Reference'].value=} {sch_sym.uuid=}") + pins = rich.table.Table("pin.name=", "pin.number=") + for pin in all_sch_lib_pins: + pins.add_row(pin.name.name, pin.number.number) + rich.print(pins) + for sch_pin in sch_sym.pins: - lib_sch_pin = find(all_sch_lib_pins, key=lambda x: x.name == sch_pin.name) + rich.print(f"Pin {sch_pin.name=}") + try: + lib_sch_pin = find( + all_sch_lib_pins, + lambda x: str(x.number.number) == str(sch_pin.name), + ) + except KeyErrorNotFound: + # KiCAD seems to make a full duplication of all the symbol objects + # despite not displaying them unless they're relevant to the current + # unit. Do our best to make sure it's at least a pin the symbol + # overall has (ignoring the unit) + lib_sym_pins_all_units = [ + p.number.number + for sym in self.get_sub_syms(lib_sym, None) + for p in sym.pins + ] + if sch_pin.name in lib_sym_pins_all_units: + continue + raise ValueError( + f"Pin {sch_pin.name} not found in any unit of symbol {sch_sym.name}" + ) + assert isinstance(lib_sch_pin, SCH.C_lib_symbols.C_symbol.C_symbol.C_pin) shim_pin = sch_to_shim_pin_map.setdefault(sch_pin, shims.Pin()) shim_pin.name = sch_pin.name diff --git a/src/faebryk/libs/geometry/basic.py b/src/faebryk/libs/geometry/basic.py index caf36eb9..bff140a0 100644 --- a/src/faebryk/libs/geometry/basic.py +++ b/src/faebryk/libs/geometry/basic.py @@ -601,6 +601,8 @@ def distance_euclid(p1: Point, p2: Point): def bbox( points: Sequence[Point | Point2D], tolerance=0.0 ) -> tuple[Point2D, Point2D]: + # points can't be a generator + assert len(points) > 0 min_x = min(p[0] for p in points) min_y = min(p[1] for p in points) max_x = max(p[0] for p in points) From ff9df92d753c648de9ab89f8fcfcc3b87d9424fe Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Sat, 28 Sep 2024 10:54:16 +0200 Subject: [PATCH 38/85] Format transformer.py --- .../exporters/schematic/kicad/transformer.py | 124 ++++++++++-------- 1 file changed, 72 insertions(+), 52 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 4e43c628..875f3194 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -146,9 +146,7 @@ def attach(self): # Log what we were able to attach attached = { n: t.symbol - for n, t in self.graph.nodes_with_trait( - Transformer.has_linked_sch_symbol - ) + for n, t in self.graph.nodes_with_trait(Transformer.has_linked_sch_symbol) } logger.debug(f"Attached: {pprint.pformat(attached)}") logger.debug(f"Missing: {pprint.pformat(self.missing_symbols)}") @@ -165,9 +163,7 @@ def attach_symbol(self, f_symbol: F.Symbol, sym_inst: SCH.C_symbol_instance): # Attach the pins on the symbol to the module interface pin_by_name = groupby(sym_inst.pins, key=lambda p: pin_no_to_name[p.name]) for pin_name, pins in pin_by_name: - f_symbol.pins[pin_name].add( - Transformer.has_linked_sch_pins(pins, sym_inst) - ) + f_symbol.pins[pin_name].add(Transformer.has_linked_sch_pins(pins, sym_inst)) # TODO: remove cleanup, it shouldn't really be required if we're marking propertys # def cleanup(self): @@ -221,9 +217,7 @@ def get_symbol(cmp: Node) -> F.Symbol: def get_all_symbols(self) -> List[tuple[Module, F.Symbol]]: return [ (cast_assert(Module, cmp), t.symbol) - for cmp, t in self.graph.nodes_with_trait( - Transformer.has_linked_sch_symbol - ) + for cmp, t in self.graph.nodes_with_trait(Transformer.has_linked_sch_symbol) ] @once @@ -274,10 +268,7 @@ def get_sub_syms( @staticmethod def get_unit_count(lib_sym: SCH.C_lib_symbols.C_symbol) -> int: - return max( - int(name.split("_")[1]) - for name in lib_sym.symbols.keys() - ) + return max(int(name.split("_")[1]) for name in lib_sym.symbols.keys()) # TODO: remove # @singledispatchmethod @@ -329,6 +320,7 @@ def is_uuid_marked(obj) -> bool: @staticmethod def hash_contents(obj) -> str: """Hash the contents of an object, minus the mark""" + # filter out mark properties def _filter(k: tuple[Any, list[Any], list[str]]) -> bool: obj, _, _ = k @@ -514,13 +506,15 @@ def get_bbox(obj) -> BoundingBox | None: @staticmethod def _(obj: C_arc) -> BoundingBox: return Geometry.bbox( - list(chain.from_iterable( - Geometry.approximate_arc( - (obj.start.x, obj.start.y), - (obj.mid.x, obj.mid.y), - (obj.end.x, obj.end.y), + list( + chain.from_iterable( + Geometry.approximate_arc( + (obj.start.x, obj.start.y), + (obj.mid.x, obj.mid.y), + (obj.end.x, obj.end.y), + ) ) - )), + ), tolerance=obj.stroke.width, ) @@ -567,13 +561,15 @@ def _(obj: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin) -> BoundingBox: @get_bbox.register @classmethod def _(cls, obj: SCH.C_lib_symbols.C_symbol.C_symbol) -> BoundingBox | None: - all_geos = list(chain( - obj.arcs, - obj.polylines, - obj.circles, - obj.rectangles, - obj.pins, - )) + all_geos = list( + chain( + obj.arcs, + obj.polylines, + obj.circles, + obj.rectangles, + obj.pins, + ) + ) bboxes = [] for geo in all_geos: @@ -588,45 +584,47 @@ def _(cls, obj: SCH.C_lib_symbols.C_symbol.C_symbol) -> BoundingBox | None: @get_bbox.register @classmethod def _(cls, obj: SCH.C_lib_symbols.C_symbol) -> BoundingBox: - sub_points = list(chain.from_iterable( - bboxes - for unit in obj.symbols.values() - if (bboxes := cls.get_bbox(unit)) is not None - )) + sub_points = list( + chain.from_iterable( + bboxes + for unit in obj.symbols.values() + if (bboxes := cls.get_bbox(unit)) is not None + ) + ) assert len(sub_points) > 0 return Geometry.bbox(sub_points) @get_bbox.register @classmethod def _(cls, obj: list) -> BoundingBox: - return Geometry.bbox(list(chain.from_iterable(cls.get_bbox(item) for item in obj))) - - # TODO: remove - # @get_bbox.register - # def _(self, symbol: SCH.C_symbol_instance) -> BoundingBox: - # # FIXME: this requires context to get the lib symbol, - # # which means it must be called with self - # return Geometry.abs_pos( - # self.get_bbox(self.get_lib_syms(symbol)) - # ) - - def generate_schematic(self, **options: Unpack[shims.Options]): - """Does what it says on the tin.""" - from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point + return Geometry.bbox( + list(chain.from_iterable(cls.get_bbox(item) for item in obj)) + ) - # 1. add missing symbols + def _add_missing_symbols(self): + """ + Add symbols to the schematic that are missing based on the fab graph + """ for f_symbol in self.missing_symbols: self.insert_symbol(f_symbol) self.missing_symbols = [] + def _build_shim_circuit(self) -> shims.Circuit: + """Does what it says on the tin.""" + from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point + # 1.1 create hollow circuits to append to circuit = shims.Circuit() circuit.parts = [] circuit.nets = [] # 1.2 create maps to short-cut access between fab and sch - sch_to_fab_pin_map: FuncDict[SCH.C_symbol_instance.C_pin, F.Symbol.Pin | None] = FuncDict() - sch_to_fab_sym_map: FuncDict[SCH.C_symbol_instance, F.Symbol | None] = FuncDict() + sch_to_fab_pin_map: FuncDict[ + SCH.C_symbol_instance.C_pin, F.Symbol.Pin | None + ] = FuncDict() + sch_to_fab_sym_map: FuncDict[SCH.C_symbol_instance, F.Symbol | None] = ( + FuncDict() + ) # for each sch_symbol / (fab_symbol | None) pair, create a shim part # we need to shim sym object which aren't even in the graph to avoid colliding for _, f_sym_trait in self.graph.nodes_with_trait(F.Symbol.has_symbol): @@ -643,7 +641,9 @@ def generate_schematic(self, **options: Unpack[shims.Options]): # 2. create shim objects # 2.1 make nets - sch_to_shim_pin_map: FuncDict[SCH.C_symbol_instance.C_pin, shims.Pin] = FuncDict() + sch_to_shim_pin_map: FuncDict[SCH.C_symbol_instance.C_pin, shims.Pin] = ( + FuncDict() + ) fab_nets = self.graph.nodes_of_type(F.Net) for net in fab_nets: shim_net = shims.Net() @@ -672,6 +672,7 @@ def _hierarchy(module: Module) -> str: Make a string representation of the module's hierarchy using the best name for each part we have """ + def _best_name(module: Module) -> str: if name_trait := module.try_get_trait(F.has_overriden_name): return name_trait.get_name() @@ -688,7 +689,9 @@ def _best_name(module: Module) -> str: shim_part = shims.Part() shim_part.ref = sch_sym.propertys["Reference"].value # if we don't have a fab symbol, place the part at the top of the hierarchy - shim_part.hierarchy = _hierarchy(f_symbol.represents) if f_symbol else shim_part.ref + shim_part.hierarchy = ( + _hierarchy(f_symbol.represents) if f_symbol else shim_part.ref + ) # TODO: what's the ato analog? # TODO: should this desc shim_part.symtx = "" @@ -703,7 +706,9 @@ def _best_name(module: Module) -> str: all_sch_lib_pins = [p for u in sch_lib_symbol_units for p in u.pins] # if logger.isEnabledFor(logging.DEBUG): # TODO: enable - rich.print(f"Symbol {sch_sym.propertys['Reference'].value=} {sch_sym.uuid=}") + rich.print( + f"Symbol {sch_sym.propertys['Reference'].value=} {sch_sym.uuid=}" + ) pins = rich.table.Table("pin.name=", "pin.number=") for pin in all_sch_lib_pins: pins.add_row(pin.name.name, pin.number.number) @@ -732,7 +737,9 @@ def _best_name(module: Module) -> str: f"Pin {sch_pin.name} not found in any unit of symbol {sch_sym.name}" ) - assert isinstance(lib_sch_pin, SCH.C_lib_symbols.C_symbol.C_symbol.C_pin) + assert isinstance( + lib_sch_pin, SCH.C_lib_symbols.C_symbol.C_symbol.C_pin + ) shim_pin = sch_to_shim_pin_map.setdefault(sch_pin, shims.Pin()) shim_pin.name = sch_pin.name shim_pin.num = lib_sch_pin.number @@ -755,6 +762,19 @@ def _best_name(module: Module) -> str: # 2.-1 run audit on circuit circuit.audit() + return circuit + + def generate_schematic(self, **options: Unpack[shims.Options]): + """Does what it says on the tin.""" + # 1. add missing symbols + self._add_missing_symbols() + + # 2. build shim circuit + circuit = self._build_shim_circuit() # 3. run skidl schematic generation + from faebryk.exporters.schematic.kicad.skidl.gen_schematic import gen_schematic + gen_schematic(circuit, **options) + # 4. transform sch according to skidl + # TODO: From 6d5a61306e718781050a522253d8770e4556c205 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Mon, 30 Sep 2024 15:51:48 +0200 Subject: [PATCH 39/85] Progress on auto-sch layout generator. Basically runs, but there are a function or two I missed implementing --- dev_schematics.py | 55 +++++++++++++++++++ .../schematic/kicad/skidl/debug_draw.py | 6 +- .../schematic/kicad/skidl/gen_schematic.py | 6 +- .../exporters/schematic/kicad/skidl/place.py | 7 ++- .../exporters/schematic/kicad/skidl/route.py | 18 +++--- .../exporters/schematic/kicad/skidl/shims.py | 11 ++-- .../exporters/schematic/kicad/transformer.py | 8 ++- 7 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 dev_schematics.py diff --git a/dev_schematics.py b/dev_schematics.py new file mode 100644 index 00000000..b38fa499 --- /dev/null +++ b/dev_schematics.py @@ -0,0 +1,55 @@ +import contextlib +import sys +from pathlib import Path + +import faebryk.library._F as F +from faebryk.core.module import Module +from faebryk.exporters.schematic.kicad.transformer import Transformer +from faebryk.libs.kicad.fileformats_sch import C_kicad_sch_file + +root_dir = Path(__file__).parent +test_dir = root_dir / "test" + +fp_lib_path_path = test_dir / "common/resources/fp-lib-table" +sch_file = C_kicad_sch_file.loads(test_dir / "common/resources/test.kicad_sch") + + +@contextlib.contextmanager +def add_to_sys_path(path): + sys.path.append(str(path)) + yield + sys.path.remove(str(path)) + + +# with add_to_sys_path(root_dir / "examples"): +# from minimal_led import App +# app = App() +# assert isinstance(app, Module) + +app = Module() + +full_transformer = Transformer(sch_file.kicad_sch, app.get_graph(), app) +full_transformer.index_symbol_files(fp_lib_path_path, load_globals=False) + +# mimicing typically design/user-space +audio_jack = full_transformer.app.add(Module()) +pin_s = audio_jack.add(F.Electrical()) +pin_t = audio_jack.add(F.Electrical()) +pin_r = audio_jack.add(F.Electrical()) +audio_jack.add(F.has_overriden_name_defined("U1")) + +# mimicing typically lcsc code +sym = F.Symbol.with_component( + audio_jack, + { + "S": pin_s, + "T": pin_t, + "R": pin_r, + }, +) +audio_jack.add(F.Symbol.has_symbol(sym)) +sym.add(F.Symbol.has_kicad_symbol("test:AudioJack-CUI-SJ-3523-SMT")) + +full_transformer.insert_symbol(audio_jack) + +full_transformer.generate_schematic() diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index bb61b90b..5b7bc1f7 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -79,7 +79,7 @@ def draw_endpoint(pt: Point, scr: "pygame.Surface", tx: Tx, color=(100, 100, 100 pygame.draw.circle(scr, color, (pt.x, pt.y), radius) def draw_seg( - seg: Segment | "Interval" | "NetInterval", + seg: "Segment | Interval | NetInterval", scr: "pygame.Surface", tx: Tx, color=(100, 100, 100), @@ -145,7 +145,7 @@ def draw_text( # syntax highlighting doesn't recognise this font.render_to(scr, (pt.x, pt.y), txt, color) -def draw_part(part: Part, scr: "pygame.Surface", tx: Tx, font: "pygame.font.Font"): +def draw_part(part: "Part", scr: "pygame.Surface", tx: Tx, font: "pygame.font.Font"): """Draw part bounding box. Args: @@ -208,7 +208,7 @@ def draw_net( ) def draw_force( - part: Part, + part: "Part", force: Vector, scr: "pygame.Surface", tx: Tx, diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index e18ae0e5..ce8fa3d6 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -20,7 +20,7 @@ from .geometry import BBox, Point, Tx, Vector from .net_terminal import NetTerminal from .node import HIER_SEP, SchNode -from .shims import Circuit, Options, Part, PartUnit, Pin, get_script_name, rmv_attr +from .shims import Circuit, Options, Part, PartUnit, Pin, rmv_attr def bbox_to_eeschema(bbox: BBox, tx: Tx, name=None): @@ -671,8 +671,8 @@ def finalize_parts_and_nets(circuit: Circuit, **options: Unpack[Options]): def gen_schematic( circuit: Circuit, - filepath: str = ".", - top_name: str = get_script_name(), + filepath: str, + top_name: str, title="SKiDL-Generated Schematic", flatness: float = 0.0, retries: int = 2, diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index d8ac2d7e..c169499e 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -84,7 +84,7 @@ class PartBlock: def __init__( self, - src: list[Part | "SchNode"] | Part | "SchNode", + src: "list[Part | SchNode] | Part | SchNode", bbox: BBox, anchor_pt: Point, snap_pt: Point, @@ -1297,8 +1297,9 @@ def place_floating_parts(node: "SchNode", parts: list[Part], **options: Unpack[O continue # HACK: Get similarity forces right-sized. - part_similarity[part][other_part] = part.similarity(other_part) / 100 - # part_similarity[part][other_part] = 0.1 + # TODO: @mawildoer put this back + # part_similarity[part][other_part] = part.similarity(other_part) / 100 + part_similarity[part][other_part] = 0.1 # Select the top-most pin in each part as the anchor point for force-directed placement. # tx = part.tx diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index a26daf02..6280b688 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -694,7 +694,7 @@ def seg(self) -> Segment: def draw( self, - scr: Surface, + scr: "Surface", tx: Tx, font, color: tuple[int, int, int] = (128, 128, 128), @@ -786,7 +786,7 @@ def cvt_faces_to_terminals(self) -> None: def draw( self, - scr: Surface, + scr: "Surface", tx: Tx, color: tuple[int, int, int] = (0, 0, 0), thickness: int = 1, @@ -847,7 +847,7 @@ def cvt_faces_to_terminals(self) -> None: def draw( self, - scr: Surface, + scr: "Surface", tx: Tx, font, color: tuple[int, int, int] = (0, 0, 0), @@ -1021,7 +1021,7 @@ def audit(self) -> None: def draw( self, - scr: Surface, + scr: "Surface", tx: Tx, font, **options: Unpack[Options], @@ -1313,7 +1313,7 @@ def flip_xy(self) -> None: for seg in segments: seg.flip_xy() - def coalesce(self, switchboxes: list["SwitchBox"]) -> "SwitchBox" | None: + def coalesce(self, switchboxes: list["SwitchBox"]) -> "SwitchBox | None": """Group switchboxes around a seed switchbox into a larger switchbox. Args: @@ -2031,7 +2031,7 @@ def trim_column_intervals( def draw( self, - scr: Surface | None = None, + scr: "Surface | None" = None, tx: Tx | None = None, font = None, color: tuple[int, int, int] = (128, 0, 128), @@ -3169,7 +3169,7 @@ def get_jogs(segments: list[Segment]) -> Iterator[tuple[list[Segment], list[Poin # Update the node net's wire with the cleaned version. node.wires[net] = segments - def add_junctions(node: SchNode): + def add_junctions(node: "SchNode"): """Add X & T-junctions where wire segments in the same net meet.""" def find_junctions(route: list[Segment]) -> list[Point]: @@ -3217,14 +3217,14 @@ def find_junctions(route: list[Segment]) -> list[Point]: junctions = find_junctions(segments) node.junctions[net].extend(junctions) - def rmv_routing_stuff(node: SchNode): + def rmv_routing_stuff(node: "SchNode"): """Remove attributes added to parts/pins during routing.""" rmv_attr(node.parts, ("left_track", "right_track", "top_track", "bottom_track")) for part in node.parts: rmv_attr(part.pins, ("route_pt", "face")) - def route(node: SchNode, **options: Unpack[Options]): + def route(node: "SchNode", **options: Unpack[Options]): """Route the wires between part pins in this node and its children. Steps: diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 43ef8b7e..fbd2d47d 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -19,11 +19,6 @@ from .route import Face, GlobalTrack -def get_script_name(): - # TODO: - raise NotImplementedError - - def to_list(x): """ Return x if it is already a list, or return a list containing x if x is a scalar. @@ -115,6 +110,9 @@ def audit(self) -> None: def __iter__(self) -> Iterator["Pin"]: yield from self.pins + def __len__(self) -> int: + return len(self.pins) + @property def draw(self): # TODO: @@ -171,6 +169,7 @@ class Pin: fab_pin: F.Symbol.Pin fab_is_gnd: bool fab_is_pwr: bool + _is_connected: bool def audit(self) -> None: """Ensure mandatory attributes are set""" @@ -200,7 +199,7 @@ def audit(self) -> None: def is_connected(self) -> bool: """Whether the pin is connected to anything""" - raise NotImplementedError + return getattr(self, "_is_connected", False) class Net: diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 875f3194..858fabc9 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -664,6 +664,11 @@ def _build_shim_circuit(self) -> shims.Circuit: shim_net.pins.append(shim_pin) sch_to_shim_pin_map[sch_pin] = shim_pin + # set is_connected for all pins on net if len(net.pins) > 0 + is_connected = len(shim_net.pins) > 0 + for pin in shim_net.pins: + pin._is_connected = is_connected + circuit.nets.append(shim_net) # 2.2 make parts @@ -774,7 +779,8 @@ def generate_schematic(self, **options: Unpack[shims.Options]): # 3. run skidl schematic generation from faebryk.exporters.schematic.kicad.skidl.gen_schematic import gen_schematic - gen_schematic(circuit, **options) + gen_schematic(circuit, ".", "test", **options) # 4. transform sch according to skidl # TODO: + pass From c2b08797b1d5a26d390b7ee8c320c620dc435298 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 1 Oct 2024 10:49:31 +0200 Subject: [PATCH 40/85] Auto-place/route runs to completion --- .../exporters/schematic/kicad/skidl/place.py | 24 ++++++-- .../exporters/schematic/kicad/skidl/shims.py | 10 ++++ .../exporters/schematic/kicad/transformer.py | 59 ++++++++++++++++++- 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index c169499e..cda963dc 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -271,7 +271,18 @@ def add_place_pt(part: Part, pin: Pin): except ValueError: # Set anchor for part with no pins at all. anchor_pull_pin = Pin() + + # FAEBRYK: manual construction of the part from here because + anchor_pull_pin.part = part + anchor_pull_pin.name = "ANCHOR_PULL" + anchor_pull_pin.num = "ANCHOR_PULL" + anchor_pull_pin.orientation = "top" + anchor_pull_pin.stub = True + anchor_pull_pin.pt = part.place_bbox.max + part.pins.append(anchor_pull_pin) + anchor_pull_pin.place_pt = part.place_bbox.max + part.anchor_pins["similarity"] = [anchor_pull_pin] part.pull_pins["similarity"] = all_pull_pins all_pull_pins.append(anchor_pull_pin) @@ -1152,7 +1163,9 @@ def dist_to_bbox_edge(term: "NetTerminal"): class Placer: """Mixin to add place function to Node class.""" - def group_parts(node: "SchNode", **options: Unpack[Options]): + def group_parts( + node: "SchNode", **options: Unpack[Options] + ) -> tuple[list[list[Part]], list[Net], list[Part]]: """Group parts in the Node that are connected by internal nets Args: @@ -1193,10 +1206,10 @@ def group_parts(node: "SchNode", **options: Unpack[Options]): break # Remove any empty groups that were unioned into other groups. - connected_parts = [group for group in connected_parts if group] + connected_parts = [list(group) for group in connected_parts if group] # Find parts that aren't connected to anything. - floating_parts = set(node.parts) - set(itertools.chain(*connected_parts)) + floating_parts = list(set(node.parts) - set(itertools.chain(*connected_parts))) return connected_parts, internal_nets, floating_parts @@ -1297,9 +1310,8 @@ def place_floating_parts(node: "SchNode", parts: list[Part], **options: Unpack[O continue # HACK: Get similarity forces right-sized. - # TODO: @mawildoer put this back - # part_similarity[part][other_part] = part.similarity(other_part) / 100 - part_similarity[part][other_part] = 0.1 + part_similarity[part][other_part] = part.similarity(other_part) / 100 + # part_similarity[part][other_part] = 0.1 # Select the top-most pin in each part as the anchor point for force-directed placement. # tx = part.tx diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index fbd2d47d..00a8d04f 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -66,6 +66,7 @@ class Part: sch_symbol: "fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance" fab_symbol: F.Symbol | None bare_bbox: BBox + _similarites: dict["Part", float] def audit(self) -> None: """Ensure mandatory attributes are set""" @@ -77,6 +78,7 @@ def audit(self) -> None: "unit", "fab_symbol", "bare_bbox", + "_similarites", ]: if not hasattr(self, attr): raise ValueError(f"Missing attribute: {attr}") @@ -113,6 +115,10 @@ def __iter__(self) -> Iterator["Pin"]: def __len__(self) -> int: return len(self.pins) + def __hash__(self) -> int: + """Make hashable for use in dicts""" + return id(self) + @property def draw(self): # TODO: @@ -124,6 +130,10 @@ def grab_pins(self) -> None: for pin in self.pins: pin.part = self + def similarity(self, part: "Part", **options) -> float: + assert not options, "No options supported" + return self._similarites[part] + class PartUnit(Part): # TODO: represent these in Faebryk diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 858fabc9..50f5b6d6 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -765,6 +765,64 @@ def _best_name(module: Module) -> str: circuit.parts.append(shim_part) + # 2.4 generate similarity matrix + def similarity(part: "shims.Part", other: "shims.Part", **options) -> float: + """ + NOTE: Straight outta skidl + Return a measure of how similar two parts are. + + Args: + part (Part): The part to compare to for similarity. + options (dict): Dictionary of options and settings affecting + similarity computation. + + Returns: + Float value for similarity (larger means more similar). + """ + + def score_pins(): + pin_score = 0 + if len(part.pins) == len(other.pins): + for p_self, p_other in zip(part.ordered_pins, other.ordered_pins): + if p_self.is_attached(p_other): + pin_score += 1 + return pin_score + + # Every part starts off somewhat similar to another. + score = 1 + + if part.description == other.description: + score += 5 + if part.name == other.name: + score += 5 + if part.value == other.value: + score += 2 + score += score_pins() + elif part.ref_prefix == other.ref_prefix: + score += 3 + if part.value == other.value: + score += 2 + score += score_pins() + + return score / 3 + + similarities: dict[tuple[int, int], float] = {} + for part in circuit.parts: + part._similarites = {} + + for other in circuit.parts: + if part is other: + continue + + key = tuple(sorted((hash(part), hash(other)))) + + if key not in similarities: + # TODO: actually compute similarity + # similarities[key] = similarity(part, other) + similarities[key] = 10 + + part._similarites[other] = similarities[key] + # 2.-1 run audit on circuit circuit.audit() return circuit @@ -783,4 +841,3 @@ def generate_schematic(self, **options: Unpack[shims.Options]): # 4. transform sch according to skidl # TODO: - pass From 2cb6c81d59d10166474b12eec82ed7445de305ae Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 1 Oct 2024 10:51:19 +0200 Subject: [PATCH 41/85] Format place and route --- .../exporters/schematic/kicad/skidl/place.py | 98 ++++++++++++++----- .../exporters/schematic/kicad/skidl/route.py | 28 +++--- 2 files changed, 87 insertions(+), 39 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index cda963dc..6d1843de 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -70,6 +70,7 @@ class PlacementFailure(Exception): def pt_sum(pts): return sum(pts, Point(0, 0)) + def force_sum(forces): return sum(forces, Vector(0, 0)) @@ -79,6 +80,7 @@ def is_net_terminal(part): return isinstance(part, NetTerminal) + # Class for movable groups of parts/child nodes. class PartBlock: @@ -92,7 +94,9 @@ def __init__( block_pull_pins: dict[str, list[Pin]], ): self.src = src # Source for this block. - self.place_bbox = bbox # FIXME: Is this needed if place_bbox includes room for routing? + self.place_bbox = ( + bbox # FIXME: Is this needed if place_bbox includes room for routing? + ) # Create anchor pin to which forces are applied to this block. anchor_pin = Pin() @@ -196,7 +200,9 @@ def get_enclosing_bbox(parts: list[Part]) -> BBox: return BBox().add(*(part.place_bbox * part.tx for part in parts)) -def add_anchor_pull_pins(parts: list[Part], nets: list[Net], **options: Unpack[Options]): +def add_anchor_pull_pins( + parts: list[Part], nets: list[Net], **options: Unpack[Options] +): """Add positions of anchor and pull pins for attractive net forces between parts. Args: @@ -394,7 +400,9 @@ def find_best_orientation(part: Part) -> None: # Find the point at which the cost reaches its lowest point. # delta_cost at location i is the change in cost *before* part i is moved. # Start with cost change of zero before any parts are moved. - delta_costs = [0,] + delta_costs = [ + 0, + ] delta_costs.extend((part.delta_cost for part in moved_parts)) cost_seq = list(itertools.accumulate(delta_costs)) min_cost = min(cost_seq) @@ -609,12 +617,23 @@ def overlap_force(part: Part, parts: list[Part], **options: Unpack[Options]) -> # Add some small random offset to break symmetry when parts exactly overlay each other. # Move right edge of part to the left of other part's left edge, etc... moves: list[list[float, Vector]] = [] - rnd = Vector(random.random()-0.5, random.random()-0.5) - for edges, dir in ((("ll", "lr"), Vector(1,0)), (("ul", "ll"), Vector(0,1))): - move: Vector = (getattr(other_part_bbox, edges[0]) - getattr(part_bbox, edges[1]) - rnd) * dir + rnd = Vector(random.random() - 0.5, random.random() - 0.5) + for edges, dir in ( + (("ll", "lr"), Vector(1, 0)), + (("ul", "ll"), Vector(0, 1)), + ): + move: Vector = ( + getattr(other_part_bbox, edges[0]) + - getattr(part_bbox, edges[1]) + - rnd + ) * dir moves.append([move.magnitude, move]) # Flip edges... - move = (getattr(other_part_bbox, edges[1]) - getattr(part_bbox, edges[0]) - rnd) * dir + move = ( + getattr(other_part_bbox, edges[1]) + - getattr(part_bbox, edges[0]) + - rnd + ) * dir moves.append([move.magnitude, move]) # Select the smallest move that separates the parts. @@ -626,7 +645,9 @@ def overlap_force(part: Part, parts: list[Part], **options: Unpack[Options]) -> return total_force -def overlap_force_rand(part: Part, parts: list[Part], **options: Unpack[Options]) -> Vector: +def overlap_force_rand( + part: Part, parts: list[Part], **options: Unpack[Options] +) -> Vector: """Compute the repulsive force on a part from overlapping other parts. Args: @@ -652,14 +673,18 @@ def overlap_force_rand(part: Part, parts: list[Part], **options: Unpack[Options] # Add some small random offset to break symmetry when parts exactly overlay each other. # Move right edge of part to the left of other part's left edge. moves = [] - rnd = Vector(random.random()-0.5, random.random()-0.5) + rnd = Vector(random.random() - 0.5, random.random() - 0.5) for edges, dir in ( (("ll", "lr"), Vector(1, 0)), (("lr", "ll"), Vector(1, 0)), (("ul", "ll"), Vector(0, 1)), (("ll", "ul"), Vector(0, 1)), ): - move: Vector = (getattr(other_part_bbox, edges[0]) - getattr(part_bbox, edges[1]) - rnd) * dir + move: Vector = ( + getattr(other_part_bbox, edges[0]) + - getattr(part_bbox, edges[1]) + - rnd + ) * dir moves.append([move.magnitude, move]) accum = 0 for move in moves: @@ -685,7 +710,9 @@ def overlap_force_rand(part: Part, parts: list[Part], **options: Unpack[Options] def scale_attractive_repulsive_forces( - parts: list[Part], force_func: Callable[[Part, list[Part], ...], Vector], **options: Unpack[Options] + parts: list[Part], + force_func: Callable[[Part, list[Part], ...], Vector], + **options: Unpack[Options] ) -> float: """Set scaling between attractive net forces and repulsive part overlap forces.""" @@ -719,7 +746,11 @@ def scale_attractive_repulsive_forces( def total_part_force( - part: Part, parts: list[Part], scale: float, alpha: float, **options: Unpack[Options] + part: Part, + parts: list[Part], + scale: float, + alpha: float, + **options: Unpack[Options] ) -> Vector: """Compute the total of the attractive net and repulsive overlap forces on a part. @@ -768,7 +799,12 @@ def similarity_force( def total_similarity_force( - part: Part, parts: list[Part], similarity: dict, scale: float, alpha: float, **options: Unpack[Options] + part: Part, + parts: list[Part], + similarity: dict, + scale: float, + alpha: float, + **options: Unpack[Options] ) -> Vector: """Compute the total of the attractive similarity and repulsive overlap forces on a part. @@ -1075,7 +1111,9 @@ def orient(terminals: list["NetTerminal"], bbox: BBox): # Right side, so terminal label flipped to jut out to the right. insets.append((abs(pull_pt.x - bbox.lr.x), Tx().flip_x())) # Top side, so terminal label rotated by 270 to jut out to the top. - insets.append((abs(pull_pt.y - bbox.ul.y), Tx().rot_90cw().rot_90cw().rot_90cw())) + insets.append( + (abs(pull_pt.y - bbox.ul.y), Tx().rot_90cw().rot_90cw().rot_90cw()) + ) # Bottom side. so terminal label rotated 90 to jut out to the bottom. insets.append((abs(pull_pt.y - bbox.ll.y), Tx().rot_90cw())) @@ -1091,7 +1129,9 @@ def move_to_pull_pin(terminals: list["NetTerminal"]): pull_pt = pull_pin.place_pt * pull_pin.part.tx terminal.tx = terminal.tx.move(pull_pt - anchor_pt) - def evolution(net_terminals: list["NetTerminal"], placed_parts: list[Part], bbox: BBox): + def evolution( + net_terminals: list["NetTerminal"], placed_parts: list[Part], bbox: BBox + ): """Evolve placement of NetTerminals starting from outermost from center to innermost.""" evolution_type = options.get("terminal_evolution", "all_at_once") @@ -1108,11 +1148,13 @@ def evolution(net_terminals: list["NetTerminal"], placed_parts: list[Part], bbox # Sort terminals from outermost to innermost w.r.t. the center. def dist_to_bbox_edge(term: "NetTerminal"): pt = term.pins[0].place_pt * term.tx - return min(( - abs(pt.x - bbox.ll.x), - abs(pt.x - bbox.lr.x), - abs(pt.y - bbox.ll.y), - abs(pt.y - bbox.ul.y)) + return min( + ( + abs(pt.x - bbox.ll.x), + abs(pt.x - bbox.lr.x), + abs(pt.y - bbox.ll.y), + abs(pt.y - bbox.ul.y), + ) ) terminals = sorted( @@ -1213,7 +1255,9 @@ def group_parts( return connected_parts, internal_nets, floating_parts - def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], **options): + def place_connected_parts( + node: "SchNode", parts: list[Part], nets: list[Net], **options + ): """Place individual parts. Args: @@ -1271,7 +1315,9 @@ def place_connected_parts(node: "SchNode", parts: list[Part], nets: list[Net], * # Pause to look at placement for debugging purposes. draw_pause() - def place_floating_parts(node: "SchNode", parts: list[Part], **options: Unpack[Options]): + def place_floating_parts( + node: "SchNode", parts: list[Part], **options: Unpack[Options] + ): """Place individual parts. Args: @@ -1378,7 +1424,9 @@ def place_blocks( bbox = bbox.resize(Vector(pad, pad)) # Create the part block and place it on the list. - part_blocks.append(PartBlock(part_list, bbox, bbox.ctr, snap_pt, tag, block_pull_pins)) + part_blocks.append( + PartBlock(part_list, bbox, bbox.ctr, snap_pt, tag, block_pull_pins) + ) # Add part blocks for child nodes. for child in children: @@ -1407,7 +1455,9 @@ def place_blocks( tag = 4 # A child node with no snapping point. # Create the child block and place it on the list. - part_blocks.append(PartBlock(child, bbox, bbox.ctr, snap_pt, tag, block_pull_pins)) + part_blocks.append( + PartBlock(child, bbox, bbox.ctr, snap_pt, tag, block_pull_pins) + ) # Get ordered list of all block tags. Use this list to tell if tags are # adjacent since there may be missing tags if a particular type of block diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index 6280b688..5a0b79fc 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -1594,9 +1594,7 @@ def find_connection( # Return connection segments. return column_intvls - def prune_targets( - targets: list["Target"], current_col: int - ) -> list["Target"]: + def prune_targets(targets: list["Target"], current_col: int) -> list["Target"]: """Remove targets in columns to the left of the current left-to-right routing column""" return [target for target in targets if target.col > current_col] @@ -1611,9 +1609,7 @@ def insert_column_nets( nets[intvl.end] = intvl.net return nets - def net_search( - net: "Net", start: int, track_nets: list["Net"] - ) -> int: + def net_search(net: "Net", start: int, track_nets: list["Net"]) -> int: """Search for the closest points for the net before and after the start point.""" # illegal offset past the end of the list of track nets. @@ -2033,7 +2029,7 @@ def draw( self, scr: "Surface | None" = None, tx: Tx | None = None, - font = None, + font=None, color: tuple[int, int, int] = (128, 0, 128), thickness: int = 2, **options: Unpack[Options], @@ -2311,9 +2307,7 @@ def global_router(node: "SchNode", nets: list[Net]) -> list[GlobalRoute]: # d. Add the faces on the new route to the stop_faces list. # Core routing function. - def rt_srch( - start_face: Face, stop_faces: list[Face] - ) -> GlobalWire: + def rt_srch(start_face: Face, stop_faces: list[Face]) -> GlobalWire: """Return a minimal-distance path from the start face to one of the stop faces. Args: @@ -2771,7 +2765,9 @@ def remove_jogs_old( part_bboxes (list): List of BBoxes for the placed parts. """ - def get_touching_segs(seg: Segment, ortho_segs: list[Segment]) -> list[Segment]: + def get_touching_segs( + seg: Segment, ortho_segs: list[Segment] + ) -> list[Segment]: """Return list of orthogonal segments that touch the given segment.""" touch_segs = set() for oseg in ortho_segs: @@ -3009,7 +3005,9 @@ def get_corners(segments: list[Segment]) -> dict[Point, list[Segment]]: } return corners - def get_jogs(segments: list[Segment]) -> Iterator[tuple[list[Segment], list[Point]]]: + def get_jogs( + segments: list[Segment], + ) -> Iterator[tuple[list[Segment], list[Point]]]: """Yield the three segments and starting and end points of a staircase or tophat jog.""" # Get dict of right-angle corners formed by segments. @@ -3298,7 +3296,7 @@ def route(node: "SchNode", **options: Unpack[Options]): h_tracks, v_tracks, global_routes, - **options + **options, ) # Create detailed wiring using switchbox routing for the global routes. @@ -3312,7 +3310,7 @@ def route(node: "SchNode", **options: Unpack[Options]): node.parts, switchboxes, global_routes, - **options + **options, ) node.switchbox_router(switchboxes, **options) @@ -3325,7 +3323,7 @@ def route(node: "SchNode", **options: Unpack[Options]): node.parts, global_routes, switchboxes, - **options + **options, ) # Now clean-up the wires and add junctions. From e11eaebf6f747ff2361b2088aeb44becc664de9d Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 1 Oct 2024 10:54:08 +0200 Subject: [PATCH 42/85] Print tables only when debugging --- .../exporters/schematic/kicad/transformer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 50f5b6d6..b1f7e15f 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -710,14 +710,14 @@ def _best_name(module: Module) -> str: # 2.3 finish making pins, this time from a part-orientation all_sch_lib_pins = [p for u in sch_lib_symbol_units for p in u.pins] - # if logger.isEnabledFor(logging.DEBUG): # TODO: enable - rich.print( - f"Symbol {sch_sym.propertys['Reference'].value=} {sch_sym.uuid=}" - ) - pins = rich.table.Table("pin.name=", "pin.number=") - for pin in all_sch_lib_pins: - pins.add_row(pin.name.name, pin.number.number) - rich.print(pins) + if logger.isEnabledFor(logging.DEBUG): + rich.print( + f"Symbol {sch_sym.propertys['Reference'].value=} {sch_sym.uuid=}" + ) + pins = rich.table.Table("pin.name=", "pin.number=") + for pin in all_sch_lib_pins: + pins.add_row(pin.name.name, pin.number.number) + rich.print(pins) for sch_pin in sch_sym.pins: rich.print(f"Pin {sch_pin.name=}") From be7d0199e28fb71612d91fc9084fe3f3966dc948 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 2 Oct 2024 16:06:45 +0200 Subject: [PATCH 43/85] Add interface to Pad. Seems simpler with a reference, but I don't wanna get into it --- src/faebryk/library/Pad.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/faebryk/library/Pad.py b/src/faebryk/library/Pad.py index 63636e60..6fc493c6 100644 --- a/src/faebryk/library/Pad.py +++ b/src/faebryk/library/Pad.py @@ -4,10 +4,12 @@ import faebryk.library._F as F from faebryk.core.moduleinterface import ModuleInterface +from faebryk.core.node import NodeException from faebryk.libs.util import not_none class Pad(ModuleInterface): + # FIXME: can net this become a reference instead? net: F.Electrical pcb: ModuleInterface @@ -15,6 +17,15 @@ def attach(self, intf: F.Electrical): self.net.connect(intf) intf.add(F.has_linked_pad_defined(self)) + @property + def interface(self) -> F.Electrical: + connections = self.net.get_direct_connections() + if len(connections) == 0: + raise NodeException(self, f"Pad {self} has no interface connected") + if len(connections) > 1: + raise NodeException(self, f"Pad {self} has multiple interfaces connected") + return list(connections)[0] + @staticmethod def find_pad_for_intf_with_parent_that_has_footprint_unique( intf: ModuleInterface, From df44fb1b04689d242448990e1925b46ebaee2b20 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 2 Oct 2024 16:20:41 +0200 Subject: [PATCH 44/85] Add skeleton helpers to fileformats_sch --- .../exporters/schematic/kicad/transformer.py | 35 +-- src/faebryk/libs/kicad/fileformats_sch.py | 216 ++++++++++-------- 2 files changed, 140 insertions(+), 111 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index b1f7e15f..bb35f04f 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -38,6 +38,7 @@ C_circle, C_kicad_sch_file, C_kicad_sym_file, + C_lib_symbol, C_polyline, C_property, C_rect, @@ -201,6 +202,12 @@ def index_symbol_files( "${KICAD8_FOOTPRINT_DIR}", str(GLOBAL_FP_DIR_PATH) ) ) + + # HACK: paths typically look like .../libs/footprints/xyz.pretty + # we actually want the .../libs/ part of it, so we'll just knock + # off the last two directories + resolved_lib_dir = resolved_lib_dir.parent.parent + for path in resolved_lib_dir.glob("*.kicad_sym"): if path.stem not in self._symbol_files_index: self._symbol_files_index[path.stem] = path @@ -231,10 +238,10 @@ def get_symbol_file(self, lib_name: str) -> C_kicad_sym_file: @staticmethod def get_sub_syms( - lib_sym: SCH.C_lib_symbols.C_symbol, + lib_sym: C_lib_symbol, unit: int | None, body_style: int = 1, - ) -> list[SCH.C_lib_symbols.C_symbol.C_symbol]: + ) -> list[C_lib_symbol.C_symbol]: """ This is purely based on naming convention. There are two suffixed numbers on the end: __, eg "LED_0_1" @@ -254,7 +261,7 @@ def get_sub_syms( if body_style != 1: raise NotImplementedError("Only body style 1 is supported") - sub_syms: list[SCH.C_lib_symbols.C_symbol.C_symbol] = [] + sub_syms: list[C_lib_symbol.C_symbol] = [] for name, sym in lib_sym.symbols.items(): _, sub_sym_unit, sub_sym_body_style = name.split("_") sub_sym_unit = int(sub_sym_unit) @@ -267,7 +274,7 @@ def get_sub_syms( return sub_syms @staticmethod - def get_unit_count(lib_sym: SCH.C_lib_symbols.C_symbol) -> int: + def get_unit_count(lib_sym: C_lib_symbol) -> int: return max(int(name.split("_")[1]) for name in lib_sym.symbols.keys()) # TODO: remove @@ -408,7 +415,7 @@ def insert_text( def _ensure_lib_symbol( self, lib_id: str, - ) -> SCH.C_lib_symbols.C_symbol: + ) -> C_lib_symbol: """Ensure a symbol is in the schematic library, and return it""" if lib_id in self.sch.lib_symbols.symbols: return self.sch.lib_symbols.symbols[lib_id] @@ -483,7 +490,7 @@ def insert_symbol( ) else: # TODO: handle not having an overriden name better - raise Exception(f"Module {module} has no overriden name") + raise ValueError(f"Module {module} has no overriden name") self.attach_symbol(symbol, unit_instance) @@ -552,7 +559,7 @@ def _(obj: C_circle) -> BoundingBox: @get_bbox.register @staticmethod - def _(obj: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin) -> BoundingBox: + def _(obj: C_lib_symbol.C_symbol.C_pin) -> BoundingBox: # TODO: include the name and number in the bbox start = (obj.at.x, obj.at.y) end = Geometry.rotate(start, [(obj.at.x + obj.length, obj.at.y)], obj.at.r)[0] @@ -560,7 +567,7 @@ def _(obj: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin) -> BoundingBox: @get_bbox.register @classmethod - def _(cls, obj: SCH.C_lib_symbols.C_symbol.C_symbol) -> BoundingBox | None: + def _(cls, obj: C_lib_symbol.C_symbol) -> BoundingBox | None: all_geos = list( chain( obj.arcs, @@ -583,7 +590,7 @@ def _(cls, obj: SCH.C_lib_symbols.C_symbol.C_symbol) -> BoundingBox | None: @get_bbox.register @classmethod - def _(cls, obj: SCH.C_lib_symbols.C_symbol) -> BoundingBox: + def _(cls, obj: C_lib_symbol) -> BoundingBox: sub_points = list( chain.from_iterable( bboxes @@ -606,7 +613,7 @@ def _add_missing_symbols(self): Add symbols to the schematic that are missing based on the fab graph """ for f_symbol in self.missing_symbols: - self.insert_symbol(f_symbol) + self.insert_symbol(f_symbol.represents) self.missing_symbols = [] def _build_shim_circuit(self) -> shims.Circuit: @@ -711,7 +718,7 @@ def _best_name(module: Module) -> str: all_sch_lib_pins = [p for u in sch_lib_symbol_units for p in u.pins] if logger.isEnabledFor(logging.DEBUG): - rich.print( + logger.debug( f"Symbol {sch_sym.propertys['Reference'].value=} {sch_sym.uuid=}" ) pins = rich.table.Table("pin.name=", "pin.number=") @@ -720,7 +727,7 @@ def _best_name(module: Module) -> str: rich.print(pins) for sch_pin in sch_sym.pins: - rich.print(f"Pin {sch_pin.name=}") + logger.debug(f"Pin {sch_pin.name=}") try: lib_sch_pin = find( all_sch_lib_pins, @@ -743,7 +750,7 @@ def _best_name(module: Module) -> str: ) assert isinstance( - lib_sch_pin, SCH.C_lib_symbols.C_symbol.C_symbol.C_pin + lib_sch_pin, C_lib_symbol.C_symbol.C_pin ) shim_pin = sch_to_shim_pin_map.setdefault(sch_pin, shims.Pin()) shim_pin.name = sch_pin.name @@ -840,4 +847,4 @@ def generate_schematic(self, **options: Unpack[shims.Options]): gen_schematic(circuit, ".", "test", **options) # 4. transform sch according to skidl - # TODO: + diff --git a/src/faebryk/libs/kicad/fileformats_sch.py b/src/faebryk/libs/kicad/fileformats_sch.py index ce6cf376..062458ed 100644 --- a/src/faebryk/libs/kicad/fileformats_sch.py +++ b/src/faebryk/libs/kicad/fileformats_sch.py @@ -98,6 +98,97 @@ class C_polyline: pts: C_pts = field(default_factory=C_pts) +@dataclass +class C_lib_symbol: + @dataclass + class C_pin_names: + offset: float + + @dataclass + class C_symbol: + @dataclass + class C_pin: + class E_type(SymEnum): + # sorted alphabetically + bidirectional = "bidirectional" + free = "free" + input = "input" + no_connect = "no_connect" + open_collector = "open_collector" + open_emitter = "open_emitter" + output = "output" + passive = "passive" + power_in = "power_in" + power_out = "power_out" + tri_state = "tri_state" + unspecified = "unspecified" + + class E_style(SymEnum): + # sorted alphabetically + clock = "clock" + clock_low = "clock_low" + edge_clock_high = "edge_clock_high" + input_low = "input_low" + inverted = "inverted" + inverted_clock = "inverted_clock" + line = "line" + non_logic = "non_logic" + output_low = "output_low" + + @dataclass + class C_name: + name: str = field(**sexp_field(positional=True)) + effects: C_effects = field(default_factory=C_effects) + + @dataclass + class C_number: + number: str = field(**sexp_field(positional=True)) + effects: C_effects = field(default_factory=C_effects) + + at: C_xyr + length: float + type: E_type = field(**sexp_field(positional=True)) + style: E_style = field(**sexp_field(positional=True)) + name: C_name = field(default_factory=C_name) + number: C_number = field(default_factory=C_number) + + name: str = field(**sexp_field(positional=True)) + polylines: list[C_polyline] = field( + **sexp_field(multidict=True), default_factory=list + ) + circles: list[C_circle] = field( + **sexp_field(multidict=True), default_factory=list + ) + rectangles: list[C_rect] = field( + **sexp_field(multidict=True), default_factory=list + ) + arcs: list[C_arc] = field(**sexp_field(multidict=True), default_factory=list) + pins: list[C_pin] = field(**sexp_field(multidict=True), default_factory=list) + + class E_hide(SymEnum): + hide = "hide" + + @dataclass + class C_power: + pass + + name: str = field(**sexp_field(positional=True)) + power: Optional[C_power] = None + propertys: dict[str, C_property] = field( + **sexp_field(multidict=True, key=lambda x: x.name), + default_factory=dict, + ) + pin_numbers: Optional[E_hide] = None + pin_names: Optional[C_pin_names] = None + in_bom: Optional[bool] = None + on_board: Optional[bool] = None + symbols: dict[str, C_symbol] = field( + **sexp_field(multidict=True, key=lambda x: x.name), + default_factory=dict, + ) + convert: Optional[int] = None + + @dataclass class C_kicad_sch_file(SEXP_File): """ @@ -115,101 +206,7 @@ class C_title_block: @dataclass class C_lib_symbols: - @dataclass - class C_symbol: - @dataclass - class C_pin_names: - offset: float - - @dataclass - class C_symbol: - @dataclass - class C_pin: - class E_type(SymEnum): - # sorted alphabetically - bidirectional = "bidirectional" - free = "free" - input = "input" - no_connect = "no_connect" - open_collector = "open_collector" - open_emitter = "open_emitter" - output = "output" - passive = "passive" - power_in = "power_in" - power_out = "power_out" - tri_state = "tri_state" - unspecified = "unspecified" - - class E_style(SymEnum): - # sorted alphabetically - clock = "clock" - clock_low = "clock_low" - edge_clock_high = "edge_clock_high" - input_low = "input_low" - inverted = "inverted" - inverted_clock = "inverted_clock" - line = "line" - non_logic = "non_logic" - output_low = "output_low" - - @dataclass - class C_name: - name: str = field(**sexp_field(positional=True)) - effects: C_effects = field(default_factory=C_effects) - - @dataclass - class C_number: - number: str = field(**sexp_field(positional=True)) - effects: C_effects = field(default_factory=C_effects) - - at: C_xyr - length: float - type: E_type = field(**sexp_field(positional=True)) - style: E_style = field(**sexp_field(positional=True)) - name: C_name = field(default_factory=C_name) - number: C_number = field(default_factory=C_number) - - name: str = field(**sexp_field(positional=True)) - polylines: list[C_polyline] = field( - **sexp_field(multidict=True), default_factory=list - ) - circles: list[C_circle] = field( - **sexp_field(multidict=True), default_factory=list - ) - rectangles: list[C_rect] = field( - **sexp_field(multidict=True), default_factory=list - ) - arcs: list[C_arc] = field( - **sexp_field(multidict=True), default_factory=list - ) - pins: list[C_pin] = field( - **sexp_field(multidict=True), default_factory=list - ) - - class E_hide(SymEnum): - hide = "hide" - - @dataclass - class C_power: - pass - - name: str = field(**sexp_field(positional=True)) - power: Optional[C_power] = None - propertys: dict[str, C_property] = field( - **sexp_field(multidict=True, key=lambda x: x.name), - default_factory=dict, - ) - pin_numbers: Optional[E_hide] = None - pin_names: Optional[C_pin_names] = None - in_bom: Optional[bool] = None - on_board: Optional[bool] = None - symbols: dict[str, C_symbol] = field( - **sexp_field(multidict=True, key=lambda x: x.name), - default_factory=dict, - ) - convert: Optional[int] = None - - symbols: dict[str, C_symbol] = field( + symbols: dict[str, C_lib_symbol] = field( **sexp_field(multidict=True, key=lambda x: x.name), default_factory=dict ) @@ -384,6 +381,19 @@ class C_bus_entry: kicad_sch: C_kicad_sch + @classmethod + def skeleton(cls) -> "C_kicad_sch_file": + return cls( + kicad_sch=cls.C_kicad_sch( + version=20211123, + generator="faebryk", + paper="A4", + # uuid=gen_uuid(), + # lib_symbols=C_kicad_sch_file.C_kicad_sch.C_lib_symbols(symbols={}), + # title_block=C_kicad_sch_file.C_kicad_sch.C_title_block(), + ) + ) + @dataclass class C_kicad_sym_file(SEXP_File): @@ -395,8 +405,20 @@ class C_kicad_sym_file(SEXP_File): class C_kicad_symbol_lib: version: int generator: str - symbols: dict[str, C_kicad_sch_file.C_kicad_sch.C_lib_symbols.C_symbol] = field( - **sexp_field(multidict=True, key=lambda x: x.name), default_factory=dict + symbols: dict[str, C_lib_symbol] = ( + field( + **sexp_field(multidict=True, key=lambda x: x.name), default_factory=dict + ) ) kicad_symbol_lib: C_kicad_symbol_lib + + @classmethod + def skeleton(cls) -> "C_kicad_sym_file": + return cls( + kicad_symbol_lib=cls.C_kicad_symbol_lib( + version=20211123, + generator="faebryk", + symbols={}, + ) + ) From cdf17041bd6e8d791bf59494e62be2b3c6fbd99d Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 2 Oct 2024 16:21:32 +0200 Subject: [PATCH 45/85] Add separate logging levels to core --- src/faebryk/core/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/faebryk/core/__init__.py diff --git a/src/faebryk/core/__init__.py b/src/faebryk/core/__init__.py new file mode 100644 index 00000000..c8cf2054 --- /dev/null +++ b/src/faebryk/core/__init__.py @@ -0,0 +1,21 @@ +import logging +from enum import StrEnum + +from faebryk.libs.util import ConfigFlagEnum + +logger = logging.getLogger(__name__) + + +class LogLevel(StrEnum): + CRITICAL = "CRITICAL" + FATAL = "FATAL" + ERROR = "ERROR" + WARN = "WARN" + WARNING = "WARNING" + INFO = "INFO" + DEBUG = "DEBUG" + NOTSET = "NOTSET" + + +log_level = ConfigFlagEnum(LogLevel, "CORE_LOG_LEVEL", LogLevel.INFO) +logger.setLevel(logging.getLevelNamesMapping()[log_level.get()]) From d10d82c1a82982eddd5c06c7cefeabd4bb01a8ef Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 10 Oct 2024 17:23:26 -0700 Subject: [PATCH 46/85] Optional Reference --- src/faebryk/core/reference.py | 5 ++++- test/library/test_reference.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/faebryk/core/reference.py b/src/faebryk/core/reference.py index b05d7bd2..33f1b5a5 100644 --- a/src/faebryk/core/reference.py +++ b/src/faebryk/core/reference.py @@ -13,16 +13,19 @@ class Reference[O: Node](constructed_field): class UnboundError(Exception): """Cannot resolve unbound reference""" - def __init__(self, out_type: type[O] | None = None): + def __init__(self, out_type: type[O] | None = None, optional: bool = False): self.gifs: dict[Node, GraphInterfaceReference] = defaultdict( GraphInterfaceReference ) self.is_set: set[Node] = set() + self.optional = optional def get(instance: Node) -> O: try: return self.gifs[instance].get_reference() except GraphInterfaceReference.UnboundError as ex: + if self.optional: + return None raise Reference.UnboundError from ex def set_(instance: Node, value: O): diff --git a/test/library/test_reference.py b/test/library/test_reference.py index efb76f2b..151a1899 100644 --- a/test/library/test_reference.py +++ b/test/library/test_reference.py @@ -135,3 +135,14 @@ class B(Node): # check that the property is set assert b.x is a + + +def test_optional(): + class A(Node): + pass + + class B(Node): + x = Reference(A, optional=True) + + b = B() + assert b.x is None From 1ffa83f8ce672317bc74e7cf36b9ad1ee3c501f3 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 10 Oct 2024 18:16:44 -0700 Subject: [PATCH 47/85] Format --- .../exporters/schematic/kicad/skidl/bboxes.py | 4 +-- .../schematic/kicad/skidl/debug_draw.py | 21 +++++++++++++-- .../schematic/kicad/skidl/draw_objs.py | 1 + .../schematic/kicad/skidl/gen_schematic.py | 17 +++++------- .../schematic/kicad/skidl/geometry.py | 10 ++++++- .../schematic/kicad/skidl/net_terminal.py | 1 + .../exporters/schematic/kicad/skidl/node.py | 2 +- .../exporters/schematic/kicad/skidl/place.py | 27 +++++++------------ .../exporters/schematic/kicad/skidl/shims.py | 5 ++-- .../exporters/schematic/kicad/transformer.py | 7 ++--- 10 files changed, 54 insertions(+), 41 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py index 3d405abe..b0d27c8d 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py @@ -35,9 +35,11 @@ logger = logging.getLogger(__name__) + @dataclass class PinDir: """Stores information about pins in each of four directions""" + # direction: The direction the pin line is drawn from start to end. direction: Point # side: The side of the symbol the pin is on. (Opposite of the direction.) @@ -73,7 +75,6 @@ def calc_symbol_bbox(part: Part, **options: Unpack[Options]): # Named tuples for part KiCad V5 DRAW primitives. def make_pin_dir_tbl(abs_xoff: int = 20) -> dict[str, "PinDir"]: - # abs_xoff is the absolute distance of name/num from the end of the pin. rel_yoff_num = -0.15 # Relative distance of number above pin line. rel_yoff_name = ( @@ -127,7 +128,6 @@ def make_pin_dir_tbl(abs_xoff: int = 20) -> dict[str, "PinDir"]: # Go through each graphic object that makes up the component symbol. for obj in part.draw: - obj_bbox = BBox() # Bounding box of all the component objects. thickness = 0 diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index 5b7bc1f7..cabf2157 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -6,6 +6,7 @@ """ Drawing routines used for debugging place & route. """ + from collections import defaultdict from random import randint from typing import TYPE_CHECKING @@ -50,7 +51,9 @@ def draw_box( pygame.draw.polygon(scr, color, corners, thickness) -def draw_endpoint(pt: Point, scr: "pygame.Surface", tx: Tx, color=(100, 100, 100), dot_radius=10): +def draw_endpoint( + pt: Point, scr: "pygame.Surface", tx: Tx, color=(100, 100, 100), dot_radius=10 +): """Draw a line segment endpoint in the drawing area. Args: @@ -78,6 +81,7 @@ def draw_endpoint(pt: Point, scr: "pygame.Surface", tx: Tx, color=(100, 100, 100 radius = dot_radius * tx.a pygame.draw.circle(scr, color, (pt.x, pt.y), radius) + def draw_seg( seg: "Segment | Interval | NetInterval", scr: "pygame.Surface", @@ -116,6 +120,7 @@ def draw_seg( scr, color, (seg.p1.x, seg.p1.y), (seg.p2.x, seg.p2.y), width=thickness ) + def draw_text( txt: str, pt: Point, @@ -145,6 +150,7 @@ def draw_text( # syntax highlighting doesn't recognise this font.render_to(scr, (pt.x, pt.y), txt, color) + def draw_part(part: "Part", scr: "pygame.Surface", tx: Tx, font: "pygame.font.Font"): """Draw part bounding box. @@ -169,6 +175,7 @@ def draw_part(part: "Part", scr: "pygame.Surface", tx: Tx, font: "pygame.font.Fo # Probably trying to draw a block of parts which has no pins and can't iterate thru them. pass + def draw_net( net: "Net", parts: list["Part"], @@ -207,6 +214,7 @@ def draw_net( dot_radius=dot_radius, ) + def draw_force( part: "Part", force: Vector, @@ -231,6 +239,7 @@ def draw_force( Segment(anchor, anchor + force), scr, tx, color=color, thickness=5, dot_radius=5 ) + def draw_placement( parts: list["Part"], nets: list["Net"], @@ -255,6 +264,7 @@ def draw_placement( draw_net(net, parts, scr, tx, font) draw_redraw() + def draw_routing( node: "SchNode", bbox: BBox, @@ -292,6 +302,7 @@ def draw_routing( draw_end() + def draw_clear( scr: "pygame.Surface", color: tuple[int, int, int] = (255, 255, 255), @@ -304,6 +315,7 @@ def draw_clear( """ scr.fill(color) + def draw_start( bbox: BBox, ): @@ -321,7 +333,7 @@ def draw_start( import pygame # Screen drawing area. - scr_bbox = BBox(Point(0, 0), Point(2000, 1500)) + scr_bbox = BBox(Point(0, 0), Point(1000, 1000)) # Place a blank region around the object by expanding it's bounding box. border = max(bbox.w, bbox.h) / 20 @@ -357,11 +369,14 @@ def draw_start( # Return drawing screen, transformation matrix, and font. return scr, tx, font + def draw_redraw(): """Redraw the PyGame display.""" import pygame + pygame.display.flip() + def draw_pause(): """Pause drawing and then resume after button press.""" import pygame @@ -376,8 +391,10 @@ def draw_pause(): if event.type in (pygame.KEYDOWN, pygame.QUIT): running = False + def draw_end(): """Display drawing and wait for user to close PyGame window.""" import pygame + draw_pause() pygame.quit() diff --git a/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py b/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py index 4d11b3e0..3612e220 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/draw_objs.py @@ -5,6 +5,7 @@ """ KiCad 5 drawing objects. """ + from dataclasses import dataclass diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index ce8fa3d6..554fdc94 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -7,7 +7,6 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. - import datetime import os.path import re @@ -482,7 +481,6 @@ def node_to_eeschema(node: SchNode, sheet_tx: Tx = Tx()) -> str: # If this node was flattened, then return the EESCHEMA code and surrounding box # for inclusion in the parent node. if node.flattened: - # Generate the graphic box that surrounds the flattened hierarchical block of this node. block_name = node.name.split(HIER_SEP)[-1] pad = Vector(BLK_INT_PAD, BLK_INT_PAD) @@ -716,26 +714,23 @@ def gen_schematic( node.route(**options) except PlacementFailure: - # Placement failed, so clean up ... - finalize_parts_and_nets(circuit, **options) - # ... and try again. + # Placement failed, so try again. continue except RoutingFailure: - # Routing failed, so clean up ... - finalize_parts_and_nets(circuit, **options) - # ... and expand routing area ... + # Routing failed, so expand routing area ... expansion_factor *= 1.5 # HACK: Ad-hoc increase of expansion factor. # ... and try again. continue + finally: + # Clean up. + finalize_parts_and_nets(circuit, **options) + # Generate EESCHEMA code for the schematic. # TODO: extract into for generating schematic objects # node_to_eeschema(node) - # Clean up. - finalize_parts_and_nets(circuit, **options) - # Place & route was successful if we got here, so exit. return diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index a5fddbc0..852842bf 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -30,7 +30,15 @@ def to_mms(mils): class Tx: - def __init__(self, a: float = 1, b: float = 0, c: float = 0, d: float = 1, dx: float = 0, dy: float = 0): + def __init__( + self, + a: float = 1, + b: float = 0, + c: float = 0, + d: float = 1, + dx: float = 0, + dy: float = 0, + ): """Create a transformation matrix. tx = [ a b 0 diff --git a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py index 3a4b240e..00580000 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py @@ -21,6 +21,7 @@ def __init__(self, net: Net): the net spans across levels of hierarchical nodes. """ from .bboxes import calc_hier_label_bbox + # Set a default transformation matrix for this part. self.tx = Tx() diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 9a47bd81..3881f5da 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -296,7 +296,7 @@ def get_internal_nets(self) -> list[Net]: processed_nets.append(net) # Skip stubbed nets. - if getattr(net, "stub", False) is True: + if getattr(net, "stub", False): continue # Add net to collection if at least one pin is on one of the parts of the node. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 6d1843de..f1f598c6 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -83,7 +83,6 @@ def is_net_terminal(part): # Class for movable groups of parts/child nodes. class PartBlock: - def __init__( self, src: "list[Part | SchNode] | Part | SchNode", @@ -340,7 +339,6 @@ def find_best_orientation(part: Part) -> None: calc_starting_cost = True for i in range(2): for j in range(4): - if calc_starting_cost: # Calculate the cost of the starting orientation before any changes in orientation. starting_cost = net_tension(part, **options) @@ -712,7 +710,7 @@ def overlap_force_rand( def scale_attractive_repulsive_forces( parts: list[Part], force_func: Callable[[Part, list[Part], ...], Vector], - **options: Unpack[Options] + **options: Unpack[Options], ) -> float: """Set scaling between attractive net forces and repulsive part overlap forces.""" @@ -750,7 +748,7 @@ def total_part_force( parts: list[Part], scale: float, alpha: float, - **options: Unpack[Options] + **options: Unpack[Options], ) -> Vector: """Compute the total of the attractive net and repulsive overlap forces on a part. @@ -804,7 +802,7 @@ def total_similarity_force( similarity: dict, scale: float, alpha: float, - **options: Unpack[Options] + **options: Unpack[Options], ) -> Vector: """Compute the total of the attractive similarity and repulsive overlap forces on a part. @@ -879,7 +877,7 @@ def push_and_pull( mobile_parts: list[Part], nets: list[Net], force_func: Callable[[Part, list[Part], ...], Vector], - **options: Unpack[Options] + **options: Unpack[Options], ): """Move parts under influence of attractive nets and repulsive part overlaps. @@ -1021,7 +1019,7 @@ def evolve_placement( mobile_parts: list[Part], nets: list[Net], force_func: Callable[[Part, list[Part], ...], Vector], - **options: Unpack[Options] + **options: Unpack[Options], ): """Evolve part placement looking for optimum using force function. @@ -1048,7 +1046,7 @@ def place_net_terminals( placed_parts: list[Part], nets: list[Net], force_func: Callable[[Part, list[Part], ...], Vector], - **options: Unpack[Options] + **options: Unpack[Options], ): """Place net terminals around already-placed parts. @@ -1179,7 +1177,7 @@ def dist_to_bbox_edge(term: "NetTerminal"): mobile_terminals[:-1], nets, force_func, - **options + **options, ) # Anchor the mobile terminals after their placement is done. anchored_parts.extend(mobile_terminals[:-1]) @@ -1383,7 +1381,7 @@ def place_blocks( connected_parts: list[list[Part]], floating_parts: list[Part], children: list["SchNode"], - **options: Unpack[Options] + **options: Unpack[Options], ): """Place blocks of parts and hierarchical sheets. @@ -1584,17 +1582,12 @@ def place(node: "SchNode", **options: Unpack[Options]): connected_parts, floating_parts, node.children.values(), **options ) - # Remove any stuff leftover from this place & route run. - # print(f"added part attrs = {new_part_attrs}") - node.rmv_placement_stuff() - # node.show_added_attrs() - # Calculate the bounding box for the node after placement of parts and children. node.calc_bbox() - except PlacementFailure: + finally: + # Remove any stuff leftover from this place & route run. node.rmv_placement_stuff() - raise PlacementFailure def get_snap_pt(node): """Get a Point to use for snapping the node to the grid. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 00a8d04f..0c884939 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -14,6 +14,8 @@ from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point, Tx, Vector if TYPE_CHECKING: + import pygame + from faebryk.libs.kicad import fileformats_sch from .route import Face, GlobalTrack @@ -38,7 +40,6 @@ def rmv_attr(objs, attrs): pass - def angle_to_orientation(angle: float) -> str: """Convert an angle to an orientation""" if angle == 0: @@ -273,7 +274,7 @@ class Options(TypedDict): draw_placement: bool draw_routing_channels: bool draw_routing: bool - draw_scr: bool + draw_scr: "pygame.Surface" draw_switchbox_boundary: bool draw_switchbox_routing: bool draw_tx: Tx diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index bb35f04f..ce8a46e7 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -81,7 +81,6 @@ class _HasPropertys(Protocol): # TODO: consider common transformer base class Transformer: - class has_linked_sch_symbol(F.Symbol.TraitT.decless()): def __init__(self, symbol: SCH.C_symbol_instance) -> None: super().__init__() @@ -749,9 +748,7 @@ def _best_name(module: Module) -> str: f"Pin {sch_pin.name} not found in any unit of symbol {sch_sym.name}" ) - assert isinstance( - lib_sch_pin, C_lib_symbol.C_symbol.C_pin - ) + assert isinstance(lib_sch_pin, C_lib_symbol.C_symbol.C_pin) shim_pin = sch_to_shim_pin_map.setdefault(sch_pin, shims.Pin()) shim_pin.name = sch_pin.name shim_pin.num = lib_sch_pin.number @@ -844,7 +841,7 @@ def generate_schematic(self, **options: Unpack[shims.Options]): # 3. run skidl schematic generation from faebryk.exporters.schematic.kicad.skidl.gen_schematic import gen_schematic + gen_schematic(circuit, ".", "test", **options) # 4. transform sch according to skidl - From fdc544a15e7d82c527700b14fc2254cd5e334433 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 10 Oct 2024 18:17:03 -0700 Subject: [PATCH 48/85] Download symbols properly --- src/faebryk/library/Pad.py | 12 ++--- src/faebryk/libs/kicad/fileformats_sch.py | 6 +-- src/faebryk/libs/picker/lcsc.py | 61 +++++++++++++++++++---- src/faebryk/libs/sexp/dataclass_sexp.py | 17 +++---- 4 files changed, 64 insertions(+), 32 deletions(-) diff --git a/src/faebryk/library/Pad.py b/src/faebryk/library/Pad.py index 6fc493c6..9f160b18 100644 --- a/src/faebryk/library/Pad.py +++ b/src/faebryk/library/Pad.py @@ -5,6 +5,7 @@ import faebryk.library._F as F from faebryk.core.moduleinterface import ModuleInterface from faebryk.core.node import NodeException +from faebryk.core.reference import Reference, reference from faebryk.libs.util import not_none @@ -12,20 +13,13 @@ class Pad(ModuleInterface): # FIXME: can net this become a reference instead? net: F.Electrical pcb: ModuleInterface + interface = reference(F.Electrical) def attach(self, intf: F.Electrical): self.net.connect(intf) + self.interface = intf intf.add(F.has_linked_pad_defined(self)) - @property - def interface(self) -> F.Electrical: - connections = self.net.get_direct_connections() - if len(connections) == 0: - raise NodeException(self, f"Pad {self} has no interface connected") - if len(connections) > 1: - raise NodeException(self, f"Pad {self} has multiple interfaces connected") - return list(connections)[0] - @staticmethod def find_pad_for_intf_with_parent_that_has_footprint_unique( intf: ModuleInterface, diff --git a/src/faebryk/libs/kicad/fileformats_sch.py b/src/faebryk/libs/kicad/fileformats_sch.py index 062458ed..ae2259d8 100644 --- a/src/faebryk/libs/kicad/fileformats_sch.py +++ b/src/faebryk/libs/kicad/fileformats_sch.py @@ -405,10 +405,8 @@ class C_kicad_sym_file(SEXP_File): class C_kicad_symbol_lib: version: int generator: str - symbols: dict[str, C_lib_symbol] = ( - field( - **sexp_field(multidict=True, key=lambda x: x.name), default_factory=dict - ) + symbols: dict[str, C_lib_symbol] = field( + **sexp_field(multidict=True, key=lambda x: x.name), default_factory=dict ) kicad_symbol_lib: C_kicad_symbol_lib diff --git a/src/faebryk/libs/picker/lcsc.py b/src/faebryk/libs/picker/lcsc.py index 61e28a4e..45d5a311 100644 --- a/src/faebryk/libs/picker/lcsc.py +++ b/src/faebryk/libs/picker/lcsc.py @@ -5,6 +5,7 @@ import logging from pathlib import Path +import sexpdata from easyeda2kicad.easyeda.easyeda_api import EasyedaApi from easyeda2kicad.easyeda.easyeda_importer import ( Easyeda3dModelImporter, @@ -17,11 +18,17 @@ import faebryk.library._F as F from faebryk.core.module import Module +from faebryk.libs.kicad.fileformats_sch import ( + C_kicad_sym_file, + C_lib_symbol, +) from faebryk.libs.picker.picker import ( Part, PickerOption, Supplier, ) +from faebryk.libs.sexp import dataclass_sexp +from faebryk.libs.util import duplicates logger = logging.getLogger(__name__) @@ -127,8 +134,6 @@ def download_easyeda_info(partno: str, get_model: bool = True): # export to kicad --------------------------------------------------------- ki_footprint = ExporterFootprintKicad(easyeda_footprint) - ki_symbol = ExporterSymbolKicad(easyeda_symbol, KicadVersion.v6) - _fix_3d_model_offsets(ki_footprint) easyeda_model = Easyeda3dModelImporter( @@ -167,9 +172,24 @@ def download_easyeda_info(partno: str, get_model: bool = True): model_3d_path=kicad_model_path, ) - if not sym_base_path.exists(): - logger.debug(f"Exporting symbol {sym_base_path}") - ki_symbol.export(str(sym_base_path)) + # for some ungodly reason, the symbol and + # footprint exporters work very differently + logger.debug(f"Exporting symbol {sym_base_path}") + if sym_base_path.exists(): + sym_file = C_kicad_sym_file.loads(sym_base_path) + else: + sym_file = C_kicad_sym_file.skeleton() + + exporter = ExporterSymbolKicad(symbol=easyeda_symbol, kicad_version=KicadVersion.v6) + + # first, I tried to do this with a skeleton file and the exporter + # from easyeda2kicad, but it didn't work out, so fuck it, we have the tools + data = sexpdata.loads(exporter.export("lcsc")) + # clip of the first token, which is the fact that this is a symbol + lib_sym = dataclass_sexp.loads(data[1:], C_lib_symbol) + sym_file.kicad_symbol_lib.symbols[lib_sym.name] = lib_sym + + sym_file.dumps(sym_base_path) return ki_footprint, ki_model, easyeda_footprint, easyeda_model, easyeda_symbol @@ -179,7 +199,7 @@ def attach(component: Module, partno: str, get_model: bool = True): download_easyeda_info(partno, get_model=get_model) ) - # symbol + # footprint if not component.has_trait(F.has_footprint): if not component.has_trait(F.can_attach_to_footprint): if not component.has_trait(F.has_pin_association_heuristic): @@ -203,16 +223,37 @@ def attach(component: Module, partno: str, get_model: bool = True): raise LCSC_PinmapException(partno, f"Failed to get pinmap: {e}") from e component.add(F.can_attach_to_footprint_via_pinmap(pinmap)) - sym = F.Symbol.with_component(component, pinmap) - sym.add(F.Symbol.has_kicad_symbol(f"lcsc:{easyeda_footprint.info.name}")) - - # footprint fp = F.KicadFootprint( f"lcsc:{easyeda_footprint.info.name}", [p.number for p in easyeda_footprint.pads], ) component.get_trait(F.can_attach_to_footprint).attach(fp) + # symbol + if not component.has_trait(F.Symbol.has_symbol): + # FIXME: generalise this for other schematic tools + + # these traits are fucking hard to follow... I mean, geez there's like 15 of + # them interacting through as many files to figure out which pads are attached + # to which pin names + # We're assuming the thing we're handling is guaranteed to have a footprint, + # based on the above works + # We're also assuming that has a KiCAD footprint because, as of writing, it's + # the only output supported + pads = ( + component.get_trait(F.has_footprint) + .get_footprint() + .get_trait(F.has_kicad_footprint) + .get_pin_names() + ) + + # FIXME: figure out what's up with duplicates + pinmap = {name: pad.interface for pad, name in pads.items()} + + sym = F.Symbol.with_component(component, pinmap) + sym.add(F.Symbol.has_kicad_symbol(f"lcsc:{easyeda_symbol.info.name}")) + component.add(F.Symbol.has_symbol(sym)) + component.add(F.has_descriptive_properties_defined({"LCSC": partno})) # model done by kicad (in fp) diff --git a/src/faebryk/libs/sexp/dataclass_sexp.py b/src/faebryk/libs/sexp/dataclass_sexp.py index 1c213f9a..6bbe98c9 100644 --- a/src/faebryk/libs/sexp/dataclass_sexp.py +++ b/src/faebryk/libs/sexp/dataclass_sexp.py @@ -417,15 +417,14 @@ def _append_kv(name, v): return sexp -def loads[T](s: str | Path | list, t: type[T]) -> T: - text = s - sexp = s - if isinstance(s, Path): - text = s.read_text() - if isinstance(text, str): - sexp = sexpdata.loads(text) - - return _decode([sexp], t) +def loads[T](data: str | Path | list, t: type[T]) -> T: + if isinstance(data, Path): + data = data.read_text() + + if isinstance(data, str): + data = [sexpdata.loads(data)] + + return _decode(data, t) def dumps(obj, path: PathLike | None = None) -> str: From 01213dbda9d7cf558fc77d5fdbab5634f65574f7 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 11 Oct 2024 17:28:52 -0700 Subject: [PATCH 49/85] Add explicit flag to has_overridden_name --- src/faebryk/exporters/netlist/graph.py | 3 ++- src/faebryk/library/Net.py | 2 +- src/faebryk/library/has_overriden_name.py | 3 +++ src/faebryk/library/has_overriden_name_defined.py | 6 +++++- src/faebryk/libs/app/designators.py | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/faebryk/exporters/netlist/graph.py b/src/faebryk/exporters/netlist/graph.py index a67c8e18..cf87f234 100644 --- a/src/faebryk/exporters/netlist/graph.py +++ b/src/faebryk/exporters/netlist/graph.py @@ -41,7 +41,8 @@ def get_or_set_name_and_value_of_node(c: Module): c.get_full_name(), type(c).__name__, value, - ) + ), + explicit=False, ) ) diff --git a/src/faebryk/library/Net.py b/src/faebryk/library/Net.py index cecc387d..d15b57a2 100644 --- a/src/faebryk/library/Net.py +++ b/src/faebryk/library/Net.py @@ -67,5 +67,5 @@ def __repr__(self) -> str: @classmethod def with_name(cls, name: str) -> "Net": n = cls() - n.add(F.has_overriden_name_defined(name)) + n.add(F.has_overriden_name_defined(name, explicit=True)) return n diff --git a/src/faebryk/library/has_overriden_name.py b/src/faebryk/library/has_overriden_name.py index b7693e4b..d0a053fc 100644 --- a/src/faebryk/library/has_overriden_name.py +++ b/src/faebryk/library/has_overriden_name.py @@ -9,3 +9,6 @@ class has_overriden_name(Module.TraitT): @abstractmethod def get_name(self) -> str: ... + + @abstractmethod + def is_explicit(self) -> bool: ... diff --git a/src/faebryk/library/has_overriden_name_defined.py b/src/faebryk/library/has_overriden_name_defined.py index 6180e3bd..ba2e2782 100644 --- a/src/faebryk/library/has_overriden_name_defined.py +++ b/src/faebryk/library/has_overriden_name_defined.py @@ -5,9 +5,13 @@ class has_overriden_name_defined(F.has_overriden_name.impl()): - def __init__(self, name: str) -> None: + def __init__(self, name: str, explicit: bool = True) -> None: super().__init__() self.component_name = name + self.explicit = explicit def get_name(self): return self.component_name + + def is_explicit(self) -> bool: + return self.explicit diff --git a/src/faebryk/libs/app/designators.py b/src/faebryk/libs/app/designators.py index ca3f79ad..1ff2daa2 100644 --- a/src/faebryk/libs/app/designators.py +++ b/src/faebryk/libs/app/designators.py @@ -79,7 +79,7 @@ def override_names_with_designators(graph: Graph): logger.warning( f"Renaming: {n.get_trait(F.has_overriden_name).get_name()} -> {name}" ) - n.add(F.has_overriden_name_defined(name)) + n.add(F.has_overriden_name_defined(name, explicit=False)) def attach_hierarchical_designators(graph: Graph): From 3cb2057b39397b0a6ff014a8e7945a016c1c2300 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 11 Oct 2024 17:35:20 -0700 Subject: [PATCH 50/85] OMG it's working --- .../schematic/kicad/skidl/net_terminal.py | 36 +++++++++++++++++++ .../exporters/schematic/kicad/skidl/node.py | 6 ++++ .../exporters/schematic/kicad/skidl/shims.py | 35 +++++++++--------- .../exporters/schematic/kicad/transformer.py | 36 ++++++++++++------- 4 files changed, 85 insertions(+), 28 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py index 00580000..150cac09 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py @@ -2,6 +2,9 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. +import uuid +from functools import cached_property + from .constants import GRID from .geometry import Point, Tx, Vector from .shims import Net, Part, Pin @@ -24,12 +27,22 @@ def __init__(self, net: Net): # Set a default transformation matrix for this part. self.tx = Tx() + self.ref = net.name + self.symtx = "" + self.unit = {} + self.hierarchy = uuid.uuid4().hex # Add a single pin to the part. pin = Pin() pin.part = self pin.num = "1" pin.name = "~" + pin.stub = False + pin.fab_pin = None + pin.sch_pin = None + pin.fab_is_gnd = False + pin.fab_is_pwr = False + pin.net = net self.pins = [pin] # Connect the pin to the net. @@ -62,3 +75,26 @@ def __init__(self, net: Net): self.tx = ( self.tx.move(origin - term_origin).flip_x().move(term_origin - origin) ) + + pin.audit() + self.audit() + + @cached_property + def hierarchy(self) -> str: + return uuid.uuid4().hex + + @property + def sch_symbol(self) -> None: + return None + + @property + def fab_symbol(self) -> None: + return None + + @property + def bare_bbox(self) -> None: + return None + + @property + def _similarites(self) -> dict[Part, float]: + return {} diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 3881f5da..0aa563e9 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -77,8 +77,10 @@ def add_circuit(self, circuit: Circuit): Args: circuit (Circuit): Circuit object. """ + from .net_terminal import NetTerminal # Build the circuit node hierarchy by adding the parts. + assert circuit.parts, "Circuit has no parts" for part in circuit.parts: self.add_part(part) @@ -114,6 +116,10 @@ def add_circuit(self, circuit: Circuit): part = pin.part + # Skip NetTerminals because otherwise doesn't this just recurse forever? + if isinstance(part, NetTerminal): + continue + if part.hierarchy in visited: # Already added a terminal to this node, so don't add another. continue diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 0c884939..7474d342 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -54,6 +54,13 @@ def angle_to_orientation(angle: float) -> str: raise ValueError(f"Invalid angle: {angle}") +def audit_has(obj, attrs: list[str]) -> None: + """Ensure mandatory attributes are set""" + missing = [attr for attr in attrs if not hasattr(obj, attr)] + if missing: + raise ValueError(f"Missing attributes: {missing}") + + class Part: # We need to assign these hierarchy: str # dot-separated string of part names @@ -71,18 +78,17 @@ class Part: def audit(self) -> None: """Ensure mandatory attributes are set""" - for attr in [ + audit_has(self, [ "hierarchy", "pins", "ref", "symtx", "unit", "fab_symbol", + "sch_symbol", "bare_bbox", "_similarites", - ]: - if not hasattr(self, attr): - raise ValueError(f"Missing attribute: {attr}") + ]) # don't audit pins, they're handled through nets instead for unit in self.unit.values(): @@ -184,7 +190,7 @@ class Pin: def audit(self) -> None: """Ensure mandatory attributes are set""" - for attr in [ + audit_has(self, [ "name", "net", "num", @@ -196,9 +202,7 @@ def audit(self) -> None: "fab_pin", "fab_is_gnd", "fab_is_pwr", - ]: - if not hasattr(self, attr): - raise ValueError(f"Missing attribute: {attr}") + ]) # internal use bbox: BBox @@ -220,21 +224,22 @@ class Net: pins: list[Pin] stub: bool # whether to stub the pin or not + # added for our use + _is_implicit: bool + # internal use parts: set[Part] def audit(self) -> None: """Ensure mandatory attributes are set""" - for attr in ["name", "netio", "pins", "stub"]: - if not hasattr(self, attr): - raise ValueError(f"Missing attribute: {attr}") + audit_has(self, ["name", "netio", "pins", "stub"]) for pin in self.pins: pin.audit() def __bool__(self) -> bool: """TODO: does this need to be false if no parts or pins?""" - raise NotImplementedError + return bool(self.pins) or bool(self.parts) def __iter__(self) -> Iterator[Pin | Part]: raise NotImplementedError # not sure what to output here @@ -247,7 +252,7 @@ def __hash__(self) -> int: def is_implicit(self) -> bool: """Whether the net has a user-assigned name""" - raise NotImplementedError + return self._is_implicit class Circuit: @@ -256,9 +261,7 @@ class Circuit: def audit(self) -> None: """Ensure mandatory attributes are set""" - for attr in ["nets", "parts"]: - if not hasattr(self, attr): - raise ValueError(f"Missing attribute: {attr}") + audit_has(self, ["nets", "parts"]) for obj in chain(self.nets, self.parts): obj.audit() diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index ce8a46e7..d08ea984 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -127,11 +127,16 @@ def attach(self): } for node, sym_trait in self.graph.nodes_with_trait(F.Symbol.has_symbol): if not node.has_trait(F.has_overriden_name): + # These names are typically generated by the netlist transformer + logger.warning(f"Symbol {sym_trait.reference} has no overriden name") continue symbol = sym_trait.reference if not symbol.has_trait(F.Symbol.has_kicad_symbol): + # KiCAD symbols are typically generated by the lcsc picker + logger.warning(f"Symbol {symbol} has no kicad symbol") + # TODO: generate a bug instead of skipping continue sym_ref = node.get_trait(F.has_overriden_name).get_name() @@ -155,15 +160,9 @@ def attach_symbol(self, f_symbol: F.Symbol, sym_inst: SCH.C_symbol_instance): """Bind the module and symbol together on the graph""" f_symbol.add(self.has_linked_sch_symbol(sym_inst)) - lib_sym = self._ensure_lib_symbol(sym_inst.lib_id) - lib_sym_units = self.get_sub_syms(lib_sym, sym_inst.unit) - lib_sym_pins = [p for u in lib_sym_units for p in u.pins] - pin_no_to_name = {str(pin.number.number): pin.name.name for pin in lib_sym_pins} - # Attach the pins on the symbol to the module interface - pin_by_name = groupby(sym_inst.pins, key=lambda p: pin_no_to_name[p.name]) - for pin_name, pins in pin_by_name: - f_symbol.pins[pin_name].add(Transformer.has_linked_sch_pins(pins, sym_inst)) + for pin in sym_inst.pins: + f_symbol.pins[pin.name].add(Transformer.has_linked_sch_pins([pin], sym_inst)) # TODO: remove cleanup, it shouldn't really be required if we're marking propertys # def cleanup(self): @@ -274,7 +273,7 @@ def get_sub_syms( @staticmethod def get_unit_count(lib_sym: C_lib_symbol) -> int: - return max(int(name.split("_")[1]) for name in lib_sym.symbols.keys()) + return max(int(name.split("_")[1]) for name in lib_sym.symbols.keys()) or 1 # TODO: remove # @singledispatchmethod @@ -656,6 +655,7 @@ def _build_shim_circuit(self) -> shims.Circuit: shim_net.name = net.get_trait(F.has_overriden_name).get_name() shim_net.netio = "" # TODO: shim_net.stub = False # TODO: + shim_net._is_implicit = net.get_trait(F.has_overriden_name).is_explicit() # make partial net-oriented pins shim_net.pins = [] @@ -708,6 +708,7 @@ def _best_name(module: Module) -> str: shim_part.symtx = "" shim_part.unit = {} # TODO: support units shim_part.fab_symbol = f_symbol + shim_part.sch_symbol = sch_sym shim_part.bare_bbox = BBox( *[Point(*pts) for pts in Transformer.get_bbox(sch_lib_symbol_units)] ) @@ -726,7 +727,11 @@ def _best_name(module: Module) -> str: rich.print(pins) for sch_pin in sch_sym.pins: - logger.debug(f"Pin {sch_pin.name=}") + fab_pin = sch_to_fab_pin_map[sch_pin] + + if logger.isEnabledFor(logging.DEBUG): + logger.debug(f"Pin {sch_pin.name=} {fab_pin=}") + try: lib_sch_pin = find( all_sch_lib_pins, @@ -745,7 +750,8 @@ def _best_name(module: Module) -> str: if sch_pin.name in lib_sym_pins_all_units: continue raise ValueError( - f"Pin {sch_pin.name} not found in any unit of symbol {sch_sym.name}" + f"Pin {sch_pin.name} not found in any unit of" + f" symbol {sch_sym.propertys['Reference'].value}" ) assert isinstance(lib_sch_pin, C_lib_symbol.C_symbol.C_pin) @@ -759,10 +765,16 @@ def _best_name(module: Module) -> str: # - stub powery things # - override from symbol layout info trait shim_pin.stub = False + if fab_power_if := fab_pin.get_parent_of_type(F.ElectricPower): + shim_pin.fab_is_gnd = fab_pin.represents is fab_power_if.lv + shim_pin.fab_is_pwr = fab_pin.represents is fab_power_if.hv + else: + shim_pin.fab_is_gnd = False + shim_pin.fab_is_pwr = False shim_pin.x = lib_sch_pin.at.x shim_pin.y = lib_sch_pin.at.y - shim_pin.fab_pin = sch_to_fab_pin_map[sch_pin] + shim_pin.fab_pin = fab_pin shim_pin.sch_pin = sch_pin shim_part.pins.append(shim_pin) From 8ac3cec23405f25dc077091c77226fd3ead0065c Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Mon, 21 Oct 2024 21:57:01 -0700 Subject: [PATCH 51/85] Improve debug view --- .../schematic/kicad/skidl/debug_draw.py | 66 +++++++++++++------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index cabf2157..6f131f78 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -7,9 +7,11 @@ Drawing routines used for debugging place & route. """ +import contextlib +import multiprocessing from collections import defaultdict from random import randint -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generator from .geometry import BBox, Point, Segment, Tx, Vector @@ -283,24 +285,20 @@ def draw_routing( options (dict, optional): Dictionary of options and values. Defaults to {}. """ - # Initialize drawing area. - draw_scr, draw_tx, draw_font = draw_start(bbox) - - # Draw parts. - for part in parts: - draw_part(part, draw_scr, draw_tx, draw_font) + with draw_context(bbox) as (draw_scr, draw_tx, draw_font): + # Draw parts. + for part in parts: + draw_part(part, draw_scr, draw_tx, draw_font) - # Draw wiring. - for wires in node.wires.values(): - for wire in wires: - draw_seg(wire, draw_scr, draw_tx, (255, 0, 255), 3, dot_radius=10) + # Draw wiring. + for wires in node.wires.values(): + for wire in wires: + draw_seg(wire, draw_scr, draw_tx, (255, 0, 255), 3, dot_radius=10) - # Draw other stuff (global routes, switchbox routes, etc.) that has a draw() method. - for stuff in other_stuff: - for obj in stuff: - obj.draw(draw_scr, draw_tx, draw_font, **options) - - draw_end() + # Draw other stuff (global routes, switchbox routes, etc.) that has a draw() method. + for stuff in other_stuff: + for obj in stuff: + obj.draw(draw_scr, draw_tx, draw_font, **options) def draw_clear( @@ -392,9 +390,35 @@ def draw_pause(): running = False -def draw_end(): - """Display drawing and wait for user to close PyGame window.""" +@contextlib.contextmanager +def draw_context(bbox: BBox) -> Generator[tuple["pygame.Surface", Tx, "pygame.font.Font"], None, None]: + """Context manager for drawing.""" import pygame - draw_pause() - pygame.quit() + try: + yield draw_start(bbox) + finally: + # FIXME: this doesn't work properly + # It seems to take a split second longer to actually quit, + # which means exceptions raised here cause the debug window to hang + pygame.quit() + + +@contextlib.contextmanager +def optional_draw_context( + options: dict, + predicate_key: str, + bbox: BBox, + pause_on_exit: bool = True, +) -> Generator[tuple["pygame.Surface", Tx, "pygame.font.Font"], None, None]: + """Context manager for optional drawing.""" + if options.get(predicate_key): + with draw_context(bbox) as (draw_scr, draw_tx, draw_font): + options.update( + {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} + ) + yield + if pause_on_exit: + draw_pause() + else: + yield None, None, None From 83694f2b97b0475f4d0371f43dbc0a7df76711c6 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Mon, 21 Oct 2024 21:59:22 -0700 Subject: [PATCH 52/85] Spit out come schematic --- .../schematic/kicad/skidl/gen_schematic.py | 17 +- .../schematic/kicad/skidl/geometry.py | 6 - .../exporters/schematic/kicad/skidl/node.py | 2 +- .../exporters/schematic/kicad/skidl/place.py | 155 ++++++++---------- .../exporters/schematic/kicad/skidl/route.py | 90 +++++----- .../exporters/schematic/kicad/skidl/shims.py | 62 +++---- .../exporters/schematic/kicad/transformer.py | 155 +++++++++++++++++- src/faebryk/libs/kicad/fileformats_sch.py | 5 +- 8 files changed, 305 insertions(+), 187 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index 554fdc94..90dd0f67 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -675,7 +675,7 @@ def gen_schematic( flatness: float = 0.0, retries: int = 2, **options: Unpack[Options], -): +) -> SchNode: """Create a schematic file from a Circuit object. Args: @@ -715,27 +715,18 @@ def gen_schematic( except PlacementFailure: # Placement failed, so try again. + finalize_parts_and_nets(circuit, **options) continue except RoutingFailure: # Routing failed, so expand routing area ... expansion_factor *= 1.5 # HACK: Ad-hoc increase of expansion factor. # ... and try again. - continue - - finally: - # Clean up. finalize_parts_and_nets(circuit, **options) - - # Generate EESCHEMA code for the schematic. - # TODO: extract into for generating schematic objects - # node_to_eeschema(node) + continue # Place & route was successful if we got here, so exit. - return - - # Clean-up after failure. - finalize_parts_and_nets(circuit, **options) + return node # Exited the loop without successful routing. raise (RoutingFailure) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index 852842bf..9175c3e9 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -108,12 +108,6 @@ def origin(self) -> "Point": """Return the (dx, dy) translation as a Point.""" return Point(self.dx, self.dy) - # This setter doesn't work in Python 2.7.18. - # @origin.setter - # def origin(self, pt): - # """Set the (dx, dy) translation from an (x,y) Point.""" - # self.dx, self.dy = pt.x, pt.y - @property def scale(self) -> float: """Return the scaling factor.""" diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 0aa563e9..ac7fe93d 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -47,7 +47,7 @@ def __init__( self.flattened = False self.parts: list["Part"] = [] self.wires: dict[Net, list[Segment]] = defaultdict(list) - self.junctions = defaultdict(list) + self.junctions: dict[Net, list[Point]] = defaultdict(list) self.tx = Tx() self.bbox = BBox() diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index f1f598c6..1d6f3af3 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -6,6 +6,7 @@ Autoplacer for arranging symbols in a schematic. """ +import contextlib import functools import itertools import math @@ -16,6 +17,7 @@ from .constants import BLK_EXT_PAD, BLK_INT_PAD, GRID from .debug_draw import ( + optional_draw_context, draw_pause, draw_placement, draw_redraw, @@ -1278,40 +1280,30 @@ def place_connected_parts( # Randomly place connected parts. random_placement(parts) - if options.get("draw_placement"): - # Draw the placement for debug purposes. - bbox = get_enclosing_bbox(parts) - draw_scr, draw_tx, draw_font = draw_start(bbox) - options.update( - {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} - ) - - if options.get("compress_before_place"): - central_placement(parts, **options) + bbox = get_enclosing_bbox(parts) + with optional_draw_context(options, "draw_placement", bbox): + if options.get("compress_before_place"): + central_placement(parts, **options) - # Do force-directed placement of the parts in the parts. + # Do force-directed placement of the parts in the parts. - # Separate the NetTerminals from the other parts. - net_terminals = [part for part in parts if is_net_terminal(part)] - real_parts = [part for part in parts if not is_net_terminal(part)] + # Separate the NetTerminals from the other parts. + net_terminals = [part for part in parts if is_net_terminal(part)] + real_parts = [part for part in parts if not is_net_terminal(part)] - # Do the first trial placement. - evolve_placement([], real_parts, nets, total_part_force, **options) + # Do the first trial placement. + evolve_placement([], real_parts, nets, total_part_force, **options) - if options.get("rotate_parts"): - # Adjust part orientations after first trial placement is done. - if adjust_orientations(real_parts, **options): - # Some part orientations were changed, so re-do placement. - evolve_placement([], real_parts, nets, total_part_force, **options) + if options.get("rotate_parts"): + # Adjust part orientations after first trial placement is done. + if adjust_orientations(real_parts, **options): + # Some part orientations were changed, so re-do placement. + evolve_placement([], real_parts, nets, total_part_force, **options) - # Place NetTerminals after all the other parts. - place_net_terminals( - net_terminals, real_parts, nets, total_part_force, **options - ) - - if options.get("draw_placement"): - # Pause to look at placement for debugging purposes. - draw_pause() + # Place NetTerminals after all the other parts. + place_net_terminals( + net_terminals, real_parts, nets, total_part_force, **options + ) def place_floating_parts( node: "SchNode", parts: list[Part], **options: Unpack[Options] @@ -1337,44 +1329,36 @@ def place_floating_parts( # Randomly place the floating parts. random_placement(parts) - if options.get("draw_placement"): - # Compute the drawing area for the floating parts - bbox = get_enclosing_bbox(parts) - draw_scr, draw_tx, draw_font = draw_start(bbox) - options.update( - {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} - ) - - # For non-connected parts, do placement based on their similarity to each other. - part_similarity = defaultdict(lambda: defaultdict(lambda: 0)) - for part in parts: - for other_part in parts: - # Don't compute similarity of a part to itself. - if other_part is part: - continue - - # HACK: Get similarity forces right-sized. - part_similarity[part][other_part] = part.similarity(other_part) / 100 - # part_similarity[part][other_part] = 0.1 - - # Select the top-most pin in each part as the anchor point for force-directed placement. - # tx = part.tx - # part.anchor_pin = max(part.anchor_pins, key=lambda pin: (pin.place_pt * tx).y) + bbox = get_enclosing_bbox(parts) + with optional_draw_context(options, "draw_placement", bbox): + # For non-connected parts, do placement based on their similarity to each other. + part_similarity = defaultdict(lambda: defaultdict(lambda: 0)) + for part in parts: + for other_part in parts: + # Don't compute similarity of a part to itself. + if other_part is part: + continue + + # HACK: Get similarity forces right-sized. + part_similarity[part][other_part] = ( + part.similarity(other_part) / 100 + ) + # part_similarity[part][other_part] = 0.1 - force_func = functools.partial( - total_similarity_force, similarity=part_similarity - ) + # Select the top-most pin in each part as the anchor point for force-directed placement. + # tx = part.tx + # part.anchor_pin = max(part.anchor_pins, key=lambda pin: (pin.place_pt * tx).y) - if options.get("compress_before_place"): - # Compress all floating parts together. - central_placement(parts, **options) + force_func = functools.partial( + total_similarity_force, similarity=part_similarity + ) - # Do force-directed placement of the parts in the group. - evolve_placement([], parts, [], force_func, **options) + if options.get("compress_before_place"): + # Compress all floating parts together. + central_placement(parts, **options) - if options.get("draw_placement"): - # Pause to look at placement for debugging purposes. - draw_pause() + # Do force-directed placement of the parts in the group. + evolve_placement([], parts, [], force_func, **options) def place_blocks( node: "SchNode", @@ -1489,33 +1473,24 @@ def place_blocks( # Start off with a random placement of part blocks. random_placement(part_blocks) - if options.get("draw_placement"): - # Setup to draw the part block placement for debug purposes. - bbox = get_enclosing_bbox(part_blocks) - draw_scr, draw_tx, draw_font = draw_start(bbox) - options.update( - {"draw_scr": draw_scr, "draw_tx": draw_tx, "draw_font": draw_font} - ) - - # Arrange the part blocks with similarity force-directed placement. - force_func = functools.partial(total_similarity_force, similarity=blk_attr) - evolve_placement([], part_blocks, [], force_func, **options) - - if options.get("draw_placement"): - # Pause to look at placement for debugging purposes. - draw_pause() - - # Apply the placement moves of the part blocks to their underlying sources. - for blk in part_blocks: - try: - # Update the Tx matrix of the source (usually a child node). - blk.src.tx = blk.tx - except AttributeError: - # The source doesn't have a Tx so it must be a collection of parts. - # Apply the block placement to the Tx of each part. - assert isinstance(blk.src, list) - for part in blk.src: - part.tx *= blk.tx + bbox = get_enclosing_bbox(part_blocks) + + with optional_draw_context(options, "draw_placement", bbox): + # Arrange the part blocks with similarity force-directed placement. + force_func = functools.partial(total_similarity_force, similarity=blk_attr) + evolve_placement([], part_blocks, [], force_func, **options) + + # Apply the placement moves of the part blocks to their underlying sources. + for blk in part_blocks: + try: + # Update the Tx matrix of the source (usually a child node). + blk.src.tx = blk.tx + except AttributeError: + # The source doesn't have a Tx so it must be a collection of parts. + # Apply the block placement to the Tx of each part. + assert isinstance(blk.src, list) + for part in blk.src: + part.tx *= blk.tx def get_attrs(node: "SchNode"): """Return dict of attribute sets for the parts, pins, and nets in a node.""" diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index 5a0b79fc..53ae7d83 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -7,6 +7,7 @@ Autorouter for generating wiring between symbols in a schematic. """ +import contextlib import copy import random from collections import Counter, defaultdict @@ -16,11 +17,11 @@ from .constants import DRAWING_BOX_RESIZE, GRID from .debug_draw import ( - draw_end, + draw_context, draw_endpoint, + draw_pause, draw_routing, draw_seg, - draw_start, draw_text, ) from .geometry import BBox, Point, Segment, Tx, Vector @@ -2045,52 +2046,59 @@ def draw( options (dict, optional): Dictionary of options and values. Defaults to {}. """ + # Nothing to see here. + if ( + options.get("draw_switchbox_boundary") + and not options.get("draw_switchbox_routing") + and not options.get("draw_routing_channels") + ): + return + # If the screen object is not None, then PyGame drawing is enabled so set flag # to initialize PyGame. - do_start_end = not bool(scr) - - if do_start_end: + if bool(scr): + ctx = contextlib.nullcontext(enter_result=(scr, tx, font)) + else: # Initialize PyGame. - scr, tx, font = draw_start( + ctx = draw_context( self.bbox.resize(Vector(DRAWING_BOX_RESIZE, DRAWING_BOX_RESIZE)) ) - if options.get("draw_switchbox_boundary"): - # Draw switchbox boundary. - self.top_face.draw(scr, tx, font, color, thickness, **options) - self.bottom_face.draw(scr, tx, font, color, thickness, **options) - self.left_face.draw(scr, tx, font, color, thickness, **options) - self.right_face.draw(scr, tx, font, color, thickness, **options) + with ctx as (scr, tx, font): + if options.get("draw_switchbox_boundary"): + # Draw switchbox boundary. + self.top_face.draw(scr, tx, font, color, thickness, **options) + self.bottom_face.draw(scr, tx, font, color, thickness, **options) + self.left_face.draw(scr, tx, font, color, thickness, **options) + self.right_face.draw(scr, tx, font, color, thickness, **options) - if options.get("draw_switchbox_routing"): - # Draw routed wire segments. - try: - for segments in self.segments.values(): - for segment in segments: - draw_seg(segment, scr, tx, dot_radius=0) - except AttributeError: - pass - - if options.get("draw_routing_channels"): - # Draw routing channels from midpoint of one switchbox face to midpoint of another. - - def draw_channel(face1: Face, face2: Face) -> None: - seg1 = face1.seg - seg2 = face2.seg - p1 = (seg1.p1 + seg1.p2) / 2 - p2 = (seg2.p1 + seg2.p2) / 2 - draw_seg(Segment(p1, p2), scr, tx, (128, 0, 128), 1, dot_radius=0) - - draw_channel(self.top_face, self.bottom_face) - draw_channel(self.top_face, self.left_face) - draw_channel(self.top_face, self.right_face) - draw_channel(self.bottom_face, self.left_face) - draw_channel(self.bottom_face, self.right_face) - draw_channel(self.left_face, self.right_face) - - if do_start_end: - # Terminate PyGame. - draw_end() + if options.get("draw_switchbox_routing"): + # Draw routed wire segments. + try: + for segments in self.segments.values(): + for segment in segments: + draw_seg(segment, scr, tx, dot_radius=0) + except AttributeError: + pass + + if options.get("draw_routing_channels"): + # Draw routing channels from midpoint of one switchbox face to midpoint of another. + + def draw_channel(face1: Face, face2: Face) -> None: + seg1 = face1.seg + seg2 = face2.seg + p1 = (seg1.p1 + seg1.p2) / 2 + p2 = (seg2.p1 + seg2.p2) / 2 + draw_seg(Segment(p1, p2), scr, tx, (128, 0, 128), 1, dot_radius=0) + + draw_channel(self.top_face, self.bottom_face) + draw_channel(self.top_face, self.left_face) + draw_channel(self.top_face, self.right_face) + draw_channel(self.bottom_face, self.left_face) + draw_channel(self.bottom_face, self.right_face) + draw_channel(self.left_face, self.right_face) + + draw_pause() class Router: diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 7474d342..aaf67aff 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -69,26 +69,29 @@ class Part: # A string of H, V, L, R operations that are applied in sequence left-to-right. symtx: str unit: dict[str, "PartUnit"] # units within the part, empty is this is all it is + bare_bbox: BBox # things we've added to make life easier sch_symbol: "fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance" fab_symbol: F.Symbol | None - bare_bbox: BBox _similarites: dict["Part", float] def audit(self) -> None: """Ensure mandatory attributes are set""" - audit_has(self, [ - "hierarchy", - "pins", - "ref", - "symtx", - "unit", - "fab_symbol", - "sch_symbol", - "bare_bbox", - "_similarites", - ]) + audit_has( + self, + [ + "hierarchy", + "pins", + "ref", + "symtx", + "unit", + "fab_symbol", + "sch_symbol", + "bare_bbox", + "_similarites", + ], + ) # don't audit pins, they're handled through nets instead for unit in self.unit.values(): @@ -100,7 +103,8 @@ def audit(self) -> None: delta_cost_tx: Tx # transformation matrix associated with delta_cost delta_cost: float # the largest decrease in cost and the associated orientation. force: Vector # used for debugging - lbl_bbox: BBox + bbox: BBox + lbl_bbox: BBox # The bbox and lbl_bbox are tied together left_track: "GlobalTrack" mv: Vector # internal use # whether the part's orientation is locked, based on symtx or pin count @@ -128,7 +132,7 @@ def __hash__(self) -> int: @property def draw(self): - # TODO: + """This was used in SKiDL to capture all the drawn elements in a design.""" raise NotImplementedError def grab_pins(self) -> None: @@ -190,22 +194,24 @@ class Pin: def audit(self) -> None: """Ensure mandatory attributes are set""" - audit_has(self, [ - "name", - "net", - "num", - "orientation", - "part", - "stub", - "x", - "y", - "fab_pin", - "fab_is_gnd", - "fab_is_pwr", - ]) + audit_has( + self, + [ + "name", + "net", + "num", + "orientation", + "part", + "stub", + "x", + "y", + "fab_pin", + "fab_is_gnd", + "fab_is_pwr", + ], + ) # internal use - bbox: BBox face: "Face" place_pt: Point pt: Point diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index d08ea984..3a2382e4 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -3,10 +3,11 @@ import hashlib import logging +import math import pprint from copy import deepcopy from functools import singledispatchmethod -from itertools import chain, groupby +from itertools import chain from os import PathLike from pathlib import Path from typing import Any, List, Protocol, Unpack @@ -22,6 +23,8 @@ # import numpy as np # from shapely import Polygon from faebryk.exporters.pcb.kicad.transformer import is_marked +from faebryk.exporters.schematic.kicad.skidl import gen_schematic as skidl_sch +from faebryk.exporters.schematic.kicad.skidl import node as skidl_node from faebryk.exporters.schematic.kicad.skidl import shims from faebryk.libs.exceptions import FaebrykException from faebryk.libs.geometry.basic import Geometry @@ -162,7 +165,9 @@ def attach_symbol(self, f_symbol: F.Symbol, sym_inst: SCH.C_symbol_instance): # Attach the pins on the symbol to the module interface for pin in sym_inst.pins: - f_symbol.pins[pin.name].add(Transformer.has_linked_sch_pins([pin], sym_inst)) + f_symbol.pins[pin.name].add( + Transformer.has_linked_sch_pins([pin], sym_inst) + ) # TODO: remove cleanup, it shouldn't really be required if we're marking propertys # def cleanup(self): @@ -391,13 +396,26 @@ def insert_wire( ) ) + def insert_junction( + self, + coord: Geometry.Point2D, + ): + self.sch.junctions.append( + SCH.C_junction( + at=C_xy(coord[0], coord[1]), + ) + ) + def insert_text( self, text: str, at: C_xyr, - font: Font, + font: Font | None = None, alignment: Alignment | None = None, ): + if font is None: + font = self.font + self.sch.texts.append( SCH.C_text( text=text, @@ -410,6 +428,35 @@ def insert_text( ) ) + def insert_global_label( + self, + text: str, + shape: SCH.C_global_label.E_shape, + at: C_xyr, + font: Font | None = None, + alignment: Alignment | None = None, + ): + if font is None: + font = self.font + + if alignment is None: + justifys = [] + else: + justifys = [C_effects.C_justify(justifys=[alignment])] + + self.sch.global_labels.append( + SCH.C_global_label( + shape=shape, + text=text, + at=at, + effects=C_effects( + font=font, + justifys=justifys, + ), + uuid=self.gen_uuid(mark=True), + ) + ) + def _ensure_lib_symbol( self, lib_id: str, @@ -616,6 +663,7 @@ def _add_missing_symbols(self): def _build_shim_circuit(self) -> shims.Circuit: """Does what it says on the tin.""" + # TODO: add existing wire locations from faebryk.exporters.schematic.kicad.skidl.geometry import BBox, Point # 1.1 create hollow circuits to append to @@ -772,8 +820,8 @@ def _best_name(module: Module) -> str: shim_pin.fab_is_gnd = False shim_pin.fab_is_pwr = False - shim_pin.x = lib_sch_pin.at.x - shim_pin.y = lib_sch_pin.at.y + shim_pin.x = lib_sch_pin.at.x * 39.3701 + shim_pin.y = lib_sch_pin.at.y * 39.3701 shim_pin.fab_pin = fab_pin shim_pin.sch_pin = sch_pin @@ -843,6 +891,97 @@ def score_pins(): circuit.audit() return circuit + def _apply_shim_sch_node( + self, node: skidl_node.SchNode, sheet_tx: skidl_node.Tx = skidl_node.Tx() + ): + from faebryk.exporters.schematic.kicad.skidl.geometry import ( + Tx, + tx_rot_0, + tx_rot_90, + tx_rot_180, + tx_rot_270, + ) + + def get_common_rotation(part_tx: Tx) -> float | None: + def _check(tx: Tx) -> bool: + return ( + math.isclose(tx.a, part_tx.a / part_tx.scale) + and math.isclose(tx.b, part_tx.b / part_tx.scale) + and math.isclose(tx.c, part_tx.c / part_tx.scale) + and math.isclose(tx.d, part_tx.d / part_tx.scale) + ) + + if _check(tx_rot_0): + return 0 + elif _check(tx_rot_90): + return 90 + elif _check(tx_rot_180): + return 180 + elif _check(tx_rot_270): + return 270 + return None + + mils_to_mm = Tx(a=0.0254, b=0, c=0, d=0.0254, dx=0, dy=0) + + # if node.flattened: + # # Create the transformation matrix for the placement of the parts + # # in the node. + # tx = node.tx * sheet_tx * mils_to_mm + # else: + # flattened_bbox = node.internal_bbox() + # tx = skidl_sch.calc_sheet_tx(flattened_bbox) * mils_to_mm + tx = node.tx * mils_to_mm + + # TODO: Put flattened node into heirarchical block + + for part in node.parts: + ## 2 add global labels + part_tx = part.tx * tx + if isinstance(part, skidl_sch.NetTerminal): + assert isinstance(tx, skidl_node.Tx) + pin = part.pins[0] + pt = pin.pt * part_tx + rotation = { + "R": 0, + "D": 90, + "L": 180, + "U": 270, + }[skidl_sch.calc_pin_dir(pin)] + self.insert_global_label( + pin.net.name, + at=C_xyr(x=pt.x, y=pt.y, r=rotation), + shape=SCH.C_global_label.E_shape.bidirectional, + ) + else: + ## 3. modify parts + # unlike Skidl, we're working under the assumption that all the parts + # are already in the schematic, we just need to move them + part.sch_symbol.at.x = part_tx.origin.x + part.sch_symbol.at.y = part_tx.origin.y + part.sch_symbol.at.r = get_common_rotation(part_tx) or 0 + + # 4. draw wires and junctions + for _, wire in node.wires.items(): + skidl_points = [p * tx for seg in wire for p in [seg.p1, seg.p2]] + points = [(p.x, p.y) for p in skidl_points] + self.insert_wire(points) + + for _, junctions in node.junctions.items(): + for junc in junctions: + pt = junc * tx + self.insert_junction((pt.x, pt.y)) + + # 5. TODO: pin labels for stubbed nets + + def apply_shim_sch_node(self, circuit: skidl_node.SchNode): + def dfs(node: skidl_node.SchNode): + yield node + for child in node.children.values(): + yield from dfs(child) + + for node in dfs(circuit): + self._apply_shim_sch_node(node) + def generate_schematic(self, **options: Unpack[shims.Options]): """Does what it says on the tin.""" # 1. add missing symbols @@ -854,6 +993,10 @@ def generate_schematic(self, **options: Unpack[shims.Options]): # 3. run skidl schematic generation from faebryk.exporters.schematic.kicad.skidl.gen_schematic import gen_schematic - gen_schematic(circuit, ".", "test", **options) + sch = gen_schematic(circuit, ".", "test", **options) # 4. transform sch according to skidl + self.apply_shim_sch_node(sch) + + # 5. save + return self.sch diff --git a/src/faebryk/libs/kicad/fileformats_sch.py b/src/faebryk/libs/kicad/fileformats_sch.py index ae2259d8..cc9b0859 100644 --- a/src/faebryk/libs/kicad/fileformats_sch.py +++ b/src/faebryk/libs/kicad/fileformats_sch.py @@ -241,8 +241,9 @@ class C_pin: @dataclass class C_junction: at: C_xy - diameter: float - color: tuple[int, int, int, int] + diameter: float = 0 # 0 indicates default diameter + # (0,0,0,0) indicates KiCAD default color + color: tuple[int, int, int, int] = (0, 0, 0, 0) uuid: UUID = uuid_field() @dataclass From d9beda014d5f24766e6ea0eb8c202b198c1b9bf7 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 22 Oct 2024 19:55:26 -0700 Subject: [PATCH 53/85] Fix pin orientations --- .../exporters/schematic/kicad/skidl/debug_draw.py | 1 - .../exporters/schematic/kicad/skidl/route.py | 12 ++++++++++++ .../exporters/schematic/kicad/skidl/shims.py | 8 ++++---- .../exporters/schematic/kicad/transformer.py | 13 ++++++++++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index 6f131f78..d17879f9 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -8,7 +8,6 @@ """ import contextlib -import multiprocessing from collections import defaultdict from random import randint from typing import TYPE_CHECKING, Generator diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index 53ae7d83..b3e56977 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -9,6 +9,7 @@ import contextlib import copy +import logging import random from collections import Counter, defaultdict from enum import Enum @@ -32,6 +33,10 @@ from .node import SchNode + +logger = logging.getLogger(__name__) + + ################################################################### # # OVERVIEW OF SCHEMATIC AUTOROUTER @@ -2251,16 +2256,19 @@ def create_terminals( closest_dist = abs(pt.y - part.top_track.coord) pin_track = part.top_track coord = pt.x # Pin coord within top track. + dist = abs(pt.y - part.bottom_track.coord) if dist < closest_dist: closest_dist = dist pin_track = part.bottom_track coord = pt.x # Pin coord within bottom track. + dist = abs(pt.x - part.left_track.coord) if dist < closest_dist: closest_dist = dist pin_track = part.left_track coord = pt.y # Pin coord within left track. + dist = abs(pt.x - part.right_track.coord) if dist < closest_dist: closest_dist = dist @@ -2280,6 +2288,10 @@ def create_terminals( terminal = Terminal(pin.net, face, coord) face.terminals.append(terminal) break + else: + raise RuntimeError( + f"{pin.part.ref=} {pin.name=} not assigned to a face." + ) # Set routing capacity of faces based on # of terminals on each face. for track in h_tracks + v_tracks: diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index aaf67aff..6cf33408 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -43,13 +43,13 @@ def rmv_attr(objs, attrs): def angle_to_orientation(angle: float) -> str: """Convert an angle to an orientation""" if angle == 0: - return "U" + return "L" elif angle == 90: - return "R" - elif angle == 180: return "D" + elif angle == 180: + return "R" elif angle == 270: - return "L" + return "U" else: raise ValueError(f"Invalid angle: {angle}") diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 3a2382e4..c441c251 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -82,6 +82,10 @@ class _HasPropertys(Protocol): propertys: dict[str, C_property] +def mm_to_mil(mm: float) -> float: + return mm * 1000 / 25.4 + + # TODO: consider common transformer base class Transformer: class has_linked_sch_symbol(F.Symbol.TraitT.decless()): @@ -758,7 +762,10 @@ def _best_name(module: Module) -> str: shim_part.fab_symbol = f_symbol shim_part.sch_symbol = sch_sym shim_part.bare_bbox = BBox( - *[Point(*pts) for pts in Transformer.get_bbox(sch_lib_symbol_units)] + *[ + Point(mm_to_mil(pts[0]), mm_to_mil(pts[1])) + for pts in Transformer.get_bbox(sch_lib_symbol_units) + ] ) shim_part.pins = [] @@ -820,8 +827,8 @@ def _best_name(module: Module) -> str: shim_pin.fab_is_gnd = False shim_pin.fab_is_pwr = False - shim_pin.x = lib_sch_pin.at.x * 39.3701 - shim_pin.y = lib_sch_pin.at.y * 39.3701 + shim_pin.x = mm_to_mil(lib_sch_pin.at.x) + shim_pin.y = mm_to_mil(lib_sch_pin.at.y) shim_pin.fab_pin = fab_pin shim_pin.sch_pin = sch_pin From 4afbc91e7ec3b4baa3d9797d12075aa6b8613f51 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 22 Oct 2024 20:59:27 -0700 Subject: [PATCH 54/85] Improve marking --- .../exporters/schematic/kicad/transformer.py | 127 +++++++----------- src/faebryk/libs/kicad/fileformats_common.py | 5 +- src/faebryk/libs/kicad/fileformats_sch.py | 2 +- 3 files changed, 55 insertions(+), 79 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index c441c251..cab9e261 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -22,7 +22,6 @@ # import numpy as np # from shapely import Polygon -from faebryk.exporters.pcb.kicad.transformer import is_marked from faebryk.exporters.schematic.kicad.skidl import gen_schematic as skidl_sch from faebryk.exporters.schematic.kicad.skidl import node as skidl_node from faebryk.exporters.schematic.kicad.skidl import shims @@ -122,8 +121,8 @@ def __init__( ) self.font = FONT - # TODO: figure out what to do with cleanup - # self.cleanup() + if cleanup: + self.cleanup() self.attach() def attach(self): @@ -163,6 +162,13 @@ def attach(self): logger.debug(f"Attached: {pprint.pformat(attached)}") logger.debug(f"Missing: {pprint.pformat(self.missing_symbols)}") + def cleanup(self): + """Remove everything we generated in the past.""" + self.sch.symbols = list( + filter(lambda x: not self.check_mark(x), self.sch.symbols) + ) + self.sch.wires = list(filter(lambda x: not self.check_mark(x), self.sch.wires)) + def attach_symbol(self, f_symbol: F.Symbol, sym_inst: SCH.C_symbol_instance): """Bind the module and symbol together on the graph""" f_symbol.add(self.has_linked_sch_symbol(sym_inst)) @@ -173,23 +179,6 @@ def attach_symbol(self, f_symbol: F.Symbol, sym_inst: SCH.C_symbol_instance): Transformer.has_linked_sch_pins([pin], sym_inst) ) - # TODO: remove cleanup, it shouldn't really be required if we're marking propertys - # def cleanup(self): - # """Delete faebryk-created objects in schematic.""" - - # # find all objects with path_len 2 (direct children of a list in pcb) - # candidates = [o for o in dataclass_dfs(self.sch) if len(o[1]) == 2] - # for obj, path, _ in candidates: - # if not self.check_mark(obj): - # continue - - # # delete object by removing it from the container they are in - # holder = path[-1] - # if isinstance(holder, list): - # holder.remove(obj) - # elif isinstance(holder, dict): - # del holder[get_key(obj, holder)] - def index_symbol_files( self, fp_lib_tables: PathLike | list[PathLike], load_globals: bool = True ) -> None: @@ -284,30 +273,6 @@ def get_sub_syms( def get_unit_count(lib_sym: C_lib_symbol) -> int: return max(int(name.split("_")[1]) for name in lib_sym.symbols.keys()) or 1 - # TODO: remove - # @singledispatchmethod - # def get_lib_pin(self, pin) -> SCH.C_lib_symbols.C_symbol.C_symbol.C_pin: - # raise NotImplementedError(f"Don't know how to get lib pin for {type(pin)}") - - # @get_lib_pin.register - # def _(self, pin: F.Symbol.Pin) -> SCH.C_lib_symbols.C_symbol.C_symbol.C_pin: - # graph_symbol, _ = pin.get_parent() - # assert isinstance(graph_symbol, Node) - # lib_sym = self.get_lib_syms(graph_symbol) - # units = self.get_lib_syms(lib_sym) - # sym = graph_symbol.get_trait(Transformer.has_linked_sch_symbol).symbol - - # def _name_filter(sch_pin: SCH.C_lib_symbols.C_symbol.C_symbol.C_pin): - # return sch_pin.name in { - # p.name for p in pin.get_trait(self.has_linked_sch_pins).pins - # } - - # lib_pin = find( - # chain.from_iterable(u.pins for u in units[sym.unit]), - # _name_filter, - # ) - # return lib_pin - # Marking ------------------------------------------------------------------------- """ There are two methods to mark objects in the schematic: @@ -319,28 +284,18 @@ def get_unit_count(lib_sym: C_lib_symbol) -> int: Anything generated by this transformer is marked. """ - @staticmethod - def gen_uuid(mark: bool = False) -> UUID: - return _gen_uuid(mark="FBRK" if mark else "") - - @staticmethod - def is_uuid_marked(obj) -> bool: - if not hasattr(obj, "uuid"): - return False - assert isinstance(obj.uuid, str) - suffix = "FBRK".encode().hex() - return obj.uuid.replace("-", "").endswith(suffix) - @staticmethod def hash_contents(obj) -> str: """Hash the contents of an object, minus the mark""" - # filter out mark properties + # filter out mark properties and UUID def _filter(k: tuple[Any, list[Any], list[str]]) -> bool: - obj, _, _ = k + obj, _, name_path = k if isinstance(obj, C_property): if obj.name == "faebryk_mark": return False + if name_path and name_path[-1] == "uuid": + return False return True hasher = hashlib.blake2b() @@ -350,37 +305,54 @@ def _filter(k: tuple[Any, list[Any], list[str]]) -> bool: return hasher.hexdigest() @staticmethod - def check_mark(obj) -> bool: - """Return True if an object is validly marked""" + def check_mark(obj: _HasUUID | _HasPropertys) -> bool: + """ + Return True if an object is validly marked + + Items that have the capacity to be marked + via propertys are only considered marked + if they have the property and it's valid, + despite their uuid + """ if hasattr(obj, "propertys"): if "faebryk_mark" in obj.propertys: prop = obj.propertys["faebryk_mark"] assert isinstance(prop, C_property) return prop.value == Transformer.hash_contents(obj) - else: - # items that have the capacity to be marked - # via propertys are only considered marked - # if they have the property and it's valid, - # despite their uuid - return False - return Transformer.is_uuid_marked(obj) + return False + + if hasattr(obj, "uuid"): + assert isinstance(obj.uuid, str) + suffix = Transformer.hash_contents(obj).encode().hex() + uuid = obj.uuid.replace("-", "") + return uuid == suffix[: len(uuid)] + + return False @staticmethod def mark[R: _HasUUID | _HasPropertys](obj: R) -> R: """Mark the property if possible, otherwise ensure the uuid is marked""" + + # If there's already a valid mark, do nothing + # This is important to maintain consistent UUID marking + if Transformer.check_mark(obj): + return obj + + hashed_contents = Transformer.hash_contents(obj) + if hasattr(obj, "propertys"): obj.propertys["faebryk_mark"] = C_property( name="faebryk_mark", - value=Transformer.hash_contents(obj), + value=hashed_contents, ) + return obj - else: - if not hasattr(obj, "uuid"): - raise TypeError(f"Object {obj} has no propertys or uuid") + elif hasattr(obj, "uuid"): + obj.uuid = _gen_uuid(hashed_contents) - if not is_marked(obj): - obj.uuid = Transformer.gen_uuid(mark=True) + else: + raise TypeError(f"Object {obj} has no propertys or uuid") return obj @@ -397,6 +369,7 @@ def insert_wire( SCH.C_wire( pts=C_pts(xys=[C_xy(*coord) for coord in section]), stroke=stroke or C_stroke(), + uuid=_gen_uuid(), ) ) @@ -428,7 +401,7 @@ def insert_text( font=font, justify=alignment, ), - uuid=self.gen_uuid(mark=True), + uuid=_gen_uuid(), ) ) @@ -457,7 +430,7 @@ def insert_global_label( font=font, justifys=justifys, ), - uuid=self.gen_uuid(mark=True), + uuid=_gen_uuid(), ) ) @@ -514,7 +487,7 @@ def insert_symbol( pins.append( SCH.C_symbol_instance.C_pin( name=pin.number.number, - uuid=self.gen_uuid(mark=True), + uuid=_gen_uuid(), ) ) @@ -525,7 +498,7 @@ def insert_symbol( in_bom=True, on_board=True, pins=pins, - uuid=self.gen_uuid(mark=True), + uuid=_gen_uuid(), ) # It's one of ours, until it's modified in KiCAD diff --git a/src/faebryk/libs/kicad/fileformats_common.py b/src/faebryk/libs/kicad/fileformats_common.py index 1ba7a7b5..e2ad87ab 100644 --- a/src/faebryk/libs/kicad/fileformats_common.py +++ b/src/faebryk/libs/kicad/fileformats_common.py @@ -153,7 +153,10 @@ def gen_uuid(mark: str = ""): suffix = mark.encode().hex() if suffix: - value = value[: -len(suffix)] + suffix + if len(suffix) >= len(value): + value = suffix[: len(value)] + else: + value = value[: -len(suffix)] + suffix DASH_IDX = [8, 12, 16, 20] formatted = value diff --git a/src/faebryk/libs/kicad/fileformats_sch.py b/src/faebryk/libs/kicad/fileformats_sch.py index cc9b0859..4e7e0a26 100644 --- a/src/faebryk/libs/kicad/fileformats_sch.py +++ b/src/faebryk/libs/kicad/fileformats_sch.py @@ -343,7 +343,7 @@ class C_bus_entry: stroke: C_stroke uuid: UUID = uuid_field() - version: int = field(**sexp_field(assert_value=20211123)) + version: int generator: str paper: str uuid: UUID = uuid_field() From 29af66f67abda27c81d1d79c2853e7d575b5587e Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 23 Oct 2024 11:23:37 -0700 Subject: [PATCH 55/85] Fix attachment of existing symbols --- .../exporters/schematic/kicad/transformer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index cab9e261..9d3c4f20 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -128,7 +128,7 @@ def __init__( def attach(self): """This function matches and binds symbols to their symbols""" # reference (eg. C3) to symbol (eg. "Capacitor_SMD:C_0402") - symbols = { + sch_symbols = { (f.propertys["Reference"].value, f.lib_id): f for f in self.sch.symbols } for node, sym_trait in self.graph.nodes_with_trait(F.Symbol.has_symbol): @@ -137,22 +137,22 @@ def attach(self): logger.warning(f"Symbol {sym_trait.reference} has no overriden name") continue - symbol = sym_trait.reference + f_symbol = sym_trait.reference - if not symbol.has_trait(F.Symbol.has_kicad_symbol): + if not f_symbol.has_trait(F.Symbol.has_kicad_symbol): # KiCAD symbols are typically generated by the lcsc picker - logger.warning(f"Symbol {symbol} has no kicad symbol") + logger.warning(f"Symbol {f_symbol} has no kicad symbol") # TODO: generate a bug instead of skipping continue sym_ref = node.get_trait(F.has_overriden_name).get_name() - sym_name = symbol.get_trait(F.Symbol.has_kicad_symbol).symbol_name + sym_name = f_symbol.get_trait(F.Symbol.has_kicad_symbol).symbol_name - if (sym_ref, sym_name) not in symbols: - self.missing_symbols.append(symbol) + if (sym_ref, sym_name) not in sch_symbols: + self.missing_symbols.append(f_symbol) continue - self.attach_symbol(node, symbols[(sym_ref, sym_name)]) + self.attach_symbol(f_symbol, sch_symbols[(sym_ref, sym_name)]) # Log what we were able to attach attached = { From 641ec5bac05b664a8af95942ace57787f0b57b53 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 23 Oct 2024 11:29:09 -0700 Subject: [PATCH 56/85] Fix hierarchy strings to flatten design --- src/faebryk/exporters/schematic/kicad/transformer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 9d3c4f20..f030bdb9 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -706,7 +706,12 @@ def _build_shim_circuit(self) -> shims.Circuit: def _hierarchy(module: Module) -> str: """ Make a string representation of the module's hierarchy - using the best name for each part we have + using the best name for each part we have. + + NOTE: The hierarchy is used to determine whether parts live in the same module. + Top level should be unnamed, eg "" + Subsequent levels should be named and dot-separated + Part's ref should not be included at the end eg, don't do "led.U1" """ def _best_name(module: Module) -> str: @@ -715,7 +720,7 @@ def _best_name(module: Module) -> str: return module.get_name() # skip the root module, because it's name is just "*" - hierarchy = [h[0] for h in module.get_hierarchy()][1:] + hierarchy = [h[0] for h in module.get_hierarchy()][1:-1] return ".".join(_best_name(n) for n in hierarchy) # for each sch_symbol, create a shim part @@ -726,7 +731,7 @@ def _best_name(module: Module) -> str: shim_part.ref = sch_sym.propertys["Reference"].value # if we don't have a fab symbol, place the part at the top of the hierarchy shim_part.hierarchy = ( - _hierarchy(f_symbol.represents) if f_symbol else shim_part.ref + _hierarchy(f_symbol.represents) if f_symbol else "" ) # TODO: what's the ato analog? # TODO: should this desc From 28e7f6f5d1e2cd8d0211f33f4c200ece79208fa0 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 23 Oct 2024 15:43:38 -0700 Subject: [PATCH 57/85] Fix and test marking --- .../exporters/schematic/kicad/transformer.py | 121 ++++++++++++------ .../exporters/schematic/kicad/test_marking.py | 43 +++++++ 2 files changed, 124 insertions(+), 40 deletions(-) create mode 100644 test/exporters/schematic/kicad/test_marking.py diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index f030bdb9..b750df18 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -157,17 +157,22 @@ def attach(self): # Log what we were able to attach attached = { n: t.symbol - for n, t in self.graph.nodes_with_trait(Transformer.has_linked_sch_symbol) + for n, t in self.graph.nodes_with_trait(self.has_linked_sch_symbol) } logger.debug(f"Attached: {pprint.pformat(attached)}") logger.debug(f"Missing: {pprint.pformat(self.missing_symbols)}") def cleanup(self): """Remove everything we generated in the past.""" - self.sch.symbols = list( - filter(lambda x: not self.check_mark(x), self.sch.symbols) - ) - self.sch.wires = list(filter(lambda x: not self.check_mark(x), self.sch.wires)) + + def _filter(obj) -> bool: + if self.check_mark(obj): + logger.debug(f"Removing marked {obj.uuid}") + return True + return False + + self.sch.symbols = list(filter(_filter, self.sch.symbols)) + self.sch.wires = list(filter(_filter, self.sch.wires)) def attach_symbol(self, f_symbol: F.Symbol, sym_inst: SCH.C_symbol_instance): """Bind the module and symbol together on the graph""" @@ -213,14 +218,14 @@ def flipped[T](input_list: list[tuple[T, int]]) -> list[tuple[T, int]]: return [(x, (y + 180) % 360) for x, y in reversed(input_list)] # Getter --------------------------------------------------------------------------- - @staticmethod - def get_symbol(cmp: Node) -> F.Symbol: - return not_none(cmp.get_trait(Transformer.has_linked_sch_symbol)).symbol + @classmethod + def get_symbol(cls, cmp: Node) -> F.Symbol: + return not_none(cmp.get_trait(cls.has_linked_sch_symbol)).symbol def get_all_symbols(self) -> List[tuple[Module, F.Symbol]]: return [ (cast_assert(Module, cmp), t.symbol) - for cmp, t in self.graph.nodes_with_trait(Transformer.has_linked_sch_symbol) + for cmp, t in self.graph.nodes_with_trait(self.has_linked_sch_symbol) ] @once @@ -284,28 +289,58 @@ def get_unit_count(lib_sym: C_lib_symbol) -> int: Anything generated by this transformer is marked. """ - @staticmethod - def hash_contents(obj) -> str: - """Hash the contents of an object, minus the mark""" + MARK_NAME = "faebryk_mark" + + + @classmethod + def _get_hash_contents(cls, obj_to_hash: Any) -> dict[tuple[str, ...], Any]: + """ + Helper to get the contents of an object which belongs in it's mark + This is a separate function for validation purposes + """ + content_to_hash = {} + + for obj, _, name_path in dataclass_dfs(obj_to_hash): + # Covnert the name path to a tuple, so it's hashable + name_path = tuple(name_path) + + # Skip certain unstable things (like the mark we're about to add) + if isinstance(obj, C_property) and obj.name == cls.MARK_NAME: + continue + + if f"['{cls.MARK_NAME}']" in name_path: + continue + + if name_path and name_path[-1] == ".uuid": + continue + + # Skip collections, their children will be added if relevant + if isinstance(obj, (list, dict, tuple)) or is_dataclass(obj): + continue - # filter out mark properties and UUID - def _filter(k: tuple[Any, list[Any], list[str]]) -> bool: - obj, _, name_path = k - if isinstance(obj, C_property): - if obj.name == "faebryk_mark": - return False - if name_path and name_path[-1] == "uuid": - return False - return True + # Tweak properties to be more stable + # Convert all numbers to 2 decimal place floating points + if isinstance(obj, (int, float)): + obj = round(float(obj), 2) + + content_to_hash[name_path] = obj + + + return dict(sorted(content_to_hash.items(), key=lambda x: x[0])) + + @classmethod + def hash_contents(cls, obj_to_hash: Any) -> str: + """Hash the contents of an object, minus the mark""" + content_to_hash = cls._get_hash_contents(obj_to_hash) hasher = hashlib.blake2b() - for obj, _, name_path in filter(_filter, dataclass_dfs(obj)): - hasher.update(f"{name_path}={obj}".encode()) + for name, value in content_to_hash.items(): + hasher.update(f"{name}: {value}".encode()) return hasher.hexdigest() - @staticmethod - def check_mark(obj: _HasUUID | _HasPropertys) -> bool: + @classmethod + def check_mark(cls, obj: _HasUUID | _HasPropertys) -> bool: """ Return True if an object is validly marked @@ -315,35 +350,30 @@ def check_mark(obj: _HasUUID | _HasPropertys) -> bool: despite their uuid """ if hasattr(obj, "propertys"): - if "faebryk_mark" in obj.propertys: - prop = obj.propertys["faebryk_mark"] + if cls.MARK_NAME in obj.propertys: + prop = obj.propertys[cls.MARK_NAME] assert isinstance(prop, C_property) - return prop.value == Transformer.hash_contents(obj) + return prop.value == cls.hash_contents(obj) return False if hasattr(obj, "uuid"): assert isinstance(obj.uuid, str) - suffix = Transformer.hash_contents(obj).encode().hex() + suffix = cls.hash_contents(obj).encode().hex() uuid = obj.uuid.replace("-", "") return uuid == suffix[: len(uuid)] return False - @staticmethod - def mark[R: _HasUUID | _HasPropertys](obj: R) -> R: + @classmethod + def _mark[R: _HasUUID | _HasPropertys](cls, obj: R) -> R: """Mark the property if possible, otherwise ensure the uuid is marked""" - # If there's already a valid mark, do nothing - # This is important to maintain consistent UUID marking - if Transformer.check_mark(obj): - return obj - - hashed_contents = Transformer.hash_contents(obj) + hashed_contents = cls.hash_contents(obj) if hasattr(obj, "propertys"): - obj.propertys["faebryk_mark"] = C_property( - name="faebryk_mark", + obj.propertys[cls.MARK_NAME] = C_property( + name=cls.MARK_NAME, value=hashed_contents, ) return obj @@ -356,6 +386,17 @@ def mark[R: _HasUUID | _HasPropertys](obj: R) -> R: return obj + @classmethod + def mark[R: _HasUUID | _HasPropertys](cls, obj: R) -> R: + """Mark the property if possible, otherwise ensure the uuid is marked""" + + # If there's already a valid mark, do nothing + # This is important to maintain consistent UUID marking + if cls.check_mark(obj): + return obj + + return cls._mark(obj) + # Insert --------------------------------------------------------------------------- def insert_wire( @@ -502,7 +543,7 @@ def insert_symbol( ) # It's one of ours, until it's modified in KiCAD - Transformer.mark(unit_instance) + self.mark(unit_instance) # Add a C_property for the reference based on the override name if reference_name := module.get_trait(F.has_overriden_name).get_name(): diff --git a/test/exporters/schematic/kicad/test_marking.py b/test/exporters/schematic/kicad/test_marking.py new file mode 100644 index 00000000..962e2635 --- /dev/null +++ b/test/exporters/schematic/kicad/test_marking.py @@ -0,0 +1,43 @@ +from pathlib import Path + +import pytest + +from faebryk.exporters.schematic.kicad.transformer import Transformer +from faebryk.libs.kicad import fileformats_sch + + +@pytest.fixture +def symbol(): + return fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance( + lib_id="lib:abc", + at=fileformats_sch.C_xyr(0, 0, 0), + unit=1, + ) + + +def test_marking_in_program(symbol): + old_contents = Transformer._get_hash_contents(symbol) + + assert not Transformer.check_mark(symbol) + Transformer.mark(symbol) + + new_contents = Transformer._get_hash_contents(symbol) + + assert new_contents == old_contents + assert Transformer.check_mark(symbol) + + +def test_marking_in_file(symbol, tmp_path): + path = Path(tmp_path) / "test.kicad_sch" + + assert not Transformer.check_mark(symbol) + + sch = fileformats_sch.C_kicad_sch_file.skeleton() + sch.kicad_sch.symbols.append(symbol) + # prove this mutates the file properly + Transformer.mark(symbol) + sch.dumps(path) + + assert Transformer.check_mark( + fileformats_sch.C_kicad_sch_file.loads(path).kicad_sch.symbols[0] + ) From daacc3aafcb5b74a7829e8de91a44ba9b41f9eaa Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 23 Oct 2024 15:43:53 -0700 Subject: [PATCH 58/85] Explain hierarchy --- .../exporters/schematic/kicad/transformer.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index b750df18..47beed50 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -6,6 +6,7 @@ import math import pprint from copy import deepcopy +from dataclasses import is_dataclass from functools import singledispatchmethod from itertools import chain from os import PathLike @@ -291,7 +292,6 @@ def get_unit_count(lib_sym: C_lib_symbol) -> int: MARK_NAME = "faebryk_mark" - @classmethod def _get_hash_contents(cls, obj_to_hash: Any) -> dict[tuple[str, ...], Any]: """ @@ -325,7 +325,6 @@ def _get_hash_contents(cls, obj_to_hash: Any) -> dict[tuple[str, ...], Any]: content_to_hash[name_path] = obj - return dict(sorted(content_to_hash.items(), key=lambda x: x[0])) @classmethod @@ -749,10 +748,11 @@ def _hierarchy(module: Module) -> str: Make a string representation of the module's hierarchy using the best name for each part we have. - NOTE: The hierarchy is used to determine whether parts live in the same module. - Top level should be unnamed, eg "" - Subsequent levels should be named and dot-separated - Part's ref should not be included at the end eg, don't do "led.U1" + NOTE: The hierarchy is used to determine whether parts + live in the same module. + - Top level should be unnamed, eg "" + - Subsequent levels should be named and dot-separated + - Part's ref should not be included at the end eg, don't do "led.U1" """ def _best_name(module: Module) -> str: @@ -771,9 +771,7 @@ def _best_name(module: Module) -> str: shim_part = shims.Part() shim_part.ref = sch_sym.propertys["Reference"].value # if we don't have a fab symbol, place the part at the top of the hierarchy - shim_part.hierarchy = ( - _hierarchy(f_symbol.represents) if f_symbol else "" - ) + shim_part.hierarchy = _hierarchy(f_symbol.represents) if f_symbol else "" # TODO: what's the ato analog? # TODO: should this desc shim_part.symtx = "" From facea6f4bd61acc989ca76b40baf4cac5bb3efec Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 23 Oct 2024 15:46:35 -0700 Subject: [PATCH 59/85] pre-commit tidy up --- src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py | 4 +++- src/faebryk/library/Pad.py | 3 +-- src/faebryk/libs/picker/lcsc.py | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index d17879f9..b2e761d4 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -390,7 +390,9 @@ def draw_pause(): @contextlib.contextmanager -def draw_context(bbox: BBox) -> Generator[tuple["pygame.Surface", Tx, "pygame.font.Font"], None, None]: +def draw_context( + bbox: BBox, +) -> Generator[tuple["pygame.Surface", Tx, "pygame.font.Font"], None, None]: """Context manager for drawing.""" import pygame diff --git a/src/faebryk/library/Pad.py b/src/faebryk/library/Pad.py index 9f160b18..453f3516 100644 --- a/src/faebryk/library/Pad.py +++ b/src/faebryk/library/Pad.py @@ -4,8 +4,7 @@ import faebryk.library._F as F from faebryk.core.moduleinterface import ModuleInterface -from faebryk.core.node import NodeException -from faebryk.core.reference import Reference, reference +from faebryk.core.reference import reference from faebryk.libs.util import not_none diff --git a/src/faebryk/libs/picker/lcsc.py b/src/faebryk/libs/picker/lcsc.py index 45d5a311..70c3b482 100644 --- a/src/faebryk/libs/picker/lcsc.py +++ b/src/faebryk/libs/picker/lcsc.py @@ -28,7 +28,6 @@ Supplier, ) from faebryk.libs.sexp import dataclass_sexp -from faebryk.libs.util import duplicates logger = logging.getLogger(__name__) From 7324d273754739acc5d9381f2c78d07af1ad5b8e Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 23 Oct 2024 16:26:45 -0700 Subject: [PATCH 60/85] Fix power and ground pin detection --- src/faebryk/exporters/schematic/kicad/transformer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 47beed50..bcd65bc7 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -837,7 +837,9 @@ def _best_name(module: Module) -> str: # - stub powery things # - override from symbol layout info trait shim_pin.stub = False - if fab_power_if := fab_pin.get_parent_of_type(F.ElectricPower): + if fab_power_if := fab_pin.represents.get_parent_of_type( + F.ElectricPower + ): shim_pin.fab_is_gnd = fab_pin.represents is fab_power_if.lv shim_pin.fab_is_pwr = fab_pin.represents is fab_power_if.hv else: From f4fdc56737198f30e8dcbbdaf5b180b08a88155b Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 14:35:08 -0700 Subject: [PATCH 61/85] Rotate the net-terminals correctly --- .../exporters/schematic/kicad/skidl/shims.py | 33 +++++++++++--- .../exporters/schematic/kicad/transformer.py | 44 ++++++++++++++----- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 6cf33408..402d2918 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -8,6 +8,7 @@ """ from itertools import chain +import math from typing import TYPE_CHECKING, Any, Iterator, TypedDict import faebryk.library._F as F @@ -42,14 +43,14 @@ def rmv_attr(objs, attrs): def angle_to_orientation(angle: float) -> str: """Convert an angle to an orientation""" - if angle == 0: - return "L" - elif angle == 90: - return "D" - elif angle == 180: + if math.isclose(angle, 0): return "R" - elif angle == 270: + elif math.isclose(angle, 90): return "U" + elif math.isclose(angle, 180): + return "L" + elif math.isclose(angle, 270): + return "D" else: raise ValueError(f"Invalid angle: {angle}") @@ -177,7 +178,25 @@ class Pin: net: "Net" name: str num: str - orientation: str # "U"/"D"/"L"/"R" for the pin's location + + # Pin rotation is confusing, because it has to do with the way to line coming + # from the pin is facing, not the side of the part/package it's on. + # Here's a diagram to help: + # + # D + # o + # | + # +-----+ + # | | + # R o--| |--o L + # | | + # +-----+ + # | + # o + # U + # + orientation: str # "U"/"D"/"L"/"R" for the pin's rotation + part: Part # to which part does this pin belong? stub: bool # whether to stub the pin or not # position of the pin diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index bcd65bc7..28cdb1b2 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -70,8 +70,6 @@ Point2D = Geometry.Point2D Justify = C_effects.C_justify.E_justify -Alignment = tuple[Justify, Justify, Justify] -Alignment_Default = (Justify.center_horizontal, Justify.center_vertical, Justify.normal) class _HasUUID(Protocol): @@ -115,10 +113,9 @@ def __init__( self.dimensions = None - FONT_SCALE = 8 + FONT_SCALE_MM = 1.27 FONT = Font( - size=C_wh(1 / FONT_SCALE, 1 / FONT_SCALE), - thickness=0.15 / FONT_SCALE, + size=C_wh(FONT_SCALE_MM, FONT_SCALE_MM), ) self.font = FONT @@ -423,23 +420,42 @@ def insert_junction( ) ) + @staticmethod + def _build_justify( + justify: Justify | list[Justify] | None, + ) -> list[C_effects.C_justify.E_justify]: + """ + Without any alignment, the text defaults to center alignment, + which beyond looking shit, also locks rotation to 0 or 90 degrees + + Weird! + """ + if justify is None: + justify = [C_effects.C_justify.E_justify.right] + elif isinstance(justify, Justify): + justify = [justify] + + return [C_effects.C_justify(justifys=just) for just in justify] + def insert_text( self, text: str, at: C_xyr, font: Font | None = None, - alignment: Alignment | None = None, + text_alignment: Justify | list[Justify] | None = None, ): if font is None: font = self.font + justifys = self._build_justify(text_alignment) + self.sch.texts.append( SCH.C_text( text=text, at=at, effects=C_effects( font=font, - justify=alignment, + justifys=justifys, ), uuid=_gen_uuid(), ) @@ -451,15 +467,19 @@ def insert_global_label( shape: SCH.C_global_label.E_shape, at: C_xyr, font: Font | None = None, - alignment: Alignment | None = None, + text_alignment: Justify | list[Justify] | None = None, ): if font is None: font = self.font - if alignment is None: - justifys = [] + # The rotation dictates the text alignment + # If the alignment is wrong, the rotation is sacrificed + if 0 < at.r <= 90: + text_alignment = [C_effects.C_justify.E_justify.left] else: - justifys = [C_effects.C_justify(justifys=[alignment])] + text_alignment = [C_effects.C_justify.E_justify.right] + + justifys = self._build_justify(text_alignment) self.sch.global_labels.append( SCH.C_global_label( @@ -976,7 +996,7 @@ def _check(tx: Tx) -> bool: self.insert_global_label( pin.net.name, at=C_xyr(x=pt.x, y=pt.y, r=rotation), - shape=SCH.C_global_label.E_shape.bidirectional, + shape=SCH.C_global_label.E_shape.input, ) else: ## 3. modify parts From d94da09209d8a36786fce3f4c9674455407fb779 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 15:08:05 -0700 Subject: [PATCH 62/85] Wire segments were out of order, which made them netty --- src/faebryk/exporters/schematic/kicad/transformer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 28cdb1b2..4d340015 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -1007,10 +1007,11 @@ def _check(tx: Tx) -> bool: part.sch_symbol.at.r = get_common_rotation(part_tx) or 0 # 4. draw wires and junctions - for _, wire in node.wires.items(): - skidl_points = [p * tx for seg in wire for p in [seg.p1, seg.p2]] - points = [(p.x, p.y) for p in skidl_points] - self.insert_wire(points) + for wire in node.wires.values(): + for seg in wire: + skild_seg_points = [p * tx for p in [seg.p1, seg.p2]] + seg_points = [(p.x, p.y) for p in skild_seg_points] + self.insert_wire(seg_points) for _, junctions in node.junctions.items(): for junc in junctions: From 6106109f57716887d22d75fdf2d5d3934559384b Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 15:13:22 -0700 Subject: [PATCH 63/85] Test marking on wires as well --- .../exporters/schematic/kicad/test_marking.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/test/exporters/schematic/kicad/test_marking.py b/test/exporters/schematic/kicad/test_marking.py index 962e2635..dcc784fe 100644 --- a/test/exporters/schematic/kicad/test_marking.py +++ b/test/exporters/schematic/kicad/test_marking.py @@ -15,16 +15,32 @@ def symbol(): ) -def test_marking_in_program(symbol): - old_contents = Transformer._get_hash_contents(symbol) +@pytest.fixture +def wire(): + return fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_wire( + pts=fileformats_sch.C_pts( + xys=[ + fileformats_sch.C_xy(0, 0), + fileformats_sch.C_xy(1, 1), + ] + ), + stroke=fileformats_sch.C_stroke(), + ) - assert not Transformer.check_mark(symbol) - Transformer.mark(symbol) - new_contents = Transformer._get_hash_contents(symbol) +@pytest.mark.parametrize("fixture_name", ["symbol", "wire"]) +def test_marking_in_program(fixture_name, request): + obj = request.getfixturevalue(fixture_name) + + old_contents = Transformer._get_hash_contents(obj) + + assert not Transformer.check_mark(obj) + Transformer.mark(obj) + + new_contents = Transformer._get_hash_contents(obj) assert new_contents == old_contents - assert Transformer.check_mark(symbol) + assert Transformer.check_mark(obj) def test_marking_in_file(symbol, tmp_path): From eacf59fc24abeaa57324657ce29a55deb735db84 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 15:23:56 -0700 Subject: [PATCH 64/85] Hide faebryk mark --- src/faebryk/exporters/schematic/kicad/transformer.py | 1 + src/faebryk/libs/kicad/fileformats_common.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 4d340015..165abf33 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -371,6 +371,7 @@ def _mark[R: _HasUUID | _HasPropertys](cls, obj: R) -> R: obj.propertys[cls.MARK_NAME] = C_property( name=cls.MARK_NAME, value=hashed_contents, + effects=C_effects(hide=True) ) return obj diff --git a/src/faebryk/libs/kicad/fileformats_common.py b/src/faebryk/libs/kicad/fileformats_common.py index e2ad87ab..da67d2da 100644 --- a/src/faebryk/libs/kicad/fileformats_common.py +++ b/src/faebryk/libs/kicad/fileformats_common.py @@ -107,7 +107,7 @@ class E_justify(SymEnum): **sexp_field(positional=True), default_factory=list ) - font: C_font + font: Optional[C_font] = None @staticmethod def preprocess_shitty_hide(c_effects: netlist_type): From 0bb302b94a4e7c2a0173ae7c2afade90bf940c32 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 15:24:33 -0700 Subject: [PATCH 65/85] Allow rotations of things without symtx --- .../exporters/schematic/kicad/skidl/gen_schematic.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index 90dd0f67..9a1f475e 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -559,7 +559,8 @@ def rotate_power_pins(part: Part): """ # Don't rotate parts that are already explicitly rotated/flipped. - if not getattr(part, "symtx", ""): + # FIXME: this was the inverted in SKiDL + if getattr(part, "symtx", ""): return def is_pwr(pin: Pin) -> bool: @@ -584,18 +585,18 @@ def is_gnd(pin: Pin) -> bool: if pin.orientation == "D": rotation_tally[180] += 1 if pin.orientation == "L": - rotation_tally[90] += 1 - if pin.orientation == "R": rotation_tally[270] += 1 + if pin.orientation == "R": + rotation_tally[90] += 1 elif is_pwr(pin): if pin.orientation == "D": rotation_tally[0] += 1 if pin.orientation == "U": rotation_tally[180] += 1 if pin.orientation == "L": - rotation_tally[270] += 1 - if pin.orientation == "R": rotation_tally[90] += 1 + if pin.orientation == "R": + rotation_tally[270] += 1 # Rotate the part unit in the direction with the most tallies. try: From 7ce863bf139fcc72fc178d2c6c1323fa27b037a3 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 18:02:56 -0700 Subject: [PATCH 66/85] Improve some hierarchy shenanigans --- .../schematic/kicad/skidl/net_terminal.py | 11 ++---- .../exporters/schematic/kicad/skidl/node.py | 35 ++++++++++++------- .../exporters/schematic/kicad/transformer.py | 6 ++-- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py index 150cac09..d8acf172 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/net_terminal.py @@ -2,9 +2,6 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. -import uuid -from functools import cached_property - from .constants import GRID from .geometry import Point, Tx, Vector from .shims import Net, Part, Pin @@ -17,7 +14,7 @@ class NetTerminal(Part): pull_pins: dict[Net, list[Pin]] - def __init__(self, net: Net): + def __init__(self, net: Net, hierarchy: str): """Specialized Part with a single pin attached to a net. This is intended for attaching to nets to label them, typically when @@ -28,9 +25,9 @@ def __init__(self, net: Net): # Set a default transformation matrix for this part. self.tx = Tx() self.ref = net.name + self.hierarchy = hierarchy self.symtx = "" self.unit = {} - self.hierarchy = uuid.uuid4().hex # Add a single pin to the part. pin = Pin() @@ -79,10 +76,6 @@ def __init__(self, net: Net): pin.audit() self.audit() - @cached_property - def hierarchy(self) -> str: - return uuid.uuid4().hex - @property def sch_symbol(self) -> None: return None diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index ac7fe93d..04151294 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -3,6 +3,7 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. +import itertools import re from collections import defaultdict from itertools import chain @@ -77,13 +78,16 @@ def add_circuit(self, circuit: Circuit): Args: circuit (Circuit): Circuit object. """ - from .net_terminal import NetTerminal - # Build the circuit node hierarchy by adding the parts. assert circuit.parts, "Circuit has no parts" for part in circuit.parts: self.add_part(part) + # FIXME: SKiDL doesn't seem to need to clean these up. We shouldn't really either + # The flattening and adding of terminals was the other way around in SKiDL. + # Flatten the hierarchy as specified by the flatness parameter. + self.flatten(self.flatness) + # Add terminals to nodes in the hierarchy for nets that span across nodes. for net in circuit.nets: # Skip nets that are stubbed since there will be no wire to attach to the NetTerminal. @@ -91,8 +95,18 @@ def add_circuit(self, circuit: Circuit): continue # Search for pins in different nodes. - for pin1, pin2 in zip(net.pins[:-1], net.pins[1:]): - if pin1.part.hierarchy != pin2.part.hierarchy: + # NOTE: this was a zip in SKiDL + for pin1, pin2 in itertools.combinations(net.pins, 2): + def _find_effective_node(part: Part) -> SchNode: + node = self.find_node_with_part(part) + while node.flattened: + if node.parent is None: + raise RuntimeError("Cannot flatten top node") + node = node.parent + return node + + # If two pins on a net are in different nodes, we should definitely add a terminal + if _find_effective_node(pin1.part) is not _find_effective_node(pin2.part): # Found pins in different nodes, so break and add terminals to nodes below. break else: @@ -116,22 +130,16 @@ def add_circuit(self, circuit: Circuit): part = pin.part - # Skip NetTerminals because otherwise doesn't this just recurse forever? - if isinstance(part, NetTerminal): - continue - if part.hierarchy in visited: # Already added a terminal to this node, so don't add another. continue # Add NetTerminal to the node with this part/pin. - self.find_node_with_part(part).add_terminal(net) + self.find_node_with_part(part).add_terminal(net, part.hierarchy) # Record that this hierarchical node was visited. visited.append(part.hierarchy) - # Flatten the hierarchy as specified by the flatness parameter. - self.flatten(self.flatness) def add_part(self, part: Part, level: int = 0): """Add a part to the node at the appropriate level of the hierarchy. @@ -167,6 +175,7 @@ def add_part(self, part: Part, level: int = 0): else: # Part is at a level below the current node. Get the child node using # the name of the next level in the hierarchy for this part. + # This is a default dict, so it also creates new nodes as needed. child_node = self.children[level_names[level + 1]] # Attach the child node to this node. (It may have just been created.) @@ -175,7 +184,7 @@ def add_part(self, part: Part, level: int = 0): # Add part to the child node (or one of its children). child_node.add_part(part, level + 1) - def add_terminal(self, net: Net): + def add_terminal(self, net: Net, hierarchy: str): """Add a terminal for this net to the node. Args: @@ -183,7 +192,7 @@ def add_terminal(self, net: Net): """ from .net_terminal import NetTerminal - nt = NetTerminal(net) + nt = NetTerminal(net, hierarchy) self.parts.append(nt) def external_bbox(self) -> BBox: diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 165abf33..ac988f34 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -741,7 +741,9 @@ def _build_shim_circuit(self) -> shims.Circuit: shim_net.name = net.get_trait(F.has_overriden_name).get_name() shim_net.netio = "" # TODO: shim_net.stub = False # TODO: - shim_net._is_implicit = net.get_trait(F.has_overriden_name).is_explicit() + shim_net._is_implicit = not net.get_trait( + F.has_overriden_name + ).is_explicit() # make partial net-oriented pins shim_net.pins = [] @@ -783,7 +785,7 @@ def _best_name(module: Module) -> str: # skip the root module, because it's name is just "*" hierarchy = [h[0] for h in module.get_hierarchy()][1:-1] - return ".".join(_best_name(n) for n in hierarchy) + return "." + ".".join(_best_name(n) for n in hierarchy) # for each sch_symbol, create a shim part for sch_sym, f_symbol in sch_to_fab_sym_map.items(): From 723836a1d42833cdf5624d3145698df818df7f7a Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 18:16:06 -0700 Subject: [PATCH 67/85] Unorthodox (eg. no SKiddilish) mechanism to flatten parts before adding net terminals. That was getting confusing otherwise --- .../exporters/schematic/kicad/skidl/node.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 04151294..292b23cc 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -46,6 +46,7 @@ def __init__( self.title = title self.flatness = flatness self.flattened = False + self.flattened_children = [] self.parts: list["Part"] = [] self.wires: dict[Net, list[Segment]] = defaultdict(list) self.junctions: dict[Net, list[Point]] = defaultdict(list) @@ -65,9 +66,15 @@ def find_node_with_part(self, part: Part): Node: The Node object containing the part. """ level_names = part.hierarchy.split(HIER_SEP) + + # descend based on the hierarchy name node = self for lvl_nm in level_names[1:]: + # stop descending if we're trying to go into something flattened + if lvl_nm in node.flattened_children: + break node = node.children[lvl_nm] + assert part in node.parts return node @@ -97,16 +104,8 @@ def add_circuit(self, circuit: Circuit): # Search for pins in different nodes. # NOTE: this was a zip in SKiDL for pin1, pin2 in itertools.combinations(net.pins, 2): - def _find_effective_node(part: Part) -> SchNode: - node = self.find_node_with_part(part) - while node.flattened: - if node.parent is None: - raise RuntimeError("Cannot flatten top node") - node = node.parent - return node - # If two pins on a net are in different nodes, we should definitely add a terminal - if _find_effective_node(pin1.part) is not _find_effective_node(pin2.part): + if self.find_node_with_part(pin1.part) is not self.find_node_with_part(pin2.part): # Found pins in different nodes, so break and add terminals to nodes below. break else: @@ -287,6 +286,14 @@ def flatten(self, flatness: float = 0.0) -> None: for child in child_types[child_type]: child.flattened = False + # Move all the flattened children's content into this node + for child in self.children.values(): + if child.flattened: + self.parts.extend(child.parts) + self.flattened_children.append(child.name) + for child in self.flattened_children: + del self.children[child] + def get_internal_nets(self) -> list[Net]: """Return a list of nets that have at least one pin on a part in this node.""" From 0bace49ff183af52d19a6133eefe5f4679c2443a Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 18:16:21 -0700 Subject: [PATCH 68/85] Add safety in case of missing tracks for routing --- .../exporters/schematic/kicad/skidl/route.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index b3e56977..30d420f2 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -3291,6 +3291,23 @@ def route(node: "SchNode", **options: Unpack[Options]): # Create horizontal & vertical global routing tracks and faces. h_tracks, v_tracks = node.create_routing_tracks(routing_bbox) + parts_missing_tracks: dict[Part, list[str]] = {} + for part in node.parts: + if not isinstance(part, Part): + continue + if not part.left_track: + parts_missing_tracks[part] = ["left"] + if not part.right_track: + parts_missing_tracks[part] = ["right"] + if not part.top_track: + parts_missing_tracks[part] = ["top"] + if not part.bottom_track: + parts_missing_tracks[part] = ["bottom"] + if parts_missing_tracks: + raise RuntimeError( + f"Parts missing tracks: {parts_missing_tracks}" + ) + # Create terminals on the faces in the routing tracks. node.create_terminals(internal_nets, h_tracks, v_tracks) From d929bbc7be4c55025303a890e5d066c3699df603 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 18:16:45 -0700 Subject: [PATCH 69/85] Remove content that was commented out on Skidl anyway --- src/faebryk/exporters/schematic/kicad/skidl/place.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 1d6f3af3..3648fcb7 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -1343,11 +1343,6 @@ def place_floating_parts( part_similarity[part][other_part] = ( part.similarity(other_part) / 100 ) - # part_similarity[part][other_part] = 0.1 - - # Select the top-most pin in each part as the anchor point for force-directed placement. - # tx = part.tx - # part.anchor_pin = max(part.anchor_pins, key=lambda pin: (pin.place_pt * tx).y) force_func = functools.partial( total_similarity_force, similarity=part_similarity From 79a40d433b7ff4de01be87ba030d70086c34d764 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 24 Oct 2024 18:38:35 -0700 Subject: [PATCH 70/85] Reenable flattened node offsets --- .../exporters/schematic/kicad/transformer.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index ac988f34..14485104 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -972,14 +972,13 @@ def _check(tx: Tx) -> bool: mils_to_mm = Tx(a=0.0254, b=0, c=0, d=0.0254, dx=0, dy=0) - # if node.flattened: - # # Create the transformation matrix for the placement of the parts - # # in the node. - # tx = node.tx * sheet_tx * mils_to_mm - # else: - # flattened_bbox = node.internal_bbox() - # tx = skidl_sch.calc_sheet_tx(flattened_bbox) * mils_to_mm - tx = node.tx * mils_to_mm + if node.flattened: + # Create the transformation matrix for the placement of the parts + # in the node. + tx = node.tx * sheet_tx * mils_to_mm + else: + flattened_bbox = node.internal_bbox() + tx = skidl_sch.calc_sheet_tx(flattened_bbox) * mils_to_mm # TODO: Put flattened node into heirarchical block From 78e9d59777e0a267e5d19f13359f62474ae1901f Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 25 Oct 2024 09:44:28 -0700 Subject: [PATCH 71/85] Modify flattening logic in export --- src/faebryk/exporters/schematic/kicad/transformer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 14485104..9ee92c15 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -973,12 +973,12 @@ def _check(tx: Tx) -> bool: mils_to_mm = Tx(a=0.0254, b=0, c=0, d=0.0254, dx=0, dy=0) if node.flattened: - # Create the transformation matrix for the placement of the parts - # in the node. - tx = node.tx * sheet_tx * mils_to_mm - else: - flattened_bbox = node.internal_bbox() - tx = skidl_sch.calc_sheet_tx(flattened_bbox) * mils_to_mm + # This almost certainly shouldn't be hit any more because we've already + # flattened the node. + raise RuntimeError("Cannot apply flattened node") + + flattened_bbox = node.internal_bbox() + tx = skidl_sch.calc_sheet_tx(flattened_bbox) * mils_to_mm # TODO: Put flattened node into heirarchical block From ed7f9823140f18640a8b6fe2b1ebd33d36d2f6c2 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 25 Oct 2024 09:44:50 -0700 Subject: [PATCH 72/85] Find simultaneous flipping and rotations --- .../exporters/schematic/kicad/transformer.py | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 9ee92c15..e98f44ff 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -945,13 +945,10 @@ def _apply_shim_sch_node( ): from faebryk.exporters.schematic.kicad.skidl.geometry import ( Tx, - tx_rot_0, - tx_rot_90, - tx_rot_180, tx_rot_270, ) - def get_common_rotation(part_tx: Tx) -> float | None: + def find_orientation(part_tx: Tx) -> tuple[float, bool]: def _check(tx: Tx) -> bool: return ( math.isclose(tx.a, part_tx.a / part_tx.scale) @@ -960,15 +957,20 @@ def _check(tx: Tx) -> bool: and math.isclose(tx.d, part_tx.d / part_tx.scale) ) - if _check(tx_rot_0): - return 0 - elif _check(tx_rot_90): - return 90 - elif _check(tx_rot_180): - return 180 - elif _check(tx_rot_270): - return 270 - return None + candidate = Tx() + flip_x = False + + for _ in range(2): + rotation = 0 + for _ in range(4): + if _check(candidate): + return rotation, flip_x + candidate *= tx_rot_270 # 90 degrees counterclockwise + rotation += 90 + flip_x = not flip_x + candidate = candidate.flip_x() + + raise ValueError(f"No orientation found for {part_tx}") mils_to_mm = Tx(a=0.0254, b=0, c=0, d=0.0254, dx=0, dy=0) @@ -1006,7 +1008,9 @@ def _check(tx: Tx) -> bool: # are already in the schematic, we just need to move them part.sch_symbol.at.x = part_tx.origin.x part.sch_symbol.at.y = part_tx.origin.y - part.sch_symbol.at.r = get_common_rotation(part_tx) or 0 + rotation, flip_x = find_orientation(part_tx) + part.sch_symbol.at.r = rotation + # part.sch_symbol. # 4. draw wires and junctions for wire in node.wires.values(): From 5bf245bf994d1df2c9588f684432caeef61c3e52 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 25 Oct 2024 09:45:09 -0700 Subject: [PATCH 73/85] Tidy --- src/faebryk/exporters/schematic/kicad/skidl/shims.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index 402d2918..dcccc71a 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -7,8 +7,8 @@ - We're not using dataclasses because typing doesn't pass through InitVar properly """ -from itertools import chain import math +from itertools import chain from typing import TYPE_CHECKING, Any, Iterator, TypedDict import faebryk.library._F as F From f3a344e1c8f035aebd879a5a050a55cafc398a98 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Mon, 28 Oct 2024 11:54:22 -0700 Subject: [PATCH 74/85] Ignore all skidl imported files because they use different linting rules --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e1f471fe..b1a384af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -200,11 +200,12 @@ exclude = [ "node_modules", "venv", ] -per-file-ignores = {} - # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +[tool.ruff.lint.per-file-ignores] +# SKiDL imported files are ignored because they use different linting rules to us +"src/faebryk/exporters/schematic/kicad/skidl/*" = ["ALL"] [tool.ruff.lint.mccabe] # Unlike Flake8, default to a complexity level of 10. From 6d2e9d84fbcc922960e91f171c91888a17d83719 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Mon, 28 Oct 2024 22:26:49 -0700 Subject: [PATCH 75/85] Accelerated minimal LED failing --- dev_schematics.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/dev_schematics.py b/dev_schematics.py index b38fa499..8eebeb03 100644 --- a/dev_schematics.py +++ b/dev_schematics.py @@ -5,6 +5,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.exporters.schematic.kicad.transformer import Transformer +from faebryk.libs.examples.buildutil import apply_design_to_pcb from faebryk.libs.kicad.fileformats_sch import C_kicad_sch_file root_dir = Path(__file__).parent @@ -21,12 +22,20 @@ def add_to_sys_path(path): sys.path.remove(str(path)) -# with add_to_sys_path(root_dir / "examples"): -# from minimal_led import App -# app = App() -# assert isinstance(app, Module) +with add_to_sys_path(root_dir / "examples"): + from minimal_led import App +app = App() +assert isinstance(app, Module) +assert isinstance(app.battery, F.Battery) +assert isinstance(app.led, F.PoweredLED) + + +# app.battery.add(F.has_descriptive_properties_defined({"LCSC": "C5239862"})) +app.led.led.add(F.has_descriptive_properties_defined({"LCSC": "C7429912"})) +app.led.current_limiting_resistor.add(F.has_descriptive_properties_defined({"LCSC": "C25077"})) + +apply_design_to_pcb(app) -app = Module() full_transformer = Transformer(sch_file.kicad_sch, app.get_graph(), app) full_transformer.index_symbol_files(fp_lib_path_path, load_globals=False) From 2ef168c825e6b8303af19d042be9cd8d4bc5f52a Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 31 Oct 2024 16:51:55 -0700 Subject: [PATCH 76/85] Skip picking --- dev_schematics.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/dev_schematics.py b/dev_schematics.py index 8eebeb03..afbecdf4 100644 --- a/dev_schematics.py +++ b/dev_schematics.py @@ -30,7 +30,6 @@ def add_to_sys_path(path): assert isinstance(app.led, F.PoweredLED) -# app.battery.add(F.has_descriptive_properties_defined({"LCSC": "C5239862"})) app.led.led.add(F.has_descriptive_properties_defined({"LCSC": "C7429912"})) app.led.current_limiting_resistor.add(F.has_descriptive_properties_defined({"LCSC": "C25077"})) @@ -40,25 +39,25 @@ def add_to_sys_path(path): full_transformer = Transformer(sch_file.kicad_sch, app.get_graph(), app) full_transformer.index_symbol_files(fp_lib_path_path, load_globals=False) -# mimicing typically design/user-space -audio_jack = full_transformer.app.add(Module()) -pin_s = audio_jack.add(F.Electrical()) -pin_t = audio_jack.add(F.Electrical()) -pin_r = audio_jack.add(F.Electrical()) -audio_jack.add(F.has_overriden_name_defined("U1")) - -# mimicing typically lcsc code -sym = F.Symbol.with_component( - audio_jack, - { - "S": pin_s, - "T": pin_t, - "R": pin_r, - }, -) -audio_jack.add(F.Symbol.has_symbol(sym)) -sym.add(F.Symbol.has_kicad_symbol("test:AudioJack-CUI-SJ-3523-SMT")) - -full_transformer.insert_symbol(audio_jack) +# # mimicing typically design/user-space +# audio_jack = full_transformer.app.add(Module()) +# pin_s = audio_jack.add(F.Electrical()) +# pin_t = audio_jack.add(F.Electrical()) +# pin_r = audio_jack.add(F.Electrical()) +# audio_jack.add(F.has_overriden_name_defined("U1")) + +# # mimicing typically lcsc code +# sym = F.Symbol.with_component( +# audio_jack, +# { +# "S": pin_s, +# "T": pin_t, +# "R": pin_r, +# }, +# ) +# audio_jack.add(F.Symbol.has_symbol(sym)) +# sym.add(F.Symbol.has_kicad_symbol("test:AudioJack-CUI-SJ-3523-SMT")) + +# full_transformer.insert_symbol(audio_jack) full_transformer.generate_schematic() From 9d55305ffa41bb943d78f01d7d07ceeef1a145b0 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 31 Oct 2024 16:52:22 -0700 Subject: [PATCH 77/85] Format --- src/faebryk/exporters/schematic/kicad/skidl/node.py | 5 +++-- src/faebryk/exporters/schematic/kicad/skidl/route.py | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/node.py b/src/faebryk/exporters/schematic/kicad/skidl/node.py index 292b23cc..113ac3a7 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/node.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/node.py @@ -105,7 +105,9 @@ def add_circuit(self, circuit: Circuit): # NOTE: this was a zip in SKiDL for pin1, pin2 in itertools.combinations(net.pins, 2): # If two pins on a net are in different nodes, we should definitely add a terminal - if self.find_node_with_part(pin1.part) is not self.find_node_with_part(pin2.part): + if self.find_node_with_part(pin1.part) is not self.find_node_with_part( + pin2.part + ): # Found pins in different nodes, so break and add terminals to nodes below. break else: @@ -139,7 +141,6 @@ def add_circuit(self, circuit: Circuit): # Record that this hierarchical node was visited. visited.append(part.hierarchy) - def add_part(self, part: Part, level: int = 0): """Add a part to the node at the appropriate level of the hierarchy. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/route.py b/src/faebryk/exporters/schematic/kicad/skidl/route.py index 30d420f2..d1e3eb6f 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/route.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/route.py @@ -3304,9 +3304,7 @@ def route(node: "SchNode", **options: Unpack[Options]): if not part.bottom_track: parts_missing_tracks[part] = ["bottom"] if parts_missing_tracks: - raise RuntimeError( - f"Parts missing tracks: {parts_missing_tracks}" - ) + raise RuntimeError(f"Parts missing tracks: {parts_missing_tracks}") # Create terminals on the faces in the routing tracks. node.create_terminals(internal_nets, h_tracks, v_tracks) From 37641af336dc2048da4ca5ec0d5e77bf8d035b8e Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 31 Oct 2024 16:52:40 -0700 Subject: [PATCH 78/85] Protect indexing files --- .../exporters/schematic/kicad/transformer.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index e98f44ff..9a2a17d8 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -107,7 +107,7 @@ def __init__( self.sch = sch self.graph = graph self.app = app - self._symbol_files_index: dict[str, Path] = {} + self._symbol_files_index: dict[str, Path] | None = None self.missing_symbols: list[F.Symbol] = [] @@ -185,6 +185,12 @@ def attach_symbol(self, f_symbol: F.Symbol, sym_inst: SCH.C_symbol_instance): def index_symbol_files( self, fp_lib_tables: PathLike | list[PathLike], load_globals: bool = True ) -> None: + """ + Index the symbol files in the given library tables + """ + if self._symbol_files_index is None: + self._symbol_files_index = {} + if isinstance(fp_lib_tables, (str, Path)): fp_lib_table_paths = [Path(fp_lib_tables)] else: @@ -229,8 +235,11 @@ def get_all_symbols(self) -> List[tuple[Module, F.Symbol]]: @once def get_symbol_file(self, lib_name: str) -> C_kicad_sym_file: # primary caching handled by @once + if self._symbol_files_index is None: + raise ValueError("Symbol files index not indexed") + if lib_name not in self._symbol_files_index: - raise FaebrykException(f"Symbol file {lib_name} not found") + raise FaebrykException(f"Symbol file \"{lib_name}\" not found") path = self._symbol_files_index[lib_name] return C_kicad_sym_file.loads(path) @@ -369,9 +378,7 @@ def _mark[R: _HasUUID | _HasPropertys](cls, obj: R) -> R: if hasattr(obj, "propertys"): obj.propertys[cls.MARK_NAME] = C_property( - name=cls.MARK_NAME, - value=hashed_contents, - effects=C_effects(hide=True) + name=cls.MARK_NAME, value=hashed_contents, effects=C_effects(hide=True) ) return obj From 4dfa6b4962ecd3bc05df5ea450b4bada75706cf3 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 31 Oct 2024 17:31:52 -0700 Subject: [PATCH 79/85] Stabalise dev_schematics --- dev_schematics.py | 95 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/dev_schematics.py b/dev_schematics.py index afbecdf4..e2e61a19 100644 --- a/dev_schematics.py +++ b/dev_schematics.py @@ -1,18 +1,54 @@ +""" +Issues: + +- [ ] Parts aren't being rotated properly + - It appears symtx should be minimally populated + +- [ ] Multiple net terminals of the same net + +- [ ] Component references aren't aligned to anything in particular + +- [ ] Battery's 180 degrees off + +- [ ] Unit should be 1, not 0 + +- [ ] Add top-level global labels + +- [x] Placed off-sheet + +- [x] Nets are netty + +- [x] Global labels aren't being rotated appropriately + They're actually only able to be aligned down or left. + This seems like we should be adding net tags instead of global labels. + +- [x] The net terminals appear to be on the wrong side of the parts + (which is causing the wire cross-over) + +- [x] Marking isn't reloading properly + +- [x] Power pins aren't being properly detected + +""" + import contextlib import sys from pathlib import Path import faebryk.library._F as F from faebryk.core.module import Module +from faebryk.exporters.schematic.kicad.skidl.shims import Options from faebryk.exporters.schematic.kicad.transformer import Transformer from faebryk.libs.examples.buildutil import apply_design_to_pcb from faebryk.libs.kicad.fileformats_sch import C_kicad_sch_file root_dir = Path(__file__).parent test_dir = root_dir / "test" +build_dir = root_dir / "build" -fp_lib_path_path = test_dir / "common/resources/fp-lib-table" -sch_file = C_kicad_sch_file.loads(test_dir / "common/resources/test.kicad_sch") +fp_lib_path_path = build_dir / "kicad/source/fp-lib-table" +sch_file = C_kicad_sch_file.skeleton() +# sch_file = C_kicad_sch_file.loads(test_dir / "common/resources/test.kicad_sch") @contextlib.contextmanager @@ -31,7 +67,9 @@ def add_to_sys_path(path): app.led.led.add(F.has_descriptive_properties_defined({"LCSC": "C7429912"})) -app.led.current_limiting_resistor.add(F.has_descriptive_properties_defined({"LCSC": "C25077"})) +app.led.current_limiting_resistor.add( + F.has_descriptive_properties_defined({"LCSC": "C25077"}) +) apply_design_to_pcb(app) @@ -39,25 +77,32 @@ def add_to_sys_path(path): full_transformer = Transformer(sch_file.kicad_sch, app.get_graph(), app) full_transformer.index_symbol_files(fp_lib_path_path, load_globals=False) -# # mimicing typically design/user-space -# audio_jack = full_transformer.app.add(Module()) -# pin_s = audio_jack.add(F.Electrical()) -# pin_t = audio_jack.add(F.Electrical()) -# pin_r = audio_jack.add(F.Electrical()) -# audio_jack.add(F.has_overriden_name_defined("U1")) - -# # mimicing typically lcsc code -# sym = F.Symbol.with_component( -# audio_jack, -# { -# "S": pin_s, -# "T": pin_t, -# "R": pin_r, -# }, -# ) -# audio_jack.add(F.Symbol.has_symbol(sym)) -# sym.add(F.Symbol.has_kicad_symbol("test:AudioJack-CUI-SJ-3523-SMT")) - -# full_transformer.insert_symbol(audio_jack) - -full_transformer.generate_schematic() +options = Options( + # draw_global_routing=True, + draw_placement=True, + draw_routing=True, + draw_routing_channels=True, + draw_switchbox_boundary=True, + draw_switchbox_routing=True, + retries=3, + pin_normalize=True, + net_normalize=True, + compress_before_place=True, + use_optimizer=True, + use_push_pull=True, + allow_jumps=True, + align_parts=True, + remove_overlaps=True, + slip_and_slide=True, + # rotate_parts=True, # Doesn't work. It's forced on in a lower-level + trim_anchor_pull_pins=True, + # fanout_attenuation=True, + # remove_power=True, + # remove_high_fanout=True, + normalize=True, + flatness=1.0, +) + +sch = full_transformer.generate_schematic(**options) + +sch_file.dumps(build_dir / "kicad/source/test.kicad_sch") From 6351c8358240b94f669fd36b03a66526c71fdea3 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 1 Nov 2024 11:04:16 -0700 Subject: [PATCH 80/85] Battery is the right way up --- dev_schematics.py | 17 ++-- .../exporters/schematic/kicad/skidl/bboxes.py | 14 ++-- .../schematic/kicad/skidl/gen_schematic.py | 81 +++++++++--------- .../schematic/kicad/skidl/geometry.py | 82 +++++++++++++++++-- .../exporters/schematic/kicad/transformer.py | 43 ++-------- src/faebryk/libs/kicad/fileformats_sch.py | 4 + .../kicad/test_skidl_gen_schematic.py | 53 ++++++++++++ .../schematic/kicad/test_skidl_geometry.py | 30 +++++++ 8 files changed, 221 insertions(+), 103 deletions(-) create mode 100644 test/exporters/schematic/kicad/test_skidl_gen_schematic.py create mode 100644 test/exporters/schematic/kicad/test_skidl_geometry.py diff --git a/dev_schematics.py b/dev_schematics.py index e2e61a19..f64ce7ac 100644 --- a/dev_schematics.py +++ b/dev_schematics.py @@ -2,18 +2,17 @@ Issues: - [ ] Parts aren't being rotated properly - - It appears symtx should be minimally populated - [ ] Multiple net terminals of the same net - [ ] Component references aren't aligned to anything in particular -- [ ] Battery's 180 degrees off - -- [ ] Unit should be 1, not 0 +- [x] Battery's 180 degrees off - [ ] Add top-level global labels +- [x] Unit should be 1, not 0 + - [x] Placed off-sheet - [x] Nets are netty @@ -79,11 +78,11 @@ def add_to_sys_path(path): options = Options( # draw_global_routing=True, - draw_placement=True, - draw_routing=True, - draw_routing_channels=True, - draw_switchbox_boundary=True, - draw_switchbox_routing=True, + # draw_placement=True, + # draw_routing=True, + # draw_routing_channels=True, + # draw_switchbox_boundary=True, + # draw_switchbox_routing=True, retries=3, pin_normalize=True, net_normalize=True, diff --git a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py index b0d27c8d..f22aec4d 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/bboxes.py @@ -26,10 +26,6 @@ BBox, Point, Tx, - tx_rot_0, - tx_rot_90, - tx_rot_180, - tx_rot_270, ) from .shims import Options, Part @@ -155,7 +151,7 @@ def make_pin_dir_tbl(abs_xoff: int = 20) -> dict[str, "PinDir"]: bbox = BBox(Point(0, 0), Point(length, height)) # Rotate bbox around origin. - rot_tx = {"H": Tx(), "V": tx_rot_90}[obj.orientation.upper()] + rot_tx = {"H": Tx(), "V": Tx.ROT_CW_90}[obj.orientation.upper()] bbox *= rot_tx # Horizontally align bbox. @@ -325,10 +321,10 @@ def calc_hier_label_bbox(label: str, dir: str) -> BBox: # Rotation matrices for each direction. lbl_tx = { - "U": tx_rot_90, # Pin on bottom pointing upwards. - "D": tx_rot_270, # Pin on top pointing down. - "L": tx_rot_180, # Pin on right pointing left. - "R": tx_rot_0, # Pin on left pointing right. + "U": Tx.ROT_CW_90, # Pin on bottom pointing upwards. + "D": Tx.ROT_CW_270, # Pin on top pointing down. + "L": Tx.ROT_CW_180, # Pin on right pointing left. + "R": Tx.ROT_CW_0, # Pin on left pointing right. } # Calculate length and height of label + hierarchical marker. diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index 9a1f475e..8f15dfe0 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -305,9 +305,9 @@ def get_A_size(bbox: BBox) -> str: def calc_sheet_tx(bbox): """Compute the page size and positioning for this sheet.""" A_size = get_A_size(bbox) - page_bbox = bbox * Tx(d=-1) + page_bbox = bbox * Tx() move_to_ctr = A_sizes[A_size].ctr.snap(GRID) - page_bbox.ctr.snap(GRID) - move_tx = Tx(d=-1).move(move_to_ctr) + move_tx = Tx().move(move_to_ctr) return move_tx @@ -515,6 +515,43 @@ def node_to_eeschema(node: SchNode, sheet_tx: Tx = Tx()) -> str: # TODO: Handle symio attribute. +def _ideal_part_rotation(part: Part) -> float: + # Tally what rotation would make each pwr/gnd pin point up or down. + def is_pwr(pin: Pin) -> bool: + return pin.fab_is_pwr + + def is_gnd(pin: Pin) -> bool: + return pin.fab_is_gnd + + rotation_tally = Counter() + for pin in part: + if is_pwr(pin): + if pin.orientation == "D": + rotation_tally[0] += 1 + if pin.orientation == "U": + rotation_tally[180] += 1 + if pin.orientation == "L": + rotation_tally[90] += 1 + if pin.orientation == "R": + rotation_tally[270] += 1 + elif is_gnd(pin): + if pin.orientation == "U": + rotation_tally[0] += 1 + if pin.orientation == "D": + rotation_tally[180] += 1 + if pin.orientation == "L": + rotation_tally[270] += 1 + if pin.orientation == "R": + rotation_tally[90] += 1 + + # Rotate the part unit in the direction with the most tallies. + try: + rotation = rotation_tally.most_common()[0][0] + except IndexError: + return 0 + + return rotation + def preprocess_circuit(circuit: Circuit, **options: Unpack[Options]): """Add stuff to parts & nets for doing placement and routing of schematics.""" @@ -563,12 +600,6 @@ def rotate_power_pins(part: Part): if getattr(part, "symtx", ""): return - def is_pwr(pin: Pin) -> bool: - return pin.fab_is_pwr - - def is_gnd(pin: Pin) -> bool: - return pin.fab_is_gnd - dont_rotate_pin_cnt = options.get("dont_rotate_pin_count", 10000) for part_unit in units(part): @@ -576,38 +607,8 @@ def is_gnd(pin: Pin) -> bool: if len(part_unit) > dont_rotate_pin_cnt: return - # Tally what rotation would make each pwr/gnd pin point up or down. - rotation_tally = Counter() - for pin in part_unit: - if is_gnd(pin): - if pin.orientation == "U": - rotation_tally[0] += 1 - if pin.orientation == "D": - rotation_tally[180] += 1 - if pin.orientation == "L": - rotation_tally[270] += 1 - if pin.orientation == "R": - rotation_tally[90] += 1 - elif is_pwr(pin): - if pin.orientation == "D": - rotation_tally[0] += 1 - if pin.orientation == "U": - rotation_tally[180] += 1 - if pin.orientation == "L": - rotation_tally[90] += 1 - if pin.orientation == "R": - rotation_tally[270] += 1 - - # Rotate the part unit in the direction with the most tallies. - try: - rotation = rotation_tally.most_common()[0][0] - except IndexError: - pass - else: - # Rotate part unit 90-degrees clockwise until the desired rotation is reached. - tx_cw_90 = Tx(a=0, b=-1, c=1, d=0) # 90-degree trans. matrix. - for _ in range(int(round(rotation / 90))): - part_unit.tx = part_unit.tx * tx_cw_90 + rotation = _ideal_part_rotation(part_unit) + part_unit.tx = part_unit.tx.rot_ccw(rotation) def calc_part_bbox(part: Part): """Calculate the labeled bounding boxes and store it in the part.""" diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index 9175c3e9..435983d1 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -4,7 +4,7 @@ # The MIT License (MIT) - Copyright (c) Dave Vandenbout. from copy import copy -from math import cos, radians, sin, sqrt +from math import cos, radians, sin, sqrt, isclose """ Stuff for handling geometry: @@ -30,6 +30,17 @@ def to_mms(mils): class Tx: + """Transformation matrix.""" + ROT_CCW_90: "Tx" + + ROT_CW_0: "Tx" + ROT_CW_90: "Tx" + ROT_CW_180: "Tx" + ROT_CW_270: "Tx" + + FLIP_X: "Tx" + FLIP_Y: "Tx" + def __init__( self, a: float = 1, @@ -103,6 +114,16 @@ def __mul__(self, m: "Tx | float") -> "Tx": dy=self.dx * tx.b + self.dy * tx.d + tx.dy, ) + def __eq__(self, other: "Tx") -> bool: + return (self.a, self.b, self.c, self.d, self.dx, self.dy) == ( + other.a, + other.b, + other.c, + other.d, + other.dx, + other.dy, + ) + @property def origin(self) -> "Point": """Return the (dx, dy) translation as a Point.""" @@ -119,13 +140,26 @@ def move(self, vec: "Point") -> "Tx": def rot_90cw(self) -> "Tx": """Return Tx with 90-deg clock-wise rotation around (0, 0).""" - return self * Tx(a=0, b=1, c=-1, d=0) + return self * self.ROT_CW_90 - def rot(self, degs: float) -> "Tx": + def rot_cw(self, degs: float) -> "Tx": """Return Tx rotated by the given angle (in degrees).""" + if degs % 360 == 0: + return self + elif degs % 360 == 90: + return self * self.ROT_CW_90 + elif degs % 360 == 180: + return self * self.ROT_CW_180 + elif degs % 360 == 270: + return self * self.ROT_CW_270 + rads = radians(degs) return self * Tx(a=cos(rads), b=sin(rads), c=-sin(rads), d=cos(rads)) + def rot_ccw(self, degs: float) -> "Tx": + """Return Tx rotated by the given angle (in degrees).""" + return self.rot_cw(-degs) + def flip_x(self) -> "Tx": """Return Tx with X coords flipped around (0, 0).""" return self * Tx(a=-1) @@ -138,16 +172,46 @@ def no_translate(self) -> "Tx": """Return Tx with translation set to (0,0).""" return Tx(a=self.a, b=self.b, c=self.c, d=self.d) + def find_orientation(self) -> tuple[bool, float]: + """ + Return the orientation of the transformation + matrix as a flip in x and rotation ccw in degrees. + """ + + def _check(tx: Tx) -> bool: + return ( + isclose(tx.a, self.a / self.scale) + and isclose(tx.b, self.b / self.scale) + and isclose(tx.c, self.c / self.scale) + and isclose(tx.d, self.d / self.scale) + ) + + candidate = Tx() + flip_x = False + + for _ in range(2): + rotation = 0 + for _ in range(4): + if _check(candidate): + return flip_x, rotation + candidate *= Tx.ROT_CCW_90 + rotation += 90 + flip_x = not flip_x + candidate = candidate.flip_x() + + raise ValueError(f"No orientation found for {self}") + # Some common rotations. -tx_rot_0 = Tx(a=1, b=0, c=0, d=1) -tx_rot_90 = Tx(a=0, b=1, c=-1, d=0) -tx_rot_180 = Tx(a=-1, b=0, c=0, d=-1) -tx_rot_270 = Tx(a=0, b=-1, c=1, d=0) +Tx.ROT_CCW_90 = Tx(a=0, b=-1, c=1, d=0) +Tx.ROT_CW_0 = Tx(a=1, b=0, c=0, d=1) +Tx.ROT_CW_90 = Tx(a=0, b=1, c=-1, d=0) +Tx.ROT_CW_180 = Tx(a=-1, b=0, c=0, d=-1) +Tx.ROT_CW_270 = Tx(a=0, b=-1, c=1, d=0) # Some common flips. -tx_flip_x = Tx(a=-1, b=0, c=0, d=1) -tx_flip_y = Tx(a=1, b=0, c=0, d=-1) +Tx.FLIP_X = Tx(a=-1, b=0, c=0, d=1) +Tx.FLIP_Y = Tx(a=1, b=0, c=0, d=-1) class Point: diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 9a2a17d8..825ea250 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -546,7 +546,7 @@ def insert_symbol( # - has_kicad_symbol mapping is currently 1:1 raise NotImplementedError("Multiple units not implemented") - for unit_key in range(self.get_unit_count(lib_sym)): + for unit_key in range(1, self.get_unit_count(lib_sym) + 1): unit_objs = self.get_sub_syms(lib_sym, unit_key) pins = [] @@ -950,36 +950,7 @@ def score_pins(): def _apply_shim_sch_node( self, node: skidl_node.SchNode, sheet_tx: skidl_node.Tx = skidl_node.Tx() ): - from faebryk.exporters.schematic.kicad.skidl.geometry import ( - Tx, - tx_rot_270, - ) - - def find_orientation(part_tx: Tx) -> tuple[float, bool]: - def _check(tx: Tx) -> bool: - return ( - math.isclose(tx.a, part_tx.a / part_tx.scale) - and math.isclose(tx.b, part_tx.b / part_tx.scale) - and math.isclose(tx.c, part_tx.c / part_tx.scale) - and math.isclose(tx.d, part_tx.d / part_tx.scale) - ) - - candidate = Tx() - flip_x = False - - for _ in range(2): - rotation = 0 - for _ in range(4): - if _check(candidate): - return rotation, flip_x - candidate *= tx_rot_270 # 90 degrees counterclockwise - rotation += 90 - flip_x = not flip_x - candidate = candidate.flip_x() - - raise ValueError(f"No orientation found for {part_tx}") - - mils_to_mm = Tx(a=0.0254, b=0, c=0, d=0.0254, dx=0, dy=0) + from faebryk.exporters.schematic.kicad.skidl.geometry import mms_per_mil if node.flattened: # This almost certainly shouldn't be hit any more because we've already @@ -987,9 +958,9 @@ def _check(tx: Tx) -> bool: raise RuntimeError("Cannot apply flattened node") flattened_bbox = node.internal_bbox() - tx = skidl_sch.calc_sheet_tx(flattened_bbox) * mils_to_mm + tx = skidl_sch.calc_sheet_tx(flattened_bbox) * mms_per_mil - # TODO: Put flattened node into heirarchical block + # TODO: Put flattened node into hierarchical block for part in node.parts: ## 2 add global labels @@ -1015,9 +986,9 @@ def _check(tx: Tx) -> bool: # are already in the schematic, we just need to move them part.sch_symbol.at.x = part_tx.origin.x part.sch_symbol.at.y = part_tx.origin.y - rotation, flip_x = find_orientation(part_tx) - part.sch_symbol.at.r = rotation - # part.sch_symbol. + flip_x, rot_ccw = part_tx.find_orientation() + part.sch_symbol.at.r = rot_ccw + part.sch_symbol.mirror = part.sch_symbol.E_mirror.x if flip_x else None # 4. draw wires and junctions for wire in node.wires.values(): diff --git a/src/faebryk/libs/kicad/fileformats_sch.py b/src/faebryk/libs/kicad/fileformats_sch.py index 4e7e0a26..0fa8a95a 100644 --- a/src/faebryk/libs/kicad/fileformats_sch.py +++ b/src/faebryk/libs/kicad/fileformats_sch.py @@ -222,6 +222,9 @@ class C_pin: name: str = field(**sexp_field(positional=True)) uuid: UUID = uuid_field() + class E_mirror(SymEnum): + x = "x" + lib_id: str at: C_xyr unit: int @@ -229,6 +232,7 @@ class C_pin: on_board: bool = False uuid: UUID = uuid_field() fields_autoplaced: bool = True + mirror: Optional[E_mirror] = None # Can be "x" for x-axis mirroring propertys: dict[str, C_property] = field( **sexp_field(multidict=True, key=lambda x: x.name), default_factory=dict, diff --git a/test/exporters/schematic/kicad/test_skidl_gen_schematic.py b/test/exporters/schematic/kicad/test_skidl_gen_schematic.py new file mode 100644 index 00000000..a42a40bd --- /dev/null +++ b/test/exporters/schematic/kicad/test_skidl_gen_schematic.py @@ -0,0 +1,53 @@ +import pytest + +from faebryk.exporters.schematic.kicad.skidl.shims import Part, Pin + + +@pytest.fixture +def part() -> Part: + + part = Part() + + part.pins = [Pin() for _ in range(4)] + part.pins[0].orientation = "U" + part.pins[1].orientation = "D" + part.pins[2].orientation = "L" + part.pins[3].orientation = "R" + + part.pins_by_orientation = { + "U": part.pins[0], + "D": part.pins[1], + "L": part.pins[2], + "R": part.pins[3], + } + + return part + + +@pytest.mark.parametrize( + "pwr_pins, gnd_pins, expected", + [ + (["U"], ["D"], 180), + (["L"], ["R"], 90), + (["R"], ["L"], 270), + ], +) +def test_ideal_part_rotation(part, pwr_pins, gnd_pins, expected): + from faebryk.exporters.schematic.kicad.skidl.gen_schematic import ( + _ideal_part_rotation, + ) + + assert isinstance(part, Part) + for orientation, pin in part.pins_by_orientation.items(): + assert isinstance(pin, Pin) + if orientation in pwr_pins: + pin.fab_is_pwr = True + pin.fab_is_gnd = False + elif orientation in gnd_pins: + pin.fab_is_gnd = True + pin.fab_is_pwr = False + else: + pin.fab_is_pwr = False + pin.fab_is_gnd = False + + assert _ideal_part_rotation(part) == expected diff --git a/test/exporters/schematic/kicad/test_skidl_geometry.py b/test/exporters/schematic/kicad/test_skidl_geometry.py new file mode 100644 index 00000000..ee1c51e6 --- /dev/null +++ b/test/exporters/schematic/kicad/test_skidl_geometry.py @@ -0,0 +1,30 @@ +import pytest + +from faebryk.exporters.schematic.kicad.skidl.geometry import Tx + + +@pytest.mark.parametrize( + "tx, expected", + [ + (Tx.ROT_CCW_90, (90, False)), + (Tx.FLIP_X * Tx.ROT_CCW_90, (90, True)), + (Tx.ROT_CW_90, (270, False)), + (Tx.FLIP_X * Tx.ROT_CW_90, (270, True)), + ], +) +def test_find_orientation(tx: Tx, expected: tuple[float, bool]): + assert tx.find_orientation() == expected + + +@pytest.mark.parametrize( + "degs, expected", + [ + (0, Tx()), + (90, Tx.ROT_CW_90), + (180, Tx.ROT_CW_180), + (270, Tx.ROT_CW_270), + ], +) +def test_rot_cw(degs: float, expected: Tx): + tx = Tx() + assert tx.rot_cw(degs) == expected From 51f5b2be608cd74299f0602b7f167a8bc7a87456 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 1 Nov 2024 15:44:30 -0700 Subject: [PATCH 81/85] Add schematic hints --- .../exporters/schematic/kicad/skidl/shims.py | 1 + .../exporters/schematic/kicad/transformer.py | 4 ++++ src/faebryk/library/_F.py | 2 +- src/faebryk/library/has_schematic_hints.py | 17 +++++++++++++++++ src/faebryk/library/has_symbol_layout.py | 8 -------- 5 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 src/faebryk/library/has_schematic_hints.py delete mode 100644 src/faebryk/library/has_symbol_layout.py diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index dcccc71a..a82bc887 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -71,6 +71,7 @@ class Part: symtx: str unit: dict[str, "PartUnit"] # units within the part, empty is this is all it is bare_bbox: BBox + hints: F.has_schematic_hints # things we've added to make life easier sch_symbol: "fileformats_sch.C_kicad_sch_file.C_kicad_sch.C_symbol_instance" diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 825ea250..b0ede07e 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -814,6 +814,10 @@ def _best_name(module: Module) -> str: for pts in Transformer.get_bbox(sch_lib_symbol_units) ] ) + shim_part.hints = ( + f_symbol.represents.try_get_trait(F.has_schematic_hints) + or F.has_schematic_hints() + ) shim_part.pins = [] # 2.3 finish making pins, this time from a part-orientation diff --git a/src/faebryk/library/_F.py b/src/faebryk/library/_F.py index 8f056067..25bcffbe 100644 --- a/src/faebryk/library/_F.py +++ b/src/faebryk/library/_F.py @@ -44,6 +44,7 @@ from faebryk.library.has_pcb_layout import has_pcb_layout from faebryk.library.has_pcb_routing_strategy import has_pcb_routing_strategy from faebryk.library.has_resistance import has_resistance +from faebryk.library.has_schematic_hints import has_schematic_hints from faebryk.library.has_single_connection import has_single_connection from faebryk.library.is_representable_by_single_value import is_representable_by_single_value from faebryk.library.ANY import ANY @@ -85,7 +86,6 @@ from faebryk.library.Pad import Pad from faebryk.library.Button import Button from faebryk.library.GDT import GDT -from faebryk.library.has_symbol_layout import has_symbol_layout from faebryk.library.has_pin_association_heuristic_lookup_table import has_pin_association_heuristic_lookup_table from faebryk.library.LogicGate import LogicGate from faebryk.library.has_footprint_defined import has_footprint_defined diff --git a/src/faebryk/library/has_schematic_hints.py b/src/faebryk/library/has_schematic_hints.py new file mode 100644 index 00000000..112d3511 --- /dev/null +++ b/src/faebryk/library/has_schematic_hints.py @@ -0,0 +1,17 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +from faebryk.libs.library import L + + +class has_schematic_hints(L.Module.TraitT.decless()): + """ + Hints for the schematic exporter. + + Attributes: + lock_rotation_certainty: Don't rotate symbols to that have an obviously + correct orientation (eg a power source with the positive terminal up). + """ + + def __init__(self, lock_rotation_certainty: float = 0.6): + self.lock_rotation_certainty = lock_rotation_certainty diff --git a/src/faebryk/library/has_symbol_layout.py b/src/faebryk/library/has_symbol_layout.py deleted file mode 100644 index 26ac4d67..00000000 --- a/src/faebryk/library/has_symbol_layout.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -import faebryk.library._F as F - - -class has_symbol_layout(F.Symbol.TraitT.decless()): - translations: str From 12f9cca7acb1a6a38eaffcd0e56957bd468f3775 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 1 Nov 2024 15:45:42 -0700 Subject: [PATCH 82/85] Fix motherfucking backwards-ass rotation matrices --- .../schematic/kicad/skidl/debug_draw.py | 30 ++++++++--- .../schematic/kicad/skidl/gen_schematic.py | 51 +++++++++---------- .../schematic/kicad/skidl/geometry.py | 28 +++++++--- .../exporters/schematic/kicad/skidl/place.py | 16 ++++-- .../exporters/schematic/kicad/skidl/shims.py | 11 +++- .../kicad/test_skidl_gen_schematic.py | 13 ++--- .../schematic/kicad/test_skidl_geometry.py | 21 +++++++- 7 files changed, 116 insertions(+), 54 deletions(-) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py index b2e761d4..e539dcef 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/debug_draw.py @@ -152,7 +152,7 @@ def draw_text( font.render_to(scr, (pt.x, pt.y), txt, color) -def draw_part(part: "Part", scr: "pygame.Surface", tx: Tx, font: "pygame.font.Font"): +def draw_part(part: "Part", scr: "pygame.Surface", tx: Tx, font: "pygame.font.Font", **options): """Draw part bounding box. Args: @@ -167,14 +167,27 @@ def draw_part(part: "Part", scr: "pygame.Surface", tx: Tx, font: "pygame.font.Fo draw_box(tx_bbox, scr, tx, color=(180, 255, 180), thickness=0) draw_box(tx_bbox, scr, tx, color=(90, 128, 90), thickness=5) draw_text(part.ref, tx_bbox.ctr, scr, tx, font) + try: - for pin in part: - if hasattr(pin, "place_pt"): - pt = pin.place_pt * part.tx - draw_endpoint(pt, scr, tx, color=(200, 0, 200), dot_radius=10) + pins = list(part) except TypeError: # Probably trying to draw a block of parts which has no pins and can't iterate thru them. - pass + pins = [] + + for pin in pins: + if hasattr(pin, "place_pt"): + pt = pin.place_pt * part.tx + draw_endpoint(pt, scr, tx, color=(200, 0, 200), dot_radius=10) + + if options.get("draw_pin_names"): + pt = pin.pt * part.tx + draw_text(pin.name, pt, scr, tx, font, color=(200, 0, 200)) + + # TODO: remove debug things + import pygame + pygame.draw.circle(scr, (255, 0, 0), (100, 100), 10) + pygame.draw.circle(scr, (0, 255, 0), (150, 100), 10) + pygame.draw.circle(scr, (0, 0, 255), (100, 150), 10) def draw_net( @@ -247,6 +260,7 @@ def draw_placement( scr: "pygame.Surface", tx: Tx, font: "pygame.font.Font", + **options, ): """Draw placement of parts and interconnecting nets. @@ -259,7 +273,7 @@ def draw_placement( """ draw_clear(scr) for part in parts: - draw_part(part, scr, tx, font) + draw_part(part, scr, tx, font, **options) draw_force(part, getattr(part, "force", Vector(0, 0)), scr, tx, font) for net in nets: draw_net(net, parts, scr, tx, font) @@ -330,7 +344,7 @@ def draw_start( import pygame # Screen drawing area. - scr_bbox = BBox(Point(0, 0), Point(1000, 1000)) + scr_bbox = BBox(Point(0, 0), Point(1500, 1500)) # Place a blank region around the object by expanding it's bounding box. border = max(bbox.w, bbox.h) / 20 diff --git a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py index 8f15dfe0..275f09f0 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/gen_schematic.py @@ -305,9 +305,9 @@ def get_A_size(bbox: BBox) -> str: def calc_sheet_tx(bbox): """Compute the page size and positioning for this sheet.""" A_size = get_A_size(bbox) - page_bbox = bbox * Tx() + page_bbox = bbox * Tx(d=-1) move_to_ctr = A_sizes[A_size].ctr.snap(GRID) - page_bbox.ctr.snap(GRID) - move_tx = Tx().move(move_to_ctr) + move_tx = Tx(d=-1).move(move_to_ctr) return move_tx @@ -515,7 +515,7 @@ def node_to_eeschema(node: SchNode, sheet_tx: Tx = Tx()) -> str: # TODO: Handle symio attribute. -def _ideal_part_rotation(part: Part) -> float: +def _ideal_part_rotation(part: Part) -> tuple[float, float]: # Tally what rotation would make each pwr/gnd pin point up or down. def is_pwr(pin: Pin) -> bool: return pin.fab_is_pwr @@ -523,34 +523,25 @@ def is_pwr(pin: Pin) -> bool: def is_gnd(pin: Pin) -> bool: return pin.fab_is_gnd + def rotation_for(start: str, finish: str) -> float: + seq = ["L", "U", "R", "D"] * 2 + start_idx = seq.index(start) + finish_idx = seq.index(finish, start_idx) + return (finish_idx - start_idx) * 90 + rotation_tally = Counter() - for pin in part: + for pin in part.pins: if is_pwr(pin): - if pin.orientation == "D": - rotation_tally[0] += 1 - if pin.orientation == "U": - rotation_tally[180] += 1 - if pin.orientation == "L": - rotation_tally[90] += 1 - if pin.orientation == "R": - rotation_tally[270] += 1 + rotation_tally[rotation_for("D", pin.orientation)] += 1 elif is_gnd(pin): - if pin.orientation == "U": - rotation_tally[0] += 1 - if pin.orientation == "D": - rotation_tally[180] += 1 - if pin.orientation == "L": - rotation_tally[270] += 1 - if pin.orientation == "R": - rotation_tally[90] += 1 + rotation_tally[rotation_for("U", pin.orientation)] += 1 + # TODO: add support for IO pins left and right # Rotate the part unit in the direction with the most tallies. - try: - rotation = rotation_tally.most_common()[0][0] - except IndexError: - return 0 - - return rotation + if most_common := rotation_tally.most_common(1): + assert len(most_common) == 1 + return most_common[0][0], most_common[0][1] / rotation_tally.total() + return 0, 0 def preprocess_circuit(circuit: Circuit, **options: Unpack[Options]): @@ -607,8 +598,12 @@ def rotate_power_pins(part: Part): if len(part_unit) > dont_rotate_pin_cnt: return - rotation = _ideal_part_rotation(part_unit) - part_unit.tx = part_unit.tx.rot_ccw(rotation) + rotation, certainty = _ideal_part_rotation(part_unit) + if certainty: + part_unit.tx = part_unit.tx.rot_ccw(rotation) + + if certainty >= part_unit.hints.lock_rotation_certainty: + part_unit.orientation_locked = True def calc_part_bbox(part: Part): """Calculate the labeled bounding boxes and store it in the part.""" diff --git a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py index 435983d1..2e899d61 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/geometry.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/geometry.py @@ -203,11 +203,27 @@ def _check(tx: Tx) -> bool: # Some common rotations. -Tx.ROT_CCW_90 = Tx(a=0, b=-1, c=1, d=0) -Tx.ROT_CW_0 = Tx(a=1, b=0, c=0, d=1) -Tx.ROT_CW_90 = Tx(a=0, b=1, c=-1, d=0) -Tx.ROT_CW_180 = Tx(a=-1, b=0, c=0, d=-1) -Tx.ROT_CW_270 = Tx(a=0, b=-1, c=1, d=0) +# DANGER! These keywords are out of order! +Tx.ROT_CCW_90 = Tx( + a=0, b=1, + c=-1, d=0 +) +Tx.ROT_CW_0 = Tx( + a=1, b=0, + c=0, d=1 +) +Tx.ROT_CW_90 = Tx( + a=0, b=-1, + c=1, d=0 +) +Tx.ROT_CW_180 = Tx( + a=-1, b=0, + c=0, d=-1 +) +Tx.ROT_CW_270 = Tx( + a=0, b=1, + c=-1, d=0 +) # Some common flips. Tx.FLIP_X = Tx(a=-1, b=0, c=0, d=1) @@ -248,7 +264,7 @@ def __sub__(self, pt: "Point | float") -> "Point": pt = Point(pt, pt) return Point(self.x - pt.x, self.y - pt.y) - def __mul__(self, m: "Tx | Point") -> "Point": + def __mul__(self, m: "Tx | Point | float") -> "Point": """Apply transformation matrix or scale factor to a point and return a point.""" if isinstance(m, Tx): return Point( diff --git a/src/faebryk/exporters/schematic/kicad/skidl/place.py b/src/faebryk/exporters/schematic/kicad/skidl/place.py index 3648fcb7..39b11cf5 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/place.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/place.py @@ -6,7 +6,6 @@ Autoplacer for arranging symbols in a schematic. """ -import contextlib import functools import itertools import math @@ -15,13 +14,12 @@ from copy import copy from typing import TYPE_CHECKING, Callable, Unpack +import logging from .constants import BLK_EXT_PAD, BLK_INT_PAD, GRID from .debug_draw import ( optional_draw_context, - draw_pause, draw_placement, draw_redraw, - draw_start, draw_text, ) from .geometry import BBox, Point, Tx, Vector @@ -31,6 +29,10 @@ from .net_terminal import NetTerminal from .node import SchNode + +logger = logging.getLogger(__name__) + + ################################################################### # # OVERVIEW OF AUTOPLACER @@ -1001,7 +1003,7 @@ def cost(parts: list[Part], alpha: float) -> float: if scr: # Draw current part placement for debugging purposes. - draw_placement(parts, nets, scr, tx, font) + draw_placement(parts, nets, scr, tx, font, **options) draw_text( "alpha:{alpha:3.2f} iter:{_} force:{sum_of_forces:.1f} stable:{stable_threshold}".format( **locals() @@ -1291,6 +1293,12 @@ def place_connected_parts( net_terminals = [part for part in parts if is_net_terminal(part)] real_parts = [part for part in parts if not is_net_terminal(part)] + if logger.isEnabledFor(logging.DEBUG): + for part in real_parts: + logger.debug(f"{part.ref=} {part.tx=}") + for pin in part: + logger.debug(f" {pin.name=} {pin.pt=}") + # Do the first trial placement. evolve_placement([], real_parts, nets, total_part_force, **options) diff --git a/src/faebryk/exporters/schematic/kicad/skidl/shims.py b/src/faebryk/exporters/schematic/kicad/skidl/shims.py index a82bc887..cef0882f 100644 --- a/src/faebryk/exporters/schematic/kicad/skidl/shims.py +++ b/src/faebryk/exporters/schematic/kicad/skidl/shims.py @@ -120,7 +120,7 @@ def audit(self) -> None: saved_anchor_pins: dict[Any, list["Pin"]] # copy of anchor_pins saved_pull_pins: dict[Any, list["Pin"]] # copy of pull_pins top_track: "GlobalTrack" - tx: Tx # transformation matrix of the part's position + _tx: Tx # transformation matrix of the part's position def __iter__(self) -> Iterator["Pin"]: yield from self.pins @@ -147,6 +147,14 @@ def similarity(self, part: "Part", **options) -> float: assert not options, "No options supported" return self._similarites[part] + @property + def tx(self) -> Tx: + return self._tx + + @tx.setter + def tx(self, tx: Tx) -> None: + self._tx = tx + class PartUnit(Part): # TODO: represent these in Faebryk @@ -300,6 +308,7 @@ class Options(TypedDict): draw_assigned_terminals: bool draw_font: str draw_global_routing: bool + draw_pin_names: bool draw_placement: bool draw_routing_channels: bool draw_routing: bool diff --git a/test/exporters/schematic/kicad/test_skidl_gen_schematic.py b/test/exporters/schematic/kicad/test_skidl_gen_schematic.py index a42a40bd..ac8d936e 100644 --- a/test/exporters/schematic/kicad/test_skidl_gen_schematic.py +++ b/test/exporters/schematic/kicad/test_skidl_gen_schematic.py @@ -25,14 +25,15 @@ def part() -> Part: @pytest.mark.parametrize( - "pwr_pins, gnd_pins, expected", + "pwr_pins, gnd_pins, expected, certainty", [ - (["U"], ["D"], 180), - (["L"], ["R"], 90), - (["R"], ["L"], 270), + (["U"], ["D"], 180, 1.0), + (["L"], ["R"], 90, 1.0), + (["R"], ["L"], 270, 1.0), + ([], [], 0, 0), ], ) -def test_ideal_part_rotation(part, pwr_pins, gnd_pins, expected): +def test_ideal_part_rotation(part, pwr_pins, gnd_pins, expected, certainty): from faebryk.exporters.schematic.kicad.skidl.gen_schematic import ( _ideal_part_rotation, ) @@ -50,4 +51,4 @@ def test_ideal_part_rotation(part, pwr_pins, gnd_pins, expected): pin.fab_is_pwr = False pin.fab_is_gnd = False - assert _ideal_part_rotation(part) == expected + assert _ideal_part_rotation(part) == (expected, certainty) diff --git a/test/exporters/schematic/kicad/test_skidl_geometry.py b/test/exporters/schematic/kicad/test_skidl_geometry.py index ee1c51e6..9983265d 100644 --- a/test/exporters/schematic/kicad/test_skidl_geometry.py +++ b/test/exporters/schematic/kicad/test_skidl_geometry.py @@ -1,6 +1,6 @@ import pytest -from faebryk.exporters.schematic.kicad.skidl.geometry import Tx +from faebryk.exporters.schematic.kicad.skidl.geometry import Point, Tx @pytest.mark.parametrize( @@ -28,3 +28,22 @@ def test_find_orientation(tx: Tx, expected: tuple[float, bool]): def test_rot_cw(degs: float, expected: Tx): tx = Tx() assert tx.rot_cw(degs) == expected + + +@pytest.mark.parametrize( + "tx, expected", + [ + (Tx(), (1, 0)), + (Tx.ROT_CCW_90, (0, 1)), + (Tx.FLIP_X, (-1, 0)), + (Tx.FLIP_X * Tx.ROT_CCW_90, (0, -1)), + (Tx.FLIP_Y, (1, 0)), + (Tx.ROT_CCW_90 * Tx.FLIP_Y, (0, -1)), + (Tx.ROT_CW_90 * Tx.FLIP_Y, (0, 1)), + (Tx.ROT_CW_180, (-1, 0)), + (Tx.ROT_CW_270, (0, 1)), + ], +) +def test_find_orientation_from_tx(tx: Tx, expected: tuple[float, float]): + txd = Point(1, 0) * tx + assert txd == Point(expected[0], expected[1]) From 0e31597095eb5cf2f481ddbdda607462a3cc914b Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 1 Nov 2024 17:32:56 -0700 Subject: [PATCH 83/85] Rotations work! Yeeeeewwww! --- dev_schematics.py | 19 +++++++++----- .../exporters/schematic/kicad/transformer.py | 26 +++++++++++++++---- src/faebryk/libs/kicad/fileformats_sch.py | 7 ++++- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/dev_schematics.py b/dev_schematics.py index f64ce7ac..2ca76964 100644 --- a/dev_schematics.py +++ b/dev_schematics.py @@ -1,15 +1,17 @@ """ Issues: -- [ ] Parts aren't being rotated properly +- [ ] Component references aren't aligned to anything in particular -- [ ] Multiple net terminals of the same net +- [ ] Add top-level global labels -- [ ] Component references aren't aligned to anything in particular +- [x] Multiple net terminals of the same net -- [x] Battery's 180 degrees off +- [x] X/Y flips are dependent on the rotation of the part -- [ ] Add top-level global labels +- [x] Parts aren't being rotated properly + +- [x] Battery's 180 degrees off - [x] Unit should be 1, not 0 @@ -31,6 +33,7 @@ """ import contextlib +import logging import sys from pathlib import Path @@ -41,6 +44,9 @@ from faebryk.libs.examples.buildutil import apply_design_to_pcb from faebryk.libs.kicad.fileformats_sch import C_kicad_sch_file +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + root_dir = Path(__file__).parent test_dir = root_dir / "test" build_dir = root_dir / "build" @@ -78,7 +84,8 @@ def add_to_sys_path(path): options = Options( # draw_global_routing=True, - # draw_placement=True, + draw_placement=True, + draw_pin_names=True, # draw_routing=True, # draw_routing_channels=True, # draw_switchbox_boundary=True, diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index b0ede07e..510b46bc 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -990,9 +990,23 @@ def _apply_shim_sch_node( # are already in the schematic, we just need to move them part.sch_symbol.at.x = part_tx.origin.x part.sch_symbol.at.y = part_tx.origin.y - flip_x, rot_ccw = part_tx.find_orientation() - part.sch_symbol.at.r = rot_ccw - part.sch_symbol.mirror = part.sch_symbol.E_mirror.x if flip_x else None + + # The allowed mirrors are dependent on the rotation of the part + flip_rot_conversions: dict[ + tuple[bool, int], tuple[SCH.C_symbol_instance.E_mirror, int] + ] = { + (False, 0): (None, 0), + (True, 0): (SCH.C_symbol_instance.E_mirror.y, 0), + (False, 90): (None, 90), + (True, 90): (SCH.C_symbol_instance.E_mirror.x, 90), + (False, 180): (None, 180), + (True, 180): (SCH.C_symbol_instance.E_mirror.x, 0), + (False, 270): (None, 270), + (True, 270): (SCH.C_symbol_instance.E_mirror.x, 270), + } + part.sch_symbol.mirror, part.sch_symbol.at.r = flip_rot_conversions[ + part.tx.find_orientation() + ] # 4. draw wires and junctions for wire in node.wires.values(): @@ -1017,7 +1031,9 @@ def dfs(node: skidl_node.SchNode): for node in dfs(circuit): self._apply_shim_sch_node(node) - def generate_schematic(self, **options: Unpack[shims.Options]): + def generate_schematic( + self, **options: Unpack[shims.Options] + ) -> tuple[skidl_node.SchNode, C_kicad_sch_file]: """Does what it says on the tin.""" # 1. add missing symbols self._add_missing_symbols() @@ -1034,4 +1050,4 @@ def generate_schematic(self, **options: Unpack[shims.Options]): self.apply_shim_sch_node(sch) # 5. save - return self.sch + return circuit, sch diff --git a/src/faebryk/libs/kicad/fileformats_sch.py b/src/faebryk/libs/kicad/fileformats_sch.py index 0fa8a95a..ca9c91f6 100644 --- a/src/faebryk/libs/kicad/fileformats_sch.py +++ b/src/faebryk/libs/kicad/fileformats_sch.py @@ -223,7 +223,12 @@ class C_pin: uuid: UUID = uuid_field() class E_mirror(SymEnum): + """ + Mirroring is applied about the X or Y axes + The allowed mirrors are dependent on the rotation of the part + """ x = "x" + y = "y" lib_id: str at: C_xyr @@ -232,7 +237,7 @@ class E_mirror(SymEnum): on_board: bool = False uuid: UUID = uuid_field() fields_autoplaced: bool = True - mirror: Optional[E_mirror] = None # Can be "x" for x-axis mirroring + mirror: Optional[E_mirror] = None propertys: dict[str, C_property] = field( **sexp_field(multidict=True, key=lambda x: x.name), default_factory=dict, From 3e23c291af6d9677dfe9c058bd50e183a934c5b8 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Fri, 1 Nov 2024 18:10:06 -0700 Subject: [PATCH 84/85] Add basic options to the schematic transformer --- dev_schematics.py | 35 +++++-------------- .../exporters/schematic/kicad/transformer.py | 32 +++++++++++++++-- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/dev_schematics.py b/dev_schematics.py index 2ca76964..0b868702 100644 --- a/dev_schematics.py +++ b/dev_schematics.py @@ -64,17 +64,17 @@ def add_to_sys_path(path): with add_to_sys_path(root_dir / "examples"): - from minimal_led import App -app = App() -assert isinstance(app, Module) -assert isinstance(app.battery, F.Battery) -assert isinstance(app.led, F.PoweredLED) + from mcu import App +app = App() -app.led.led.add(F.has_descriptive_properties_defined({"LCSC": "C7429912"})) -app.led.current_limiting_resistor.add( - F.has_descriptive_properties_defined({"LCSC": "C25077"}) -) +# assert isinstance(app, Module) +# assert isinstance(app.battery, F.Battery) +# assert isinstance(app.led, F.PoweredLED) +# app.led.led.add(F.has_descriptive_properties_defined({"LCSC": "C7429912"})) +# app.led.current_limiting_resistor.add( +# F.has_descriptive_properties_defined({"LCSC": "C25077"}) +# ) apply_design_to_pcb(app) @@ -90,23 +90,6 @@ def add_to_sys_path(path): # draw_routing_channels=True, # draw_switchbox_boundary=True, # draw_switchbox_routing=True, - retries=3, - pin_normalize=True, - net_normalize=True, - compress_before_place=True, - use_optimizer=True, - use_push_pull=True, - allow_jumps=True, - align_parts=True, - remove_overlaps=True, - slip_and_slide=True, - # rotate_parts=True, # Doesn't work. It's forced on in a lower-level - trim_anchor_pull_pins=True, - # fanout_attenuation=True, - # remove_power=True, - # remove_high_fanout=True, - normalize=True, - flatness=1.0, ) sch = full_transformer.generate_schematic(**options) diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 510b46bc..5e27be13 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -3,7 +3,6 @@ import hashlib import logging -import math import pprint from copy import deepcopy from dataclasses import is_dataclass @@ -1035,6 +1034,35 @@ def generate_schematic( self, **options: Unpack[shims.Options] ) -> tuple[skidl_node.SchNode, C_kicad_sch_file]: """Does what it says on the tin.""" + _options = shims.Options( + # draw_global_routing=True, + # draw_placement=True, + # draw_pin_names=True, + # draw_routing=True, + # draw_routing_channels=True, + # draw_switchbox_boundary=True, + # draw_switchbox_routing=True, + retries=3, + pin_normalize=True, + net_normalize=True, + compress_before_place=True, + use_optimizer=True, + use_push_pull=True, + allow_jumps=True, + align_parts=True, + remove_overlaps=True, + slip_and_slide=True, + # rotate_parts=True, # Doesn't work. It's forced on in a lower-level + trim_anchor_pull_pins=True, + # fanout_attenuation=True, + # remove_power=True, + # remove_high_fanout=True, + normalize=True, + flatness=1.0, + ) + + _options.update(options) + # 1. add missing symbols self._add_missing_symbols() @@ -1044,7 +1072,7 @@ def generate_schematic( # 3. run skidl schematic generation from faebryk.exporters.schematic.kicad.skidl.gen_schematic import gen_schematic - sch = gen_schematic(circuit, ".", "test", **options) + sch = gen_schematic(circuit, ".", "test", **_options) # 4. transform sch according to skidl self.apply_shim_sch_node(sch) From c01e06dbd57f7c89f97bffe2a0c5c9b1e82b43d7 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Sun, 3 Nov 2024 08:33:21 -0800 Subject: [PATCH 85/85] Fix for alternative circle definition --- dev_schematics.py | 2 +- src/faebryk/exporters/schematic/kicad/transformer.py | 8 +++++++- src/faebryk/libs/kicad/fileformats_sch.py | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/dev_schematics.py b/dev_schematics.py index 0b868702..c59f2153 100644 --- a/dev_schematics.py +++ b/dev_schematics.py @@ -64,7 +64,7 @@ def add_to_sys_path(path): with add_to_sys_path(root_dir / "examples"): - from mcu import App + from iterative_design_nand import App app = App() diff --git a/src/faebryk/exporters/schematic/kicad/transformer.py b/src/faebryk/exporters/schematic/kicad/transformer.py index 5e27be13..a47dfc4c 100644 --- a/src/faebryk/exporters/schematic/kicad/transformer.py +++ b/src/faebryk/exporters/schematic/kicad/transformer.py @@ -639,7 +639,13 @@ def _(obj: C_rect) -> BoundingBox | None: @get_bbox.register @staticmethod def _(obj: C_circle) -> BoundingBox: - radius = Geometry.distance_euclid(obj.center, obj.end) + if obj.radius is None: + radius = Geometry.distance_euclid(obj.center, obj.end) + elif obj.end is None: + radius = obj.radius + else: + raise ValueError("Circle has both radius and end") + return Geometry.bbox( (obj.center.x - radius, obj.center.y - radius), (obj.center.x + radius, obj.center.y + radius), diff --git a/src/faebryk/libs/kicad/fileformats_sch.py b/src/faebryk/libs/kicad/fileformats_sch.py index ca9c91f6..2edf6962 100644 --- a/src/faebryk/libs/kicad/fileformats_sch.py +++ b/src/faebryk/libs/kicad/fileformats_sch.py @@ -69,9 +69,10 @@ class E_type(SymEnum): @dataclass(kw_only=True) class C_circle: center: C_xy - end: C_xy stroke: C_stroke fill: C_fill + radius: Optional[float] = None + end: Optional[C_xy] = None @dataclass(kw_only=True)