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/routing_geometry.py

import json,math,re,collections
from pathlib import Path
from shapely.geometry import Point,LineString,Polygon,box
from shapely.affinity import rotate
from shapely.ops import unary_union
from shapely.strtree import STRtree

LAYERS=['top','inner1','inner2','bottom']
LMAP={'F.Cu':'top','In1.Cu':'inner1','In2.Cu':'inner2','B.Cu':'bottom'}
REMOVABLE_JUMPERS={'JP_UART_RX','JP_UART_TX'}
class Nets:
    def __init__(self,data):
        self.parent={};self.data=data
        # Fitted shunts complete the schematic circuit, but PCB copper must
        # leave their contacts isolated when the user removes the shunt.
        self.removable={c['source_component_id']:c['name'] for c in self.of('source_component') if c.get('name') in REMOVABLE_JUMPERS}
        for t in self.of('source_trace'):self.join(t.get('connected_source_port_ids',[])+t.get('connected_source_net_ids',[]))
        for ids in self.permanent_internal_connections():self.join(ids)
        self.names={self.find(n['source_net_id']):n['name'] for n in self.of('source_net')}
        self.ids={n['name']:n['source_net_id'] for n in self.of('source_net')}
        self.ports={p['pcb_port_id']:p for p in self.of('pcb_port')}
        self.traces={t['source_trace_id']:t for t in self.of('source_trace')}
    def of(self,t):return [x for x in self.data if x['type']==t]
    def permanent_internal_connections(self):
        return [ids for c in self.of('source_component') if c['source_component_id'] not in self.removable for ids in c.get('internally_connected_source_port_ids',[])] + [c.get('source_port_ids',[]) for c in self.of('source_component_internal_connection') if c.get('source_component_id') not in self.removable]
    def find(self,x):
        self.parent.setdefault(x,x)
        if self.parent[x]!=x:self.parent[x]=self.find(self.parent[x])
        return self.parent[x]
    def join(self,ids):
        for x in ids[1:]:self.parent[self.find(x)]=self.find(ids[0])
    def source(self,sid):return self.names.get(self.find(sid),self.find(sid))
    def net(self,g):
        if g.get('source_net_id'):return self.source(g['source_net_id'])
        if g.get('source_trace_id'):
            t=self.traces[g['source_trace_id']];ids=t.get('connected_source_port_ids',[])+t.get('connected_source_net_ids',[])
            return self.source(ids[0])
        if g.get('pcb_port_id') in self.ports:return self.source(self.ports[g['pcb_port_id']]['source_port_id'])
        return 'NC_'+str(g.get(g['type']+'_id'))

def geom(g,drill=False):
    x,y=g['x'],g['y'];s=g.get('shape',g.get('hole_shape','circle'))
    if s=='circle':return Point(x,y).buffer((g.get('hole_diameter') if drill else g.get('outer_diameter',g.get('diameter',g.get('hole_diameter'))))/2,quad_segs=24)
    if s=='rect' or s=='circular_hole_with_rect_pad':
        if drill:return Point(x,y).buffer(g['hole_diameter']/2,quad_segs=24)
        w=g.get('width',g.get('rect_pad_width',g.get('outer_width')));h=g.get('height',g.get('rect_pad_height',g.get('outer_height')))
        p=box(x-w/2,y-h/2,x+w/2,y+h/2)
        return rotate(p,g.get('ccw_rotation',0),origin=(x,y))
    if s=='pill':
        w=g['hole_width' if drill else 'outer_width'];h=g['hole_height' if drill else 'outer_height'];r=min(w,h)/2
        p=LineString([(x-(w/2-r),y-(h/2-r)),(x+(w/2-r),y+(h/2-r))]).buffer(r,quad_segs=24)
        return rotate(p,g.get('ccw_rotation',0),origin=(x,y))
    raise ValueError(s)

