krishnax12/t113-minimal-linux

Allwinner T113-S3 dual-Cortex-A7 SoC with integrated 128MB DDR3, multi-rail decoupling, 24MHz and 32.768kHz oscillators, filtering, and exposed test points.

Version
0.1.1
License
unset
Stars
0

scripts/route-remaining.py

"""Route remaining pad groups around the exact saved copper on a 0.1 mm grid.

Saved traces are immutable obstacles to foreign nets. New through vias must
clear copper on every layer and all existing drills. The independent final
copper audit remains the release gate.
"""
import collections
import heapq
import itertools
import json
import math
import sys
from pathlib import Path

import numpy as np
from scipy.ndimage import distance_transform_edt
from shapely import intersects_xy
from shapely.geometry import LineString, Point, Polygon
from shapely.ops import unary_union
from shapely.strtree import STRtree

from routing_geometry import LAYERS, Nets, audit, copper, fill_planes, geom

STEP = .1
CLEARANCE = .151
OUTER_LAYERS = ['top', 'bottom']

class RemainingRouter:
    def __init__(self, circuit):
        self.circuit = circuit
        self.board = next(record for record in circuit if record['type'] == 'pcb_board')
        self.boundary = Polygon([(point['x'], point['y']) for point in self.board['outline']])
        xmin, ymin, xmax, ymax = self.boundary.bounds
        self.x = np.arange(xmin + STEP / 2, xmax, STEP)
        self.y = np.arange(ymin + STEP / 2, ymax, STEP)
        self.xx, self.yy = np.meshgrid(self.x, self.y)
        self.rows, self.cols = self.xx.shape
        self.cells = self.rows * self.cols
        self.serial = 0

    def raster(self, shape):
        return intersects_xy(shape, self.xx, self.yy)

    def refresh(self):
        self.circuit = fill_planes(self.circuit)
        self.items, self.nets = copper(self.circuit)
        for pour in self.nets.of('pcb_copper_pour'):
            shape = pour['brep_shape']
            points = lambda ring: [(point['x'], point['y']) for point in ring['vertices']]
            self.items.append(dict(id=pour['pcb_copper_pour_id'], kind='plane', net=self.nets.net(pour), layer=pour['layer'], geometry=Polygon(points(shape['outer_ring']), [points(ring) for ring in shape.get('inner_rings', [])])))

    def groups(self, net):
        items = [item for item in self.items if item['net'] == net]
        parent = list(range(len(items)))
        def find(index):
            while parent[index] != index:
                parent[index] = parent[parent[index]]
                index = parent[index]
            return index
        def join(pair):
            parent[find(pair[0])] = find(pair[1])
        ids = collections.defaultdict(list)
        ports = collections.defaultdict(list)
        for index, item in enumerate(items):
            ids[item['id']].append(index)
            if item.get('source_port_id'):
                ports[item['source_port_id']].append(index)
        for indices in ids.values():
            for index in indices[1:]:
                join((indices[0], index))
        for connected_ports in self.nets.permanent_internal_connections():
            indices = [index for port in connected_ports for index in ports[port]]
            for index in indices[1:]:
                join((indices[0], index))
        for layer in LAYERS:
            chosen = [index for index, item in enumerate(items) if item['layer'] == layer]
            shapes = [items[index]['geometry'] for index in chosen]
            tree = STRtree(shapes)
            for offset, index in enumerate(chosen):
                for hit in tree.query(shapes[offset].buffer(.0006)):
                    other = chosen[int(hit)]
                    if other > index and items[index]['geometry'].distance(items[other]['geometry']) < .0006:
                        join((index, other))
        roots = {find(index) for index, item in enumerate(items) if item.get('source_port_id')}
        groups = collections.defaultdict(list)
        for index, item in enumerate(items):
            if find(index) in roots:
                groups[find(index)].append(item)
        return sorted(groups.values(), key=len)

    def coordinate(self, state):
        layer, cell = divmod(state, self.cells)
        row, col = divmod(cell, self.cols)
        return layer, row, col

    def point(self, state):
        _, row, col = self.coordinate(state)
        return float(self.x[col]), float(self.y[row])

    def solve(self, net):
        self.refresh()
        groups = self.groups(net)
        if len(groups) < 2:
            return False
        width = .4 if net in {'V5', 'V0V9', 'V3V3', 'GND'} else .15
        source_group, targets = groups[0], [item for group in groups[1:] for item in group]
        foreign = [item for item in self.items if item['net'] != net and item['kind'] != 'plane']
        holes = [geom(record, drill=True) for record in self.nets.of('pcb_hole')]
        obstacles = []
        blocked = []
        for layer in OUTER_LAYERS:
            shapes = [item['geometry'].buffer(CLEARANCE + width / 2) for item in foreign if item['layer'] == layer]
            shapes.extend(hole.buffer(CLEARANCE + width / 2) for hole in holes)
            obstacle = unary_union(shapes)
            obstacles.append(obstacle)
            blocked.append(self.raster(obstacle) | ~self.raster(self.boundary.buffer(-(.19 + width / 2))))
        drilled = self.nets.of('pcb_via') + self.nets.of('pcb_plated_hole') + self.nets.of('pcb_hole')
        via_obstacle = unary_union([item['geometry'].buffer(.3 + CLEARANCE) for item in foreign] + [geom(record, drill=True).buffer(.3 + CLEARANCE if record['type'] == 'pcb_hole' else .301) for record in drilled])
        via_allowed = ~self.raster(via_obstacle) & self.raster(self.boundary.buffer(-.491))
        existing_vias = unary_union([item['geometry'] for item in self.items if item['net'] == net and item['kind'] in {'via', 'pad'} and sum(other['id'] == item['id'] and other['layer'] in OUTER_LAYERS for other in self.items) > 1])
        existing_via_mask = self.raster(existing_vias)
        transfer = via_allowed | existing_via_mask
        starts = []
        goals = []
        surface_goals = []
        inner_targets = unary_union([item['geometry'] for item in targets if item['layer'] in {'inner1', 'inner2'}])
        plane_goals = self.raster(inner_targets) & transfer
        for index, layer in enumerate(OUTER_LAYERS):
            start_shape = unary_union([item['geometry'] for item in source_group if item['layer'] == layer])
            goal_shape = unary_union([item['geometry'] for item in targets if item['layer'] == layer])
            starts.append(self.raster(start_shape.buffer(width / 2 - .001)) & ~blocked[index])
            surface = self.raster(goal_shape.buffer(width / 2 - .001))
            surface_goals.append(surface)
            goals.append((surface | plane_goals) & ~blocked[index])
        goal_mask = np.stack(goals)
        start_mask = np.stack(starts)
        if not start_mask.any() or not goal_mask.any():
            raise RuntimeError(f'{net}: no accessible source/target grid nodes')
        heuristic = distance_transform_edt(~(goals[0] | goals[1])) * STEP
        blocked_edges = set()
        for attempt in range(12):
            queue = []
            distances = {}
            parents = {}
            serial = itertools.count()
            for state in np.flatnonzero(start_mask.ravel()):
                state = int(state)
                _, row, col = self.coordinate(state)
                distances[state] = 0.0
                heapq.heappush(queue, (float(heuristic[row, col]), next(serial), state))
            found = None
            while queue:
                estimate, _, state = heapq.heappop(queue)
                layer, row, col = self.coordinate(state)
                cost = distances[state]
                if estimate > cost + heuristic[row, col] + 1e-9:
                    continue
                if goal_mask[layer, row, col]:
                    found = state
                    break
                for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]:
                    nr, nc = row + dr, col + dc
                    if nr < 0 or nr >= self.rows or nc < 0 or nc >= self.cols or blocked[layer][nr, nc]:
                        continue
                    if dr and dc and (blocked[layer][row, nc] or blocked[layer][nr, col]):
                        continue
                    neighbor = layer * self.cells + nr * self.cols + nc
                    if (state, neighbor) in blocked_edges:
                        continue
                    proposed = cost + STEP * (math.sqrt(2) if dr and dc else 1)
                    if proposed < distances.get(neighbor, math.inf):
                        distances[neighbor] = proposed
                        parents[neighbor] = state
                        heapq.heappush(queue, (proposed + float(heuristic[nr, nc]), next(serial), neighbor))
                if transfer[row, col] and not blocked[1 - layer][row, col]:
                    neighbor = (1 - layer) * self.cells + row * self.cols + col
                    proposed = cost + (0.2 if existing_via_mask[row, col] else 3)
                    if proposed < distances.get(neighbor, math.inf):
                        distances[neighbor] = proposed
                        parents[neighbor] = state
                        heapq.heappush(queue, (proposed + float(heuristic[row, col]), next(serial), neighbor))
            if found is None:
                raise RuntimeError(f'{net}: no path at {width} mm width')
            path = [found]
            while path[-1] in parents:
                path.append(parents[path[-1]])
            path.reverse()
            invalid = []
            for a, b in zip(path, path[1:]):
                layer_a = self.coordinate(a)[0]
                if layer_a == self.coordinate(b)[0] and LineString([self.point(a), self.point(b)]).intersects(obstacles[layer_a]):
                    invalid.append((a, b))
            if invalid:
                blocked_edges.update(invalid)
                blocked_edges.update((b, a) for a, b in invalid)
                continue
            self.active_net = net
            self.active_width = width
            self.active_existing_vias = existing_via_mask
            self.add_path(path)
            layer, row, col = self.coordinate(found)
            if not surface_goals[layer][row, col] and not existing_via_mask[row, col]:
                self.add_via(self.point(found))
            print(json.dumps({'net': net, 'groupsBefore': len(groups), 'pathLengthMm': round(distances[found], 3), 'gridNodes': len(path)}), flush=True)
            return True
        raise RuntimeError(f'{net}: could not clear raster corner conflicts')

    def metadata(self):
        return {'source_net_id': self.nets.ids[self.active_net], 'subcircuit_id': self.board['subcircuit_id']}

    def add_via(self, point):
        self.serial += 1
        self.circuit.append(dict(type='pcb_via', pcb_via_id=f'pcb_via_grid_{self.serial}', x=point[0], y=point[1], outer_diameter=.6, hole_diameter=.3, layers=LAYERS, from_layer='top', to_layer='bottom', **self.metadata()))

    def add_path(self, path):
        runs = [[path[0]]]
        for previous, state in zip(path, path[1:]):
            layer, row, col = self.coordinate(state)
            if layer != self.coordinate(previous)[0]:
                if not self.active_existing_vias[row, col]:
                    self.add_via(self.point(state))
                runs.append([state])
            else:
                runs[-1].append(state)
        for run in runs:
            if len(run) < 2:
                continue
            kept = [run[0]]
            last_direction = None
            for index in range(1, len(run)):
                a, b = self.coordinate(run[index - 1]), self.coordinate(run[index])
                direction = (b[1] - a[1], b[2] - a[2])
                if last_direction is not None and direction != last_direction:
                    kept.append(run[index - 1])
                last_direction = direction
            kept.append(run[-1])
            layer = OUTER_LAYERS[self.coordinate(run[0])[0]]
            route = [dict(route_type='wire', x=self.point(state)[0], y=self.point(state)[1], width=self.active_width, layer=layer) for state in kept]
            self.serial += 1
            self.circuit.append(dict(type='pcb_trace', pcb_trace_id=f'pcb_trace_grid_{self.serial}', route=route, **self.metadata()))

source_path, destination = map(Path, sys.argv[1:3])
router = RemainingRouter(json.loads(source_path.read_text()))
router.refresh()
initial = audit(router.circuit)
assert not initial['violations'], initial['violationCounts']
for entry in sorted(initial['unconnectedNets'], key=lambda entry: (entry['net'] not in {'V0V9', 'V5'}, entry['net'])):
    for connection in range(20):
        if not router.solve(entry['net']):
            break
    else:
        raise RuntimeError('Connection limit reached for ' + entry['net'])
router.refresh()
destination.write_text(json.dumps(router.circuit, separators=(',', ':')) + '\n')
report = audit(router.circuit)
destination.with_suffix('.routing-check.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps({'passed': report['passed'], 'unconnectedNets': len(report['unconnectedNets']), 'violations': report['violationCounts']}), flush=True)
if not report['passed']:
    sys.exit(1)