@tachikomagundam/abathur 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli.js +4 -0
- package/dist/commands/readjudicate.js +28 -0
- package/dist/commands/retract.js +53 -0
- package/dist/commands/tombstone.js +28 -15
- package/dist/core/evolve/run-bench.js +7 -1
- package/dist/core/evolve/run-rows.js +14 -0
- package/dist/core/ledger.js +2 -0
- package/dist/core/promote.js +61 -46
- package/dist/core/readjudicate.js +247 -0
- package/dist/core/retract.js +166 -0
- package/dist/core/stats.js +7 -0
- package/dist/core/worktree.js +1 -1
- package/dist/genomes/toy-smoke/init.mjs +1 -1
- package/dist/test/bundle.test.js +1 -1
- package/dist/test/fixture-loop.test.js +1 -1
- package/dist/test/fixtures-self.js +1 -1
- package/dist/test/fixtures-wt.js +1 -1
- package/dist/test/friction.test.js +3 -3
- package/dist/test/graft.test.js +1 -1
- package/dist/test/historian-run-scenario.test.js +2 -2
- package/dist/test/promote.test.js +222 -5
- package/dist/test/readjudicate.test.js +212 -0
- package/dist/test/reflect.test.js +1 -1
- package/dist/test/repopath-seams.test.js +1 -0
- package/dist/test/seam-gates.test.js +6 -0
- package/dist/test/self-snapshot.test.js +2 -2
- package/dist/test/stats.test.js +17 -0
- package/docs/AGENTISM.md +58 -0
- package/docs/release/README.md +9 -0
- package/docs/release/TEMPLATE.md +10 -0
- package/docs/release/v0.2.7.md +7 -0
- package/docs/release/v0.2.8.md +8 -0
- package/graders/historian/judge-poststage.mjs +8 -3
- package/graders/pcb-agent/bench-approver.py +77 -0
- package/graders/pcb-agent/grader.mjs +130 -0
- package/graders/pcb-agent/mutate.sh +165 -0
- package/graders/pcb-agent/r20-bench.sh +27 -0
- package/graders/pcb-agent/reset-sandbox.sh +7 -0
- package/graders/pcb-agent/run-scenario-r20.sh +77 -0
- package/graders/pcb-agent/run-scenario.sh +94 -0
- package/graders/pcb-agent/seed/esp32s3-r12-BRIEF.txt +51 -0
- package/graders/pcb-agent/seed/spec.json +129 -0
- package/graders/pcb-agent/seed/tools/board_author_pcb.py +237 -0
- package/graders/pcb-agent/seed/tools/board_author_sch.py +118 -0
- package/graders/pcb-agent/seed/tools/board_finalize.sh +23 -0
- package/graders/pcb-agent/seed/tools/board_gate.py +69 -0
- package/graders/pcb-agent/seed/tools/board_gate_pcb.py +23 -0
- package/graders/pcb-agent/seed/tools/board_route.py +324 -0
- package/graders/pcb-agent/seed/tools/board_zone_pour.py +312 -0
- package/graders/pcb-agent/seed/tools/claims_lint.py +417 -0
- package/graders/pcb-agent/seed-r20-arena.sh +54 -0
- package/graders/pcb-agent/seed-sandbox.sh +13 -0
- package/package.json +1 -1
- package/plugin/abathur-command.md +3 -3
- package/plugin/abathur.ts +6 -6
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""r17 deterministic PCB author: netlist.json + spec footprints -> placed .kicad_pcb.
|
|
3
|
+
RM-56 doctrine: no bytes through a model. Embeds real .kicad_mod libs from the
|
|
4
|
+
KiCad AppDir, injects (net N "NAME") per pad (name-match, else positional match
|
|
5
|
+
with WARN), shelf placement by footprint bbox + clearance, Edge.Cuts rect.
|
|
6
|
+
DRC gate (board_gate_pcb.py) evaluates placement-class errors; unconnected_items
|
|
7
|
+
are expected pre-routing and reported separately."""
|
|
8
|
+
import json, re, os, sys
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
|
|
11
|
+
ALIASES = {}
|
|
12
|
+
_ali = os.path.join(os.path.dirname(os.path.abspath(__file__)), "board_author_aliases.json")
|
|
13
|
+
if os.path.exists(_ali):
|
|
14
|
+
ALIASES = {tuple(k.split("|")): v for k, v in json.load(open(_ali)).items()}
|
|
15
|
+
|
|
16
|
+
FP_ROOT = "/home/lab/.local/AppDir/share/kicad/footprints"
|
|
17
|
+
PLACE_MARGIN = 3.0 # mm between footprint bboxes (courtyard-ish)
|
|
18
|
+
GRID = 0.1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def snap(v):
|
|
22
|
+
return round(v / GRID) * GRID
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def forms(s):
|
|
26
|
+
"""top-level s-expr blocks inside a ( ... ) wrapper string."""
|
|
27
|
+
out = []
|
|
28
|
+
i = 0
|
|
29
|
+
n = len(s)
|
|
30
|
+
while i < n:
|
|
31
|
+
if s[i] == "(":
|
|
32
|
+
depth = 0
|
|
33
|
+
j = i
|
|
34
|
+
instr = False
|
|
35
|
+
while j < n:
|
|
36
|
+
ch = s[j]
|
|
37
|
+
if ch == '"' and (j == 0 or s[j - 1] != "\\"):
|
|
38
|
+
instr = not instr
|
|
39
|
+
elif not instr:
|
|
40
|
+
if ch == "(":
|
|
41
|
+
depth += 1
|
|
42
|
+
elif ch == ")":
|
|
43
|
+
depth -= 1
|
|
44
|
+
if depth == 0:
|
|
45
|
+
break
|
|
46
|
+
j += 1
|
|
47
|
+
out.append(s[i:j + 1])
|
|
48
|
+
i = j + 1
|
|
49
|
+
else:
|
|
50
|
+
i += 1
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def sub_block(s, token):
|
|
55
|
+
"""return (block, start, end) of first block starting with token, else None"""
|
|
56
|
+
for b in forms(s):
|
|
57
|
+
if b.startswith(f"({token}"):
|
|
58
|
+
return b
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bbox_of(km):
|
|
63
|
+
"""rough extent from pads + courtyard graphic lines/rects."""
|
|
64
|
+
xs, ys = [], []
|
|
65
|
+
for m in re.finditer(r'\(pad[^()]*?\(at ([-\d.]+)(?: ([-\d.]+))?\)(?:\s*\(size ([-\d.]+) ([-\d.]+))?', km):
|
|
66
|
+
x, y = float(m.group(1)), float(m.group(2) or 0)
|
|
67
|
+
w, h = (float(m.group(3)), float(m.group(4))) if m.group(3) else (1.0, 1.0)
|
|
68
|
+
xs += [x - w / 2, x + w / 2]
|
|
69
|
+
ys += [y - h / 2, y + h / 2]
|
|
70
|
+
for m in re.finditer(r'\((?:xy|start|end|at) ([-\d.]+) ([-\d.]+)', km):
|
|
71
|
+
xs.append(float(m.group(1)))
|
|
72
|
+
ys.append(float(m.group(2)))
|
|
73
|
+
if not xs:
|
|
74
|
+
return -1, -1, 2, 2
|
|
75
|
+
pad = 0.5
|
|
76
|
+
return min(xs) - pad, min(ys) - pad, max(xs) + pad, max(ys) + pad
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def resolve_footprint(spec_fp):
|
|
80
|
+
lib, name = spec_fp.split(":", 1)
|
|
81
|
+
p = os.path.join(FP_ROOT, f"{lib}.pretty", f"{name}.kicad_mod")
|
|
82
|
+
return p if os.path.exists(p) else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def assign_nets(netlist):
|
|
86
|
+
nets = sorted({n for c in netlist["components"] for n in c["pins"].values()})
|
|
87
|
+
code = {n: i + 1 for i, n in enumerate(nets)}
|
|
88
|
+
return nets, code
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def author(netlist_path, out_path, rot_by_ref=None):
|
|
92
|
+
d = json.load(open(netlist_path))
|
|
93
|
+
comps = d["components"]
|
|
94
|
+
nets, ncode = assign_nets(d)
|
|
95
|
+
uuidn = [0]
|
|
96
|
+
|
|
97
|
+
def U():
|
|
98
|
+
uuidn[0] += 1
|
|
99
|
+
x = f"{uuidn[0]:032x}"
|
|
100
|
+
return f"{x[:8]}-{x[8:12]}-{x[12:16]}-{x[16:20]}-{x[20:32]}"
|
|
101
|
+
|
|
102
|
+
# ---- layout pass: shelf by bbox heights
|
|
103
|
+
items = []
|
|
104
|
+
warn = []
|
|
105
|
+
for c in sorted(comps, key=lambda c: (c["ref"][0], int(re.sub(r"\D", "", c["ref"]) or 0))):
|
|
106
|
+
fpp = resolve_footprint(c["footprint"])
|
|
107
|
+
if not fpp:
|
|
108
|
+
warn.append(f"NO-FOOTPRINT {c['ref']} {c['footprint']}")
|
|
109
|
+
continue
|
|
110
|
+
km = open(fpp).read()
|
|
111
|
+
x0, y0, x1, y1 = bbox_of(km)
|
|
112
|
+
items.append([c, km, snap(x1 - x0) + PLACE_MARGIN, snap(y1 - y0) + PLACE_MARGIN, (x0, y0)])
|
|
113
|
+
COL_W = 120.0
|
|
114
|
+
cx, cy, rowh = 10.0, 10.0, 0.0
|
|
115
|
+
placed = []
|
|
116
|
+
for it in items:
|
|
117
|
+
if cx + it[2] > COL_W:
|
|
118
|
+
cx = 10.0
|
|
119
|
+
cy += rowh
|
|
120
|
+
rowh = 0.0
|
|
121
|
+
placed.append((it, snap(cx), snap(cy)))
|
|
122
|
+
cx += it[2]
|
|
123
|
+
rowh = max(rowh, it[3])
|
|
124
|
+
board_w, board_h = snap(COL_W + 20), snap(cy + rowh + 20)
|
|
125
|
+
|
|
126
|
+
# ---- emit
|
|
127
|
+
L = ['(kicad_pcb (version 20260206) (generator "board_author_pcb") (generator_version "10.0")',
|
|
128
|
+
' (general (thickness 1.6) (legacy_teardrops no))',
|
|
129
|
+
' (paper "A3")',
|
|
130
|
+
' (layers (0 "F.Cu" signal) (31 "B.Cu" signal) (32 "B.Adhes" user "B.Adhesive") '
|
|
131
|
+
'(33 "F.Adhes" user "F.Adhesive") (34 "B.Paste" user) (35 "F.Paste" user) '
|
|
132
|
+
'(36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen") '
|
|
133
|
+
'(38 "B.Mask" user) (39 "F.Mask" user) (40 "Dwgs.User" user "User.Drawings") '
|
|
134
|
+
'(44 "Edge.Cuts" user) (45 "Margin" user "BoardOutline") (46 "B.CrtYd" user "B.Courtyard") '
|
|
135
|
+
'(47 "F.CrtYd" user "F.Courtyard") (48 "B.Fab" user) (49 "F.Fab" user) (50 "User.1" user))',
|
|
136
|
+
' (setup (pad_to_mask_clearance 0))',
|
|
137
|
+
' (net_class "Default" "" (clearance 0.2) (trace_width 0.25) (via_dia 0.6) (via_drill 0.3) (uvia_dia 0.3) (uvia_drill 0.1)'
|
|
138
|
+
+ " " + "".join(f'(add_net "{esc(n)}")' for n in sorted(nets)) + ')']
|
|
139
|
+
L.append(' (net 0 "")')
|
|
140
|
+
for n in nets:
|
|
141
|
+
L.append(f' (net {ncode[n]} "{n.replace(chr(92), chr(92)*2).replace(chr(34), chr(92)+chr(34))}")')
|
|
142
|
+
# Edge.Cuts rectangle
|
|
143
|
+
pts = [(5, 5), (board_w, 5), (board_w, board_h - 5), (5, board_h - 5), (5, 5)]
|
|
144
|
+
for i in range(4):
|
|
145
|
+
L.append(f' (gr_line (start {pts[i][0]} {pts[i][1]}) (end {pts[i+1][0]} {pts[i+1][1]})'
|
|
146
|
+
f' (stroke (width 0.1) (type solid)) (layer "Edge.Cuts") (uuid "{U()}"))')
|
|
147
|
+
for (it, px, py) in placed:
|
|
148
|
+
c, km, _, _, (x0, y0) = it
|
|
149
|
+
ref = c["ref"]
|
|
150
|
+
pins = c["pins"]
|
|
151
|
+
# pad name -> net number string; match footprint pad names
|
|
152
|
+
fp_padnames = re.findall(r'\(pad "([^"]*)"', km) # file order; '' = np/mount hole
|
|
153
|
+
named = [n for n in fp_padnames if n]
|
|
154
|
+
mapping = {}
|
|
155
|
+
alias = ALIASES.get((ref, c["footprint"]))
|
|
156
|
+
if alias:
|
|
157
|
+
mapping = dict(alias)
|
|
158
|
+
elif set(pins) <= set(named):
|
|
159
|
+
pass # identity match on covered pins; extra pads (shell/np) stay net 0
|
|
160
|
+
elif len(named) == len(pins):
|
|
161
|
+
order_pins = sorted(pins, key=lambda pin: int(pin) if pin.isdigit() else 0)
|
|
162
|
+
for fpn, spp in zip(named, order_pins):
|
|
163
|
+
mapping.setdefault(fpn, spp)
|
|
164
|
+
warn.append(f"POSITIONAL-MAP {ref} {c['footprint']}: {len(named)} pads zipped to spec pins — NO authority table, verify datasheet")
|
|
165
|
+
else:
|
|
166
|
+
warn.append(f"NO-AUTHORITY-MAP {ref} {c['footprint']}: pads {sorted(set(named))[:8]} vs spec pins {sorted(pins)[:8]} -> design-data GAP, net 0 (refusing to guess)")
|
|
167
|
+
# origin so that bbox min lands at px,py
|
|
168
|
+
ox, oy = snap(px - x0), snap(py - y0)
|
|
169
|
+
head = f' (footprint "{esc(c["footprint"])}" (layer "F.Cu") (uuid "{U()}") (at {ox} {oy})'
|
|
170
|
+
head += (f' (property "Reference" "{esc(ref)}" (at 0 -1 0) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))'
|
|
171
|
+
f' (property "Value" "{esc(c.get("mpn", ref))}" (at 0 1 0) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))')
|
|
172
|
+
body = km[km.index("(attr"):] if "(attr" in km else km[km.rindex(")"):]
|
|
173
|
+
# inject nets into pad blocks
|
|
174
|
+
out_km = []
|
|
175
|
+
pos = 0
|
|
176
|
+
for pb in forms(body):
|
|
177
|
+
m = re.match(r'\(pad "([^"]+)"', pb)
|
|
178
|
+
s, e = body.find(pb, pos), body.find(pb, pos) + len(pb)
|
|
179
|
+
if m:
|
|
180
|
+
padname = m.group(1)
|
|
181
|
+
pin = None if padname == "" else mapping.get(padname, padname if padname in pins else None)
|
|
182
|
+
if pin is not None:
|
|
183
|
+
net = pins[pin]
|
|
184
|
+
ins = f' (net {ncode[net]} "{esc(net)}")'
|
|
185
|
+
pb = pb[:-1] + ins + ")"
|
|
186
|
+
pos = e
|
|
187
|
+
out_km.append((s, pb))
|
|
188
|
+
rebuilt = body
|
|
189
|
+
for s, pb in reversed(out_km):
|
|
190
|
+
e = s + None if False else s
|
|
191
|
+
# simpler: replace each original pad block string once
|
|
192
|
+
# (do textual replacement with count=1 on the ORIGINAL blocks)
|
|
193
|
+
for orig_pb in [b for b in forms(body) if b.startswith('(pad ')]:
|
|
194
|
+
new_pb = next(p for (s, p) in out_km if s == body.find(orig_pb))
|
|
195
|
+
rebuilt = rebuilt.replace(orig_pb, new_pb, 1)
|
|
196
|
+
rebuilt = rebuilt[:rebuilt.rindex(")")] # strip trailing close of fp file
|
|
197
|
+
L.append(head + "\n" + rebuilt + "\n )")
|
|
198
|
+
L.append(')\n')
|
|
199
|
+
open(out_path, "w").write("\n".join(x for x in L if x) + "\n")
|
|
200
|
+
pro = out_path[:-len(".kicad_pcb")] + ".kicad_pro"
|
|
201
|
+
import json as _j
|
|
202
|
+
_j.dump({
|
|
203
|
+
"board": {"design_settings": {
|
|
204
|
+
"defaults": {"board_outline_line_width": 0.1, "copper_line_width": 0.2,
|
|
205
|
+
"solder_mask_clearance": 0.0, "solder_mask_min_width": 0.0,
|
|
206
|
+
"allow_soldermask_bridges_in_footprints": True},
|
|
207
|
+
"drc_exclusions": [],
|
|
208
|
+
"rules": {"min_hole_to_hole": 0.15, "min_through_hole_diameter": 0.15,
|
|
209
|
+
"min_clearance": 0.2, "min_connection": 0.0,
|
|
210
|
+
"min_copper_edge_clearance": 0.1, "min_hole_clearance": 0.15,
|
|
211
|
+
"min_silk_clearance": 0.0, "min_text_height": 0.8,
|
|
212
|
+
"min_text_thickness": 0.12, "min_track_width": 0.15,
|
|
213
|
+
"min_via_annular_width": 0.1, "min_via_diameter": 0.45,
|
|
214
|
+
"solder_mask_to_copper_clearance": 0.0, "use_height_for_length_calcs": True}}},
|
|
215
|
+
"libraries": {"fp_files": []},
|
|
216
|
+
"meta": {"filename": pro.split("/")[-1], "version": 3},
|
|
217
|
+
"net_settings": {"classes": [{"name": "Default", "clearance": 0.2,
|
|
218
|
+
"trace_width": 0.25, "via_dia": 0.6, "via_drill": 0.2, "uvia_dia": 0.3,
|
|
219
|
+
"uvia_drill": 0.1, "allow_soldermask_bridges_in_footprints": True}],
|
|
220
|
+
"meta": {"version": 3}},
|
|
221
|
+
"pcbnew": {"last_paths": {}},
|
|
222
|
+
"sheets": [], "text_variables": {}},
|
|
223
|
+
open(pro, "w"), indent=1)
|
|
224
|
+
return nets, warn, board_w, board_h
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def esc(s):
|
|
228
|
+
return str(s).replace("\\", "\\\\").replace('"', '\\"')
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
nl = sys.argv[1]
|
|
233
|
+
out = sys.argv[2]
|
|
234
|
+
nets, warn, w, h = author(nl, out)
|
|
235
|
+
print(f"authored {out}: nets={len(nets)} board={w}x{h}mm")
|
|
236
|
+
for x in warn:
|
|
237
|
+
print("WARN", x)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""r17 deterministic schematic author (RM-56: netlist bytes NEVER through a model).
|
|
3
|
+
Topology: NO wires. Every pin gets one global label anchored EXACTLY on the pin
|
|
4
|
+
end, rotation 180 (text left, away from body). Anchor == pin end by construction;
|
|
5
|
+
no dangling labels (r13 D1 defect class), no collinear merges. Grid 0.635 mm.
|
|
6
|
+
Usage: board_author_sch.py netlist.json -o out.kicad_sch"""
|
|
7
|
+
import json, re, argparse
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
|
|
10
|
+
G = 0.635
|
|
11
|
+
COLPITCH = 99.06 # column pitch (41-pin U1 fans 52mm left of anchor col)
|
|
12
|
+
ROWPITCH = 76.2 # row pitch; keep > 2.54 * maxpins/2 stack of neighbours
|
|
13
|
+
PINPITCH = 2.54 # pin vertical spacing (label height 1.27 leaves 1.27 gap)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def snap(v):
|
|
17
|
+
return round(v / G) * G
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def esc(s):
|
|
21
|
+
return str(s).replace("\\", "\\\\").replace('"', '\\"')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Uuid:
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.n = 0
|
|
27
|
+
|
|
28
|
+
def __call__(self):
|
|
29
|
+
self.n += 1
|
|
30
|
+
x = f"{self.n:032x}"
|
|
31
|
+
return f"{x[:8]}-{x[8:12]}-{x[12:16]}-{x[16:20]}-{x[20:32]}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pin_order(pins):
|
|
35
|
+
return sorted(pins, key=lambda p: int(p) if p.isdigit() else 0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build(netlist_path, out_path):
|
|
39
|
+
d = json.load(open(netlist_path))
|
|
40
|
+
comps = d["components"]
|
|
41
|
+
u = Uuid()
|
|
42
|
+
order = sorted(comps, key=lambda c: (c["ref"][0], int(re.sub(r"\D", "", c["ref"]) or 0)))
|
|
43
|
+
pos = {}
|
|
44
|
+
for slot, c in enumerate(order):
|
|
45
|
+
row, col = divmod(slot, 4)
|
|
46
|
+
pos[c["ref"]] = (snap(60 + col * COLPITCH), snap(60 + row * ROWPITCH))
|
|
47
|
+
# symbol defs: single-sided fan-left, PINPITCH spacing
|
|
48
|
+
prefix_pins = {}
|
|
49
|
+
pinset = {}
|
|
50
|
+
for c in order:
|
|
51
|
+
ref = c["ref"]
|
|
52
|
+
ps = pin_order(c["pins"])
|
|
53
|
+
key = "SR_" + re.sub(r"\d+$", "", ref)
|
|
54
|
+
if key in prefix_pins and prefix_pins[key] != ps:
|
|
55
|
+
key = "SR_" + ref
|
|
56
|
+
prefix_pins.setdefault(key, ps)
|
|
57
|
+
c["_key"] = key
|
|
58
|
+
sx, sy = pos[ref]
|
|
59
|
+
n = len(ps)
|
|
60
|
+
# world y-down vs lib-symbol-local y-UP: the def emits oy=+off (up) per
|
|
61
|
+
# pin slot i; the label anchor in SHEET coords must therefore SUBTRACT.
|
|
62
|
+
pinset[ref] = [(p, snap(sx - 5.08), snap(sy - ((n - 1) / 2 - i) * PINPITCH))
|
|
63
|
+
for i, p in enumerate(ps)]
|
|
64
|
+
labels = []
|
|
65
|
+
for c in order:
|
|
66
|
+
for pnum, net in c["pins"].items():
|
|
67
|
+
px, py = next((ax, ay) for (pp, ax, ay) in pinset[c["ref"]] if pp == pnum)
|
|
68
|
+
labels.append((net, px, py))
|
|
69
|
+
L = ['(kicad_sch (version 20250114) (generator "board_author_sch") (generator_version "10.0.5")',
|
|
70
|
+
f' (uuid "{u()}")', ' (paper "A3")', ' (lib_symbols']
|
|
71
|
+
for key, ps in sorted(prefix_pins.items()):
|
|
72
|
+
n = len(ps)
|
|
73
|
+
L.append(f' (symbol "solar_ray:{esc(key)}"')
|
|
74
|
+
L.append(' (pin_numbers (hide yes))')
|
|
75
|
+
L.append(' (pin_names (offset 1.016))')
|
|
76
|
+
L.append(' (exclude_from_sim no) (in_bom yes) (on_board yes)')
|
|
77
|
+
L.append(' (property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27))))')
|
|
78
|
+
L.append(' (property "Value" "V" (at 0 0 0) (effects (font (size 1.27 1.27))))')
|
|
79
|
+
L.append(' (property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))')
|
|
80
|
+
L.append(' (property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))')
|
|
81
|
+
L.append(f' (symbol "{esc(key)}_1_1"')
|
|
82
|
+
for i, p in enumerate(ps):
|
|
83
|
+
oy = snap(((n - 1) / 2 - i) * PINPITCH)
|
|
84
|
+
L.append(f' (pin passive line (at {-5.08} {oy} 0) (length 5.08)'
|
|
85
|
+
f' (name "~" (effects (font (size 1.016 1.016))))'
|
|
86
|
+
f' (number "{esc(p)}" (effects (font (size 1.016 1.016)))))')
|
|
87
|
+
L.append(' )')
|
|
88
|
+
L.append(' (embedded_fonts no)')
|
|
89
|
+
L.append(' )')
|
|
90
|
+
L.append(' )')
|
|
91
|
+
for c in order:
|
|
92
|
+
ref = c["ref"]
|
|
93
|
+
sx, sy = pos[ref]
|
|
94
|
+
L.append(f' (symbol (lib_id "solar_ray:{esc(c["_key"])}") (at {sx} {sy} 0) (unit 1)')
|
|
95
|
+
L.append(' (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (fields_autoplaced no)')
|
|
96
|
+
L.append(f' (uuid "{u()}")')
|
|
97
|
+
for k, v, xo in (("Reference", ref, -2.54), ("Value", c.get("mpn", ref), 2.54),
|
|
98
|
+
("Footprint", c.get("footprint", ""), 0.0), ("Datasheet", "", 0.0)):
|
|
99
|
+
L.append(f' (property "{k}" "{esc(v)}" (at {snap(sx+xo)} {snap(sy+5.08)} 0)'
|
|
100
|
+
f' (effects (font (size 1.27 1.27)) (hide yes)))')
|
|
101
|
+
for (p, _, _) in pinset[ref]:
|
|
102
|
+
L.append(f' (pin "{esc(p)}" (uuid "{u()}"))')
|
|
103
|
+
L.append(' )')
|
|
104
|
+
for net, x, y in labels:
|
|
105
|
+
L.append(f' (global_label "{esc(net)}" (shape passive) (at {x} {y} 180)'
|
|
106
|
+
f' (effects (font (size 1.27 1.27)) (justify left)) (uuid "{u()}"))')
|
|
107
|
+
L += [' (sheet_instances', ' (path "/" (page "1"))', ' )', ' (embedded_fonts no)', ')']
|
|
108
|
+
open(out_path, "w").write("\n".join(L) + "\n")
|
|
109
|
+
return len(order), len(labels)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
ap = argparse.ArgumentParser()
|
|
114
|
+
ap.add_argument("netlist")
|
|
115
|
+
ap.add_argument("-o", "--out", required=True)
|
|
116
|
+
a = ap.parse_args()
|
|
117
|
+
s, l = build(a.netlist, a.out)
|
|
118
|
+
print(f"emitted {a.out}: symbols={s} labels={l}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# r17 finalization pipeline: routed board -> pour -> native refill -> DRC gate -> fab exports
|
|
3
|
+
set -e
|
|
4
|
+
IN="${1:?routed kicad_pcb}"
|
|
5
|
+
OUT="${2:?output board pcb}"
|
|
6
|
+
RUN="${3:-evolution-round-17}"
|
|
7
|
+
cd /home/lab/workspace/pcb-control
|
|
8
|
+
python3 tools/board_zone_pour.py "$IN" "$OUT"
|
|
9
|
+
cp /tmp/sr-c4.kicad_pro "$(dirname "$OUT")/$(basename "$OUT" .kicad_pcb).kicad_pro"
|
|
10
|
+
D=/home/lab/workspace/pcb-control/reports/recovery/$RUN
|
|
11
|
+
kicad-cli pcb drc "$OUT" --refill-zones --save-board --format json --severity-error --output "$D/$(basename "$OUT").drc-refill.json" >/dev/null 2>&1
|
|
12
|
+
kicad-cli pcb drc "$OUT" --format json --severity-error --output "$D/$(basename "$OUT").drc-final.json" >/dev/null 2>&1
|
|
13
|
+
python3 - "$D/$(basename "$OUT").drc-final.json" <<'EOF'
|
|
14
|
+
import json, sys, collections
|
|
15
|
+
d = json.load(open(sys.argv[1]))
|
|
16
|
+
v = d.get('violations', []); u = d.get('unconnected_items', [])
|
|
17
|
+
print(f"GATE: errors={len(v)} {dict(collections.Counter(x.get('type','?') for x in v))} unconnected={len(u)}")
|
|
18
|
+
EOF
|
|
19
|
+
mkdir -p "$D/fab-out"
|
|
20
|
+
kicad-cli pcb export gerbers "$OUT" -o "$D/fab-out/gerbers.zip" >/dev/null
|
|
21
|
+
kicad-cli pcb export drill "$OUT" -o "$D/fab-out/" >/dev/null
|
|
22
|
+
kicad-cli pcb export pos "$OUT" -o "$D/fab-out/pos.pos" --units mm >/dev/null
|
|
23
|
+
echo "fab exports -> $D/fab-out/"; ls "$D/fab-out/" | head
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""r17 board gate: ERC-clean + netlist-exact roundtrip for an authored sheet.
|
|
3
|
+
Exit 0 only when: ERC 0 errors, 0 unconnected, and EVERY spec net appears in the
|
|
4
|
+
export with EXACTLY its spec membership (and no extra nets). Closes the r13 D1
|
|
5
|
+
defect class deterministically (no model in the byte path)."""
|
|
6
|
+
import json, re, subprocess, sys, tempfile, os
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
def main(sch, netlist_json):
|
|
10
|
+
spec = json.load(open(netlist_json))
|
|
11
|
+
got = defaultdict(set)
|
|
12
|
+
for c in spec["components"]:
|
|
13
|
+
for p, net in c["pins"].items():
|
|
14
|
+
got[net].add(f"{c['ref']}.{p}")
|
|
15
|
+
with tempfile.TemporaryDirectory() as td:
|
|
16
|
+
erc = os.path.join(td, "erc.json"); net = os.path.join(td, "netlist.net")
|
|
17
|
+
r1 = subprocess.run(["kicad-cli", "sch", "erc", sch, "--format", "json",
|
|
18
|
+
"--severity-error", "--output", erc], capture_output=True, text=True)
|
|
19
|
+
r2 = subprocess.run(["kicad-cli", "sch", "export", "netlist", sch, "-o", net],
|
|
20
|
+
capture_output=True, text=True)
|
|
21
|
+
if r1.returncode or not os.path.exists(erc):
|
|
22
|
+
print(f"FAIL ERC_RUN rc={r1.returncode} {r1.stderr[:200]}"); return 1
|
|
23
|
+
if r2.returncode or not os.path.exists(net):
|
|
24
|
+
print(f"FAIL EXPORT rc={r2.returncode} {r2.stderr[:200]}"); return 1
|
|
25
|
+
d = json.load(open(erc))
|
|
26
|
+
sh = d["sheets"][0].get("erc", {})
|
|
27
|
+
v, u = sh.get("violations", []), sh.get("unconnected", [])
|
|
28
|
+
if v or u:
|
|
29
|
+
print(f"FAIL ERC violations={len(v)} unconnected={len(u)}")
|
|
30
|
+
for x in v[:6]: print(" ", json.dumps(x)[:180])
|
|
31
|
+
return 1
|
|
32
|
+
txt = open(net).read()
|
|
33
|
+
# parenthesis-balanced extraction of each (net ...) block (regex cannot
|
|
34
|
+
# nest; truncation risk seen in dev round — never parse s-exprs with a
|
|
35
|
+
# lazy match across closers).
|
|
36
|
+
exp = defaultdict(set)
|
|
37
|
+
i = 0
|
|
38
|
+
while True:
|
|
39
|
+
i = txt.find("(net\n", i)
|
|
40
|
+
if i < 0: break
|
|
41
|
+
depth = 0; j = i; instr = False
|
|
42
|
+
while j < len(txt):
|
|
43
|
+
ch = txt[j]
|
|
44
|
+
if ch == '"' and txt[j-1] != "\\": instr = not instr
|
|
45
|
+
elif not instr:
|
|
46
|
+
if ch == "(": depth += 1
|
|
47
|
+
elif ch == ")":
|
|
48
|
+
depth -= 1
|
|
49
|
+
if depth == 0: break
|
|
50
|
+
j += 1
|
|
51
|
+
block = txt[i:j+1]
|
|
52
|
+
m = re.search(r'\(name "([^"]+)"\)', block)
|
|
53
|
+
if m:
|
|
54
|
+
for ref, pin in re.findall(r'ref "([^"]+)"\)\s*\(\s*pin "([^"]+)"', block):
|
|
55
|
+
exp[m.group(1)].add(f"{ref}.{pin}")
|
|
56
|
+
i = j + 1
|
|
57
|
+
miss = {n for n in got if exp.get(n) != got[n]}
|
|
58
|
+
extra = set(exp) - set(got)
|
|
59
|
+
if miss or extra:
|
|
60
|
+
print(f"FAIL ROUNDTRIP {len(got)-len(miss)}/{len(got)} exact extra_nets={sorted(extra)[:5]}")
|
|
61
|
+
for n in sorted(miss)[:6]:
|
|
62
|
+
print(" DIFF", n, "spec-only", sorted(got[n]-exp.get(n, set()))[:3],
|
|
63
|
+
"exp-only", sorted(exp.get(n, set())-got[n])[:3])
|
|
64
|
+
return 1
|
|
65
|
+
print(f"PASS erc=0/0 nets={len(got)}/{len(got)} exact (members {sum(len(x) for x in got.values())} pins)")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
sys.exit(main(sys.argv[1], sys.argv[2]))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""r17 pcb placement gate: kicad-cli DRC must report 0 error-level violations.
|
|
3
|
+
unconnected_items are EXPECTED pre-routing (counted, not failing)."""
|
|
4
|
+
import json, subprocess, sys, os, tempfile
|
|
5
|
+
board = sys.argv[1]
|
|
6
|
+
assert board.endswith(".kicad_pcb")
|
|
7
|
+
pro = board[:-10] + ".kicad_pro"
|
|
8
|
+
if not os.path.exists(pro):
|
|
9
|
+
print("FAIL no sibling .kicad_pro (design constraints live there)"); sys.exit(1)
|
|
10
|
+
with tempfile.TemporaryDirectory() as td:
|
|
11
|
+
out = os.path.join(td, "drc.json")
|
|
12
|
+
r = subprocess.run(["kicad-cli", "pcb", "drc", board, "--format", "json",
|
|
13
|
+
"--severity-error", "--output", out], capture_output=True, text=True)
|
|
14
|
+
if r.returncode or not os.path.exists(out):
|
|
15
|
+
print(f"FAIL load/drc rc={r.returncode} {r.stderr[:200]}"); sys.exit(1)
|
|
16
|
+
d = json.load(open(out))
|
|
17
|
+
v = d.get("violations", [])
|
|
18
|
+
u = d.get("unconnected_items", [])
|
|
19
|
+
if v:
|
|
20
|
+
print(f"FAIL DRC err={len(v)}")
|
|
21
|
+
for x in v[:8]: print(" ", x.get("description", "")[:100])
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
print(f"PASS placement-DRC 0 errors; pending-route unconnected={len(u)}")
|