def copper(data):
    nets=Nets(data);items=[]
    def add(gid,kind,net,layer,geometry,**kw):items.append(dict(id=gid,kind=kind,net=net,layer=layer,geometry=geometry,**kw))
    for g in nets.of('pcb_smtpad')+nets.of('pcb_plated_hole')+nets.of('pcb_via'):
        gid=g[g['type']+'_id'];net=nets.net(g);ls=[g['layer']] if g['type']=='pcb_smtpad' else g.get('layers',LAYERS)
        for l in ls:add(gid,'pad' if g['type']!='pcb_via' else 'via',net,l,geom(g),source_port_id=nets.ports.get(g.get('pcb_port_id'),{}).get('source_port_id'))
    for t in nets.of('pcb_trace'):
        net=nets.net(t)
        for i,(a,b) in enumerate(zip(t['route'],t['route'][1:])):
            layer=width=None
            if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']:layer,width=a['layer'],max(a['width'],b['width'])
            elif a['route_type']=='wire' and b['route_type']=='via' and a['layer']==b['from_layer']:layer,width=a['layer'],a['width']
            elif a['route_type']=='via' and b['route_type']=='wire' and a['to_layer']==b['layer']:layer,width=b['layer'],b['width']
            if layer and (a['x'],a['y'])!=(b['x'],b['y']):
                add(t['pcb_trace_id']+'_'+str(i),'trace',net,layer,LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(width/2,quad_segs=16))
    return items,nets

def remove_standalone_plane_islands(data):
    """Remove only plane components with no physical copper contact.

    Return (cleaned_records, removed_pour_records), preserving retained records
    and IDs. Same-net pours can anchor one another on the same layer. Any pad,
    trace, via or plated-hole contact anchors a component regardless of net;
    even a floating route must survive for audit() to report. Foreign-net pour
    contact also anchors both sides so cleanup cannot conceal a plane short.
    """
    contact_tolerance=.0006  # Same physical-contact tolerance as audit().
    items,nets=copper(data);pours=nets.of('pcb_copper_pour')
    if not pours:return list(data),[]
    def pour_geometry(p):
        b=p['brep_shape'];points=lambda ring:[(q['x'],q['y']) for q in ring['vertices']]
        return Polygon(points(b['outer_ring']),[points(h) for h in b.get('inner_rings',[])])
    geometries=[pour_geometry(p) for p in pours]
    plane_nets=[nets.net(p) for p in pours]
    parent=list(range(len(pours)));anchored=set()
    def find(i):
        while parent[i]!=i:
            parent[i]=parent[parent[i]];i=parent[i]
        return i
    def join(a,b):parent[find(a)]=find(b)
    for layer in LAYERS:
        chosen=[i for i,p in enumerate(pours) if p['layer']==layer]
        gs=[geometries[i] for i in chosen];plane_tree=STRtree(gs)
        nonplanes=[obj['geometry'] for obj in items if obj['layer']==layer]
        copper_tree=STRtree(nonplanes)
        for idx,i in enumerate(chosen):
            shape=gs[idx];nearby=shape.buffer(contact_tolerance)
            if any(shape.distance(nonplanes[int(hit)])<contact_tolerance for hit in copper_tree.query(nearby)):
                anchored.add(i)
            for hit in plane_tree.query(nearby):
                k=chosen[int(hit)]
                if k<=i or shape.distance(geometries[k])>=contact_tolerance:continue
                if plane_nets[i]==plane_nets[k]:join(i,k)
                else:anchored.update((i,k))
    anchored_components={find(i) for i in anchored}
    removed=[p for i,p in enumerate(pours) if find(i) not in anchored_components]
    removed_ids={p['pcb_copper_pour_id'] for p in removed}
    return [g for g in data if g.get('pcb_copper_pour_id') not in removed_ids],removed

def fill_planes(data,removed_islands=None):
    items,nets=copper(data);board=nets.of('pcb_board')[0]
    boundary=Polygon([(p['x'],p['y']) for p in board['outline']]).buffer(-0.5)
    holes=[geom(h).buffer(.2) for h in nets.of('pcb_hole')]
    out=[g for g in data if g['type']!='pcb_copper_pour']
    regions=json.loads((Path(__file__).resolve().parent.parent / 'routing' / 'plane-regions.json').read_text())
    for region in regions:
        layer,net=region['layer'],region['net']
        region_shape=Polygon([(p['x'],p['y']) for p in region['outline']]).intersection(boundary) if 'outline' in region else boundary
        # Leave 4 micrometres beyond the 0.15 mm clearance requirement.
        # A 2 micrometre outline simplification keeps exports compact while
        # retaining a positive clearance margin after coordinate rounding.
        cut=holes+[i['geometry'].buffer(.154,quad_segs=16) for i in items if i['layer']==layer and i['net']!=net]
        filled=region_shape.difference(unary_union(cut)).buffer(0).simplify(.002,preserve_topology=True)
        polygons=list(filled.geoms) if filled.geom_type=='MultiPolygon' else [filled]
        for p in polygons:
            if p.is_empty:continue
            def ring(coords):return {'vertices':[{'x':round(x,6),'y':round(y,6)} for x,y in list(coords)[:-1]]}
            out.append(dict(type='pcb_copper_pour',pcb_copper_pour_id='pcb_copper_pour_'+str(sum(x['type']=='pcb_copper_pour' for x in out)),shape='brep',layer=layer,source_net_id=nets.ids[net],subcircuit_id=nets.of('pcb_board')[0]['subcircuit_id'],covered_with_solder_mask=True,brep_shape={'outer_ring':ring(p.exterior.coords),'inner_rings':[ring(r.coords) for r in p.interiors]}))
    cleaned,removed=remove_standalone_plane_islands(out)
    if removed_islands is not None:removed_islands.extend(removed)
    return cleaned

def audit(data):
    items,nets=copper(data);parent={};issues=[]
    def f(i):
        parent.setdefault(i,i)
        if parent[i]!=i:parent[i]=f(parent[i])
        return parent[i]
    def j(a,b):parent[f(a)]=f(b)
    for p in nets.of('pcb_copper_pour'):
        b=p['brep_shape'];pts=lambda r:[(q['x'],q['y']) for q in r['vertices']]
        g=Polygon(pts(b['outer_ring']),[pts(r) for r in b.get('inner_rings',[])])
        items.append(dict(id=p['pcb_copper_pour_id'],kind='plane',net=nets.net(p),layer=p['layer'],geometry=g))
    ids=collections.defaultdict(list);ports=collections.defaultdict(list)
    for i,obj in enumerate(items):
        ids[obj['id']].append(i)
        if obj.get('source_port_id'):ports[obj['source_port_id']].append(i)
    for ii in ids.values():
        for i in ii[1:]:j(ii[0],i)
    required_ports={sid for t in nets.of('source_trace') for sid in t.get('connected_source_port_ids',[])}
    for sid in sorted(required_ports):
        if not ports.get(sid):issues.append(dict(type='missing_physical_endpoint',source_port_id=sid,net=nets.source(sid)))
    for source_ids in nets.permanent_internal_connections():
        ii=[i for sid in source_ids for i in ports[sid]]
        for i in ii[1:]:j(ii[0],i)
    for layer in LAYERS:
        chosen=[i for i,x in enumerate(items) if x['layer']==layer];gs=[items[i]['geometry'] for i in chosen];tree=STRtree(gs)
        for idx,i in enumerate(chosen):
            a=items[i]
            for hit in tree.query(gs[idx].buffer(.151)):
                k=chosen[int(hit)]
                if k<=i:continue
                b=items[k];distance=a['geometry'].distance(b['geometry'])
                # Actual copper contact conducts even when the net metadata is
                # wrong. Keep that physical graph for the open-shunt test.
                if distance<.0006:j(i,k)
                if a['net']==b['net']:
                    continue
                required=.1 if a['kind']==b['kind']=='pad' else .15
                if distance<required-.001:issues.append(dict(type='copper_clearance',layer=layer,a=a['id'],b=b['id'],nets=[a['net'],b['net']],actual=round(distance,5),required=required))
    jumper_checks=[]
    for component_id,name in sorted(nets.removable.items()):
        source_ids=[p['source_port_id'] for p in nets.of('source_port') if p.get('source_component_id')==component_id]
        if len(source_ids)!=2:
            issues.append(dict(type='removable_jumper_pin_count',name=name,source_port_ids=source_ids))
            continue
        a,b=source_ids
        present=bool(ports.get(a)) and bool(ports.get(b))
        bypassed=bool({f(i) for i in ports.get(a,[])} & {f(i) for i in ports.get(b,[])})
        physical_nets=[nets.source(a),nets.source(b)]
        if physical_nets[0]==physical_nets[1]:issues.append(dict(type='removable_jumper_net_short',name=name,source_port_ids=source_ids))
        if bypassed:issues.append(dict(type='removable_jumper_bypassed',name=name,source_port_ids=source_ids))
        jumper_checks.append(dict(name=name,source_port_ids=source_ids,physical_nets=physical_nets,isolated_when_open=present and not bypassed and physical_nets[0]!=physical_nets[1]))
    groups=collections.defaultdict(lambda:collections.defaultdict(list))
    for i,obj in enumerate(items):
        if obj['kind']=='pad' and obj.get('source_port_id'):groups[obj['net']][f(i)].append(obj['source_port_id'])
    unconnected=[]
    for net,g in groups.items():
        if len(g)>1:unconnected.append(dict(net=net,groups=[sorted(set(ii)) for ii in g.values()]))
    physical_groups=collections.defaultdict(list)
    for i,obj in enumerate(items):physical_groups[f(i)].append(obj)
    for group in physical_groups.values():
        if not any(obj.get('source_port_id') for obj in group) and any(obj['kind'] in ('trace','via') for obj in group):
            issues.append(dict(type='floating_copper',net=group[0]['net'],ids=sorted({obj['id'] for obj in group})))
    # Nonplated holes remove copper on every layer, including top/bottom tracks.
    for hole in nets.of('pcb_hole'):
        h=geom(hole,drill=True)
        for obj in items:
            d=h.distance(obj['geometry'])
            if d<.15-.001:issues.append(dict(type='nonplated_hole_clearance',hole=hole['pcb_hole_id'],id=obj['id'],layer=obj['layer'],actual=round(d,5),required=.15))
    drilled=nets.of('pcb_plated_hole')+nets.of('pcb_via')+nets.of('pcb_hole');dg=[geom(g,drill=True) for g in drilled];dt=STRtree(dg)
    min_drill=100
    for i,g in enumerate(dg):
        for hit in dt.query(g.buffer(.251)):
            k=int(hit)
            if k<=i:continue
            d=g.distance(dg[k]);required=.15
            min_drill=min(min_drill,d)
            if d<required-.001:issues.append(dict(type='drill_clearance',a=drilled[i].get(drilled[i]['type']+'_id'),b=drilled[k].get(drilled[k]['type']+'_id'),actual=d,required=required))
    board=nets.of('pcb_board')[0];boundary=Polygon([(p['x'],p['y']) for p in board['outline']])
    for a in items:
        if a['kind'] in ('trace','via') and not boundary.buffer(-.19).covers(a['geometry']):issues.append(dict(type='board_edge_clearance',id=a['id']))
    return dict(passed=not issues and not unconnected,copperItems=len(items),pcbTraces=len(nets.of('pcb_trace')),vias=len(nets.of('pcb_via')),unconnectedNets=unconnected,violations=issues,violationCounts=dict(collections.Counter(x['type'] for x in issues)),removableJumperChecks=jumper_checks)