@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,324 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""r17 deterministic 2-layer router (RM-56 doctrine; scripted A* lane).
|
|
3
|
+
Reads placed .kicad_pcb (board_author_pcb output), routes a nearest-neighbour
|
|
4
|
+
spanning tree per net on F.Cu/B.Cu, appends (segment)/(via), writes routed board.
|
|
5
|
+
Grid 0.1mm; octile A*; per-layer flat occupancy dict (cell -> set(nets)).
|
|
6
|
+
A cell is blocked for net N if occupied by nets other than N or 0.
|
|
7
|
+
Gates afterwards: kicad DRC 0 errors + unconnected_items == 0 (board_gate_routed)."""
|
|
8
|
+
import os
|
|
9
|
+
import re, math, heapq, sys
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
|
|
12
|
+
GRID = float(os.environ.get('BR_GRID', '0.05'))
|
|
13
|
+
CLEAR = 0.26
|
|
14
|
+
POWER = {"VBUS", "5V_SERVO", "5V_LOGIC", "3V3", "GND", "SW_NODE", "+5V_IN"}
|
|
15
|
+
W_POWER, W_SIG = 0.4, 0.25
|
|
16
|
+
VIA_COST = 40
|
|
17
|
+
MAX_PUSH = int(os.environ.get('BR_MAX_PUSH', '900000'))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def footprint_blocks(txt):
|
|
21
|
+
return sub_blocks(txt, "footprint")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def sub_blocks(s, token):
|
|
25
|
+
"""full (token ...) blocks, balanced-paren + string-aware scan."""
|
|
26
|
+
out = []
|
|
27
|
+
i = 0
|
|
28
|
+
pat = "(" + token + " "
|
|
29
|
+
while True:
|
|
30
|
+
i = s.find(pat, i)
|
|
31
|
+
if i < 0:
|
|
32
|
+
return out
|
|
33
|
+
depth = 0
|
|
34
|
+
j = i
|
|
35
|
+
instr = False
|
|
36
|
+
while j < len(s):
|
|
37
|
+
ch = s[j]
|
|
38
|
+
if ch == '"' and (j == 0 or s[j - 1] != "\\"):
|
|
39
|
+
instr = not instr
|
|
40
|
+
elif not instr:
|
|
41
|
+
if ch == "(":
|
|
42
|
+
depth += 1
|
|
43
|
+
elif ch == ")":
|
|
44
|
+
depth -= 1
|
|
45
|
+
if depth == 0:
|
|
46
|
+
break
|
|
47
|
+
j += 1
|
|
48
|
+
out.append(s[i:j + 1])
|
|
49
|
+
i = j + 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pads_of(txt):
|
|
53
|
+
pads = []
|
|
54
|
+
for block in footprint_blocks(txt):
|
|
55
|
+
fm = re.search(r'\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)', block)
|
|
56
|
+
fx, fy, frot = (float(fm.group(1)), float(fm.group(2)), float(fm.group(3) or 0)) if fm else (0, 0, 0)
|
|
57
|
+
refm = re.search(r'\(property "Reference" "([^"]+)"', block)
|
|
58
|
+
ref = refm.group(1) if refm else "?"
|
|
59
|
+
for pb in sub_blocks(block, "pad"):
|
|
60
|
+
nm = re.search(r'\(net (\d+) "((?:[^"\\]|\\.)*)"\)', pb)
|
|
61
|
+
if not nm:
|
|
62
|
+
continue
|
|
63
|
+
pm2 = re.match(r'\(pad "([^"]*)"', pb)
|
|
64
|
+
at = re.search(r'\(at ([-\d.]+)(?: ([-\d.]+))?(?: ([-\d.]+))?\)', pb)
|
|
65
|
+
sz = re.search(r'\(size ([-\d.]+) ([-\d.]+)\)', pb)
|
|
66
|
+
px, py = (float(at.group(1)), float(at.group(2) or 0)) if at else (0, 0)
|
|
67
|
+
pr = float(at.group(3) or 0) if at else 0.0
|
|
68
|
+
w, h = (float(sz.group(1)), float(sz.group(2))) if sz else (1.0, 1.0)
|
|
69
|
+
a = math.radians(pr + frot)
|
|
70
|
+
gx = fx + px * math.cos(a) - py * math.sin(a)
|
|
71
|
+
gy = fy + px * math.sin(a) + py * math.cos(a)
|
|
72
|
+
pads.append(dict(ref=ref, pad=pm2.group(1) if pm2 else "?", net=int(nm.group(1)),
|
|
73
|
+
netname=nm.group(2), x=gx, y=gy, w=w, h=h))
|
|
74
|
+
return pads
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Occ:
|
|
78
|
+
def __init__(self):
|
|
79
|
+
self.m = [{}, {}]
|
|
80
|
+
|
|
81
|
+
def add_rect(self, x0, y0, x1, y1, l, net):
|
|
82
|
+
for cx in range(int(math.floor(x0 / GRID)), int(math.ceil(x1 / GRID)) + 1):
|
|
83
|
+
row = self.m[l]
|
|
84
|
+
for cy in range(int(math.floor(y0 / GRID)), int(math.ceil(y1 / GRID)) + 1):
|
|
85
|
+
s = row.get((cx, cy))
|
|
86
|
+
if s is None:
|
|
87
|
+
row[(cx, cy)] = {net}
|
|
88
|
+
else:
|
|
89
|
+
s.add(net)
|
|
90
|
+
|
|
91
|
+
def passable(self, l, cx, cy, net):
|
|
92
|
+
s = self.m[l].get((cx, cy))
|
|
93
|
+
return s is None or s == {0} or net in s
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def build_occ(pads):
|
|
97
|
+
occ = Occ()
|
|
98
|
+
for p in pads:
|
|
99
|
+
if str(p.get("num", p.get("pad", "1"))) == "":
|
|
100
|
+
# NPTH retention holes/screws: universal obstacle (no net may own
|
|
101
|
+
# copper in a hole's clearance annulus), sentinel -2 matches nothing.
|
|
102
|
+
hw, hh = p["w"] / 2 + 0.35, p["h"] / 2 + 0.35
|
|
103
|
+
for l in (0, 1):
|
|
104
|
+
occ.add_rect(p["x"] - hw, p["y"] - hh, p["x"] + hw, p["y"] + hh, l, -2)
|
|
105
|
+
continue
|
|
106
|
+
hw, hh = p["w"] / 2 + CLEAR, p["h"] / 2 + CLEAR
|
|
107
|
+
# cross-layer: KiCad enforces Cu-to-Cu clearance between tracks and pads
|
|
108
|
+
# on the OTHER copper layer too -> register every pad ring on BOTH layers.
|
|
109
|
+
for l in (0, 1):
|
|
110
|
+
occ.add_rect(p["x"] - hw, p["y"] - hh, p["x"] + hw, p["y"] + hh, l, p["net"])
|
|
111
|
+
# unmark pad interiors of the net's own footprint pads as free-to-stand target cells
|
|
112
|
+
return occ
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def cell(x, y):
|
|
116
|
+
return int(round(x / GRID)), int(round(y / GRID))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def astar(occ, start, goal, net, via_cost=VIA_COST):
|
|
120
|
+
sx, sy = start[0]
|
|
121
|
+
sl = start[1]
|
|
122
|
+
gx, gy = goal[0]
|
|
123
|
+
gl = goal[1]
|
|
124
|
+
h0 = abs(sx - gx) + abs(sy - gy)
|
|
125
|
+
heap = [(h0, 0, sx, sy, sl)]
|
|
126
|
+
gsc = {(sx, sy, sl): 0}
|
|
127
|
+
came = {}
|
|
128
|
+
pushes = 0
|
|
129
|
+
while heap:
|
|
130
|
+
_, g, x, y, l = heapq.heappop(heap)
|
|
131
|
+
if x == gx and y == gy and l == gl:
|
|
132
|
+
path = [(x, y, l)]
|
|
133
|
+
while (x, y, l) in came:
|
|
134
|
+
x, y, l = came[(x, y, l)]
|
|
135
|
+
path.append((x, y, l))
|
|
136
|
+
return path[::-1]
|
|
137
|
+
pushes += 1
|
|
138
|
+
if pushes > MAX_PUSH:
|
|
139
|
+
return None
|
|
140
|
+
for dx, dy, dl, dc in ((1, 0, 0, 10), (-1, 0, 0, 10), (0, 1, 0, 10), (0, -1, 0, 10),
|
|
141
|
+
(0, 0, 1, VIA_COST), (0, 0, -1, VIA_COST)):
|
|
142
|
+
nx, ny, nl = x + dx, y + dy, l + dl
|
|
143
|
+
if nl < 0 or nl > 1:
|
|
144
|
+
continue
|
|
145
|
+
if dx and dy: # no diagonal corner cutting: both flanks must be open
|
|
146
|
+
if not (occ.passable(l, x + dx, y, net) and occ.passable(l, x, y + dy, net)):
|
|
147
|
+
continue
|
|
148
|
+
if not occ.passable(nl, nx, ny, net):
|
|
149
|
+
continue
|
|
150
|
+
if dl: # via may stand on its OWN pad (pinning) but never touch a
|
|
151
|
+
# foreign pad/box: cells whose net-set excludes us (or incl. the
|
|
152
|
+
# -7 gauntlet sentinel) are landing-banned.
|
|
153
|
+
bad = False
|
|
154
|
+
for ll in (0, 1):
|
|
155
|
+
for m in (occ.m[ll], occ.m[1 - ll]):
|
|
156
|
+
for ox in range(-3, 4):
|
|
157
|
+
for oy in range(-3, 4):
|
|
158
|
+
ss = m.get((nx + ox, ny + oy))
|
|
159
|
+
if ss is None:
|
|
160
|
+
continue
|
|
161
|
+
if -7 in ss or (net not in ss and ss != {0}):
|
|
162
|
+
bad = True
|
|
163
|
+
break
|
|
164
|
+
if bad: break
|
|
165
|
+
if bad: break
|
|
166
|
+
if bad:
|
|
167
|
+
break
|
|
168
|
+
if bad:
|
|
169
|
+
continue
|
|
170
|
+
ng = g + (via_cost if dl else dc)
|
|
171
|
+
if ng < gsc.get((nx, ny, nl), 1e18):
|
|
172
|
+
gsc[(nx, ny, nl)] = ng
|
|
173
|
+
came[(nx, ny, nl)] = (x, y, l)
|
|
174
|
+
heapq.heappush(heap, (ng + abs(nx - gx) + abs(ny - gy) + (0 if nl == gl else VIA_COST), ng, nx, ny, nl))
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def simplify(path):
|
|
179
|
+
if len(path) <= 2:
|
|
180
|
+
return path
|
|
181
|
+
out = [path[0]]
|
|
182
|
+
for i in range(1, len(path) - 1):
|
|
183
|
+
x0, y0, l0 = path[i - 1]
|
|
184
|
+
x1, y1, l1 = path[i]
|
|
185
|
+
x2, y2, l2 = path[i + 1]
|
|
186
|
+
if (x1 - x0, y1 - y0, l1 - l0) != (x2 - x1, y2 - y1, l2 - l1):
|
|
187
|
+
out.append(path[i])
|
|
188
|
+
out.append(path[-1])
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def occ_track(occ, path, net, width):
|
|
193
|
+
"""register a laid path (cells + clearance ring) into occupancy for OTHER nets."""
|
|
194
|
+
r = 2 if width < 0.35 else 3
|
|
195
|
+
for (cx, cy, l) in path:
|
|
196
|
+
for dx in range(-r, r + 1):
|
|
197
|
+
for dy in range(-r, r + 1):
|
|
198
|
+
occ.add_rect((cx + dx) * GRID, (cy + dy) * GRID,
|
|
199
|
+
(cx + dx) * GRID, (cy + dy) * GRID, l, net)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def route(board_path, out_path, only_nets=None):
|
|
203
|
+
txt = open(board_path).read()
|
|
204
|
+
pads = pads_of(txt)
|
|
205
|
+
nets = {int(m.group(1)): m.group(2) for m in re.finditer(r'\(net (\d+) "([^"]*)"\)', txt)}
|
|
206
|
+
occ = build_occ(pads)
|
|
207
|
+
# r17: J1 USB-C gauntlet — F.Cu inside the pad rows is forbidden (0.85mm pad
|
|
208
|
+
# pitch admits no 0.2mm pair); D+/D-, VBUS, CC and shield stubs are forced
|
|
209
|
+
# to B.Cu / the south channel. Sentinel net -7 closes the box to every net.
|
|
210
|
+
# (gauntlet experiment retired: foreign SMD pads only exist on their own
|
|
211
|
+
# layer's map — B.Cu threads between the J1 rows are clearance-legal.)
|
|
212
|
+
by_net = defaultdict(list)
|
|
213
|
+
for p in pads:
|
|
214
|
+
if p["net"]:
|
|
215
|
+
by_net[p["net"]].append(p)
|
|
216
|
+
# order: power/GND first, prefer bottom layer (B=1); signals then on F=0
|
|
217
|
+
def layer_pref(nid):
|
|
218
|
+
return 1 if nets.get(nid, "") in POWER else 0
|
|
219
|
+
ordered = sorted(by_net.items(), key=lambda kv: (nets.get(kv[0], "") not in POWER, -len(kv[1])))
|
|
220
|
+
segs, vias, fails = [], [], []
|
|
221
|
+
kept_segs = []
|
|
222
|
+
if only_nets:
|
|
223
|
+
fp_cut = txt.index("(footprint")
|
|
224
|
+
root_end = len(txt) - 1
|
|
225
|
+
tail_blocks = [m.group(0) for m in re.finditer(r'\(segment [^\n]*\)|\(via [^\n]*\)', txt[fp_cut:])]
|
|
226
|
+
newblocks = []
|
|
227
|
+
for blk in tail_blocks:
|
|
228
|
+
mn2 = re.search(r'\(net (\d+)\)', blk)
|
|
229
|
+
name2 = nets.get(int(mn2.group(1)), "") if mn2 else ""
|
|
230
|
+
if name2 in only_nets:
|
|
231
|
+
continue
|
|
232
|
+
newblocks.append(blk)
|
|
233
|
+
mm = re.search(r'\(segment \(start ([-\d.]+) ([-\d.]+)\) \(end ([-\d.]+) ([-\d.]+)\) \(width ([.\d]+)\) \(layer "([FB])\.Cu"\)', blk)
|
|
234
|
+
mv = re.search(r'\(via \(at ([-\d.]+) ([-\d.]+)', blk)
|
|
235
|
+
if mm:
|
|
236
|
+
x1_, y1_, x2_, y2_ = (float(mm.group(i)) for i in (2, 3, 4, 5))
|
|
237
|
+
w_ = float(mm.group(5)); l_ = 0 if mm.group(6) == "F" else 1
|
|
238
|
+
nn_ = max(1, int(max(abs(x2_ - x1_), abs(y2_ - y1_)) / GRID))
|
|
239
|
+
pts = [(cell(x1_ + (x2_ - x1_) * k / nn_, y1_ + (y2_ - y1_) * k / nn_)[0], cell(x1_ + (x2_ - x1_) * k / nn_, y1_ + (y2_ - y1_) * k / nn_)[1], l_) for k in range(nn_ + 1)]
|
|
240
|
+
occ_track(occ, pts, -1, w_)
|
|
241
|
+
elif mv:
|
|
242
|
+
vx, vy = float(mv.group(1)), float(mv.group(2))
|
|
243
|
+
for l_ in (0, 1):
|
|
244
|
+
occ.add_rect(vx - 0.4, vy - 0.4, vx + 0.4, vy + 0.4, l_, -1)
|
|
245
|
+
kept_segs = newblocks
|
|
246
|
+
ordered = [kv for kv in ordered if nets.get(kv[0], "") in only_nets]
|
|
247
|
+
txt = txt[:fp_cut] + "\n".join(newblocks) + "\n)\n"
|
|
248
|
+
for nid, plist in ordered:
|
|
249
|
+
if len(plist) < 2:
|
|
250
|
+
continue
|
|
251
|
+
if nets.get(nid, "").startswith("NC"):
|
|
252
|
+
continue # NC nets are floating-on-purpose groups: copper would
|
|
253
|
+
# only weld foreign pads together inside connector gauntlets
|
|
254
|
+
width = W_POWER if nets.get(nid, "") in POWER else W_SIG
|
|
255
|
+
root = min(plist, key=lambda q: (q["x"], q["y"]))
|
|
256
|
+
remaining = [q for q in plist if q is not root]
|
|
257
|
+
tree = [root]
|
|
258
|
+
while remaining:
|
|
259
|
+
best = None
|
|
260
|
+
for a in tree:
|
|
261
|
+
for q in remaining:
|
|
262
|
+
if a.get("ref") and q.get("ref") == a["ref"]:
|
|
263
|
+
continue # same-part pins bond internally: no copper
|
|
264
|
+
dist = abs(q["x"] - a["x"]) + abs(q["y"] - a["y"])
|
|
265
|
+
if best is None or dist < best[0]:
|
|
266
|
+
best = (dist, q)
|
|
267
|
+
if best is None:
|
|
268
|
+
break
|
|
269
|
+
tree.append(best[1])
|
|
270
|
+
remaining.remove(best[1])
|
|
271
|
+
lp = layer_pref(nid)
|
|
272
|
+
for a, c in zip(tree, tree[1:]):
|
|
273
|
+
ca = cell(a["x"], a["y"])
|
|
274
|
+
cc = cell(c["x"], c["y"])
|
|
275
|
+
def ok_via_ends(pp):
|
|
276
|
+
if not pp:
|
|
277
|
+
return False
|
|
278
|
+
a_ok = True if pp[0][2] == lp else (len(pp) > 1 and pp[1][2] != pp[0][2])
|
|
279
|
+
b_ok = True if pp[-1][2] == lp else (len(pp) > 1 and pp[-2][2] != pp[-1][2])
|
|
280
|
+
return a_ok and b_ok
|
|
281
|
+
cands = [astar(occ, (ca, lp), (cc, lp), nid),
|
|
282
|
+
astar(occ, (ca, lp), (cc, 1 - lp), nid),
|
|
283
|
+
astar(occ, (ca, 1 - lp), (cc, lp), nid)]
|
|
284
|
+
path = next((c for c in cands if ok_via_ends(c)), None)
|
|
285
|
+
if not path:
|
|
286
|
+
cands = [astar(occ, (ca, lp), (cc, lp), nid, via_cost=12),
|
|
287
|
+
astar(occ, (ca, lp), (cc, 1 - lp), nid, via_cost=12)]
|
|
288
|
+
path = next((c for c in cands if ok_via_ends(c)), None)
|
|
289
|
+
if not path:
|
|
290
|
+
fails.append((nets.get(nid), a["ref"] + "." + a["pad"], c["ref"] + "." + c["pad"]))
|
|
291
|
+
continue
|
|
292
|
+
sp = simplify(path)
|
|
293
|
+
occ_track(occ, path, nid, width)
|
|
294
|
+
for n1, n2 in zip(sp, sp[1:]):
|
|
295
|
+
if n1[2] != n2[2]:
|
|
296
|
+
vias.append((n1[0] * GRID, n1[1] * GRID, nid))
|
|
297
|
+
else:
|
|
298
|
+
segs.append((n1[0] * GRID, n1[1] * GRID, n2[0] * GRID, n2[1] * GRID, n1[2], width, nid))
|
|
299
|
+
u = [0x5A000000]
|
|
300
|
+
def U():
|
|
301
|
+
u[0] += 1
|
|
302
|
+
x = f"{u[0]:032x}"
|
|
303
|
+
return f"{x[:8]}-{x[8:12]}-{x[12:16]}-{x[16:20]}-{x[20:32]}"
|
|
304
|
+
emit = []
|
|
305
|
+
for (x1, y1, x2, y2, l, w, n) in segs:
|
|
306
|
+
emit.append(f' (segment (start {x1:.4f} {y1:.4f}) (end {x2:.4f} {y2:.4f}) (width {w})'
|
|
307
|
+
f' (layer "{"F" if l==0 else "B"}.Cu") (net {n}) (uuid "{U()}"))')
|
|
308
|
+
for (x, y, n) in vias:
|
|
309
|
+
emit.append(f' (via (at {x:.4f} {y:.4f}) (size 0.6) (drill 0.3)'
|
|
310
|
+
f' (layers "F.Cu" "B.Cu") (net {n}) (uuid "{U()}"))')
|
|
311
|
+
head = txt[:txt.rindex(")")].rstrip("\n")
|
|
312
|
+
mid = ("\n" + "\n".join(kept_segs)) if (only_nets and kept_segs) else ""
|
|
313
|
+
open(out_path, "w").write(head + mid + "\n".join(emit) + "\n)\n")
|
|
314
|
+
print(f"routed: segments={len(segs)} vias={len(vias)} failed_edges={len(fails)}")
|
|
315
|
+
for f in fails[:12]:
|
|
316
|
+
print(" FAIL-EDGE", f)
|
|
317
|
+
return len(fails)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
only = None
|
|
322
|
+
if "--only-nets" in sys.argv:
|
|
323
|
+
only = set(sys.argv[sys.argv.index("--only-nets") + 1].split(","))
|
|
324
|
+
sys.exit(1 if route(sys.argv[1], sys.argv[2], only) else 0)
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""r17 GND pour: strip greedy GND tracks, paint GND pads, raster-erode foreign
|
|
3
|
+
items, emit dual-layer (F.Cu+B.Cu) zone with scanline-exact boundary rings plus
|
|
4
|
+
stitching vias. Deterministic 0.2mm raster. Usage: board_zone_pour.py in out"""
|
|
5
|
+
import json, os, re, sys
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
CELL = 0.2
|
|
9
|
+
EDGE_INSET = 0.3
|
|
10
|
+
OBS_CLEAR = 0.25
|
|
11
|
+
HOLE_CLEAR = 0.30
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def sub_blocks(s, start):
|
|
15
|
+
out = []; i = start; n = len(s)
|
|
16
|
+
while i < n:
|
|
17
|
+
while i < n and s[i] != "(":
|
|
18
|
+
i += 1
|
|
19
|
+
if i >= n:
|
|
20
|
+
break
|
|
21
|
+
depth = 0; j = i; instr = False; esc = False
|
|
22
|
+
while j < n:
|
|
23
|
+
ch = s[j]
|
|
24
|
+
if esc: esc = False
|
|
25
|
+
elif ch == "\\": esc = True
|
|
26
|
+
elif ch == '"': instr = not instr
|
|
27
|
+
elif not instr:
|
|
28
|
+
if ch == "(": depth += 1
|
|
29
|
+
elif ch == ")":
|
|
30
|
+
depth -= 1
|
|
31
|
+
if depth == 0: break
|
|
32
|
+
j += 1
|
|
33
|
+
out.append(s[i + 1:j]); i = j + 1
|
|
34
|
+
p2 = i
|
|
35
|
+
while p2 < n and s[p2] in " \t\n\r":
|
|
36
|
+
p2 += 1
|
|
37
|
+
if p2 < n and s[p2] == ")":
|
|
38
|
+
break
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_pads(txt):
|
|
43
|
+
pads = []
|
|
44
|
+
for b in sub_blocks(sub_blocks(txt, txt.index("(kicad_pcb"))[0], 0):
|
|
45
|
+
if not b.startswith("footprint"):
|
|
46
|
+
continue
|
|
47
|
+
m0 = re.search(r'\(at ([-\d.]+) ([-\d.]+)\)', b[:b.find("(property") if "(property" in b else 200])
|
|
48
|
+
ax, ay = float(m0.group(1)), float(m0.group(2))
|
|
49
|
+
mr = re.search(r'\(rotate (\d+\.?\d*)\)', b[:b.find("(layer")])
|
|
50
|
+
rot = __import__("math").radians(float(mr.group(1))) if mr else 0.0
|
|
51
|
+
mref = re.search(r'\(property "Reference" "([^"]+)"', b)
|
|
52
|
+
ref = mref.group(1) if mref else ""
|
|
53
|
+
i = 0
|
|
54
|
+
while True:
|
|
55
|
+
i = b.find("(pad ", i)
|
|
56
|
+
if i < 0: break
|
|
57
|
+
d = 0; j = i; instr = False
|
|
58
|
+
while j < len(b):
|
|
59
|
+
c = b[j]
|
|
60
|
+
if c == '"': instr = not instr
|
|
61
|
+
elif not instr:
|
|
62
|
+
if c == "(": d += 1
|
|
63
|
+
elif c == ")":
|
|
64
|
+
d -= 1
|
|
65
|
+
if d == 0: break
|
|
66
|
+
j += 1
|
|
67
|
+
pb = b[i + 1:j]; i = j + 1
|
|
68
|
+
pnum = re.match(r'pad "([^"]*)"', pb).group(1)
|
|
69
|
+
sm = re.search(r'\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)', pb)
|
|
70
|
+
ox, oy = float(sm.group(1)), float(sm.group(2))
|
|
71
|
+
ca, sa = __import__("math").cos(rot), __import__("math").sin(rot)
|
|
72
|
+
wx, wy = ax + ox * ca - oy * sa, ay + ox * sa + oy * ca
|
|
73
|
+
sz = re.search(r'\(size ([-\d.]+) ([-\d.]+)\)', pb)
|
|
74
|
+
w, h = float(sz.group(1)), float(sz.group(2))
|
|
75
|
+
shape = "circle" if "(shape circle)" in pb or '"circle"' in pb else "rect"
|
|
76
|
+
drl = re.search(r'\(drill(?: oval)? ([\d.]+)(?: ([\d.]+))?\)', pb)
|
|
77
|
+
pads.append(dict(ref=ref, num=pnum, x=wx, y=wy, w=w, h=h, shape=shape,
|
|
78
|
+
drill=(float(drl.group(1)), float(drl.group(2) or drl.group(1))) if drl else None,
|
|
79
|
+
bnet=(re.search(r'\(net \d+ "([^"]*)"\)', pb) or [None, None])[1]))
|
|
80
|
+
return pads
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def rings_from_rects(rects, x0, y0):
|
|
84
|
+
segs = set()
|
|
85
|
+
for (xa, ya, xb, yb) in rects:
|
|
86
|
+
ax, ay = x0 + (xa - 1) * CELL, y0 + (ya - 1) * CELL
|
|
87
|
+
bx, by = x0 + (xb - 1) * CELL, y0 + (yb - 1) * CELL
|
|
88
|
+
segs.add(((ax, ay), (bx, ay)))
|
|
89
|
+
segs.add(((bx, ay), (bx, by)))
|
|
90
|
+
segs.add(((bx, by), (ax, by)))
|
|
91
|
+
segs.add(((ax, by), (ax, ay)))
|
|
92
|
+
kill = set()
|
|
93
|
+
for a, b in segs:
|
|
94
|
+
if (b, a) in segs:
|
|
95
|
+
kill.add((a, b)); kill.add((b, a))
|
|
96
|
+
eout = defaultdict(list)
|
|
97
|
+
for a, b in segs - kill:
|
|
98
|
+
eout[a].append(b)
|
|
99
|
+
rings = []
|
|
100
|
+
for st in list(eout.keys()):
|
|
101
|
+
while eout[st]:
|
|
102
|
+
stack = [st]; trail = []
|
|
103
|
+
cur = st
|
|
104
|
+
while True:
|
|
105
|
+
outs = eout[cur]
|
|
106
|
+
if not outs:
|
|
107
|
+
break
|
|
108
|
+
nxt = outs.pop()
|
|
109
|
+
stack.append(nxt)
|
|
110
|
+
cur = nxt
|
|
111
|
+
if cur == st:
|
|
112
|
+
break
|
|
113
|
+
if cur == st and len(stack) >= 5:
|
|
114
|
+
rings.append(stack[:-1])
|
|
115
|
+
elif cur != st:
|
|
116
|
+
for q in reversed(stack[stack.index(st) + 1:]):
|
|
117
|
+
eout.setdefault(stack[stack.index(q)][0], [])
|
|
118
|
+
def area(r):
|
|
119
|
+
return sum(r[k][0] * r[k + 1][1] - r[k + 1][0] * r[k][1] for k in range(len(r) - 1)) / 2.0
|
|
120
|
+
out = []
|
|
121
|
+
for r in rings:
|
|
122
|
+
rr = []
|
|
123
|
+
for q in r:
|
|
124
|
+
if not rr or rr[-1] != q:
|
|
125
|
+
rr.append(q)
|
|
126
|
+
if len(rr) >= 5 and rr[0] == rr[-1]:
|
|
127
|
+
rr = rr[:-1]
|
|
128
|
+
if len(rr) >= 4 and abs(area(rr)) > 0.5:
|
|
129
|
+
out.append(rr)
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main(inp, outp):
|
|
134
|
+
txt = open(inp).read()
|
|
135
|
+
nm = dict((m.group(2).strip('"'), int(m.group(1))) for m in
|
|
136
|
+
re.finditer(r'\(net (\d+) "([^"]+)"\)', txt))
|
|
137
|
+
gnd_code = nm.get("GND")
|
|
138
|
+
assert gnd_code is not None, "no GND net"
|
|
139
|
+
POW = [x for x in (os.environ.get("PCB_POWER_ZONE_NETS", "5V_SERVO,5V_LOGIC,3V3,VBUS")).split(",") if x]
|
|
140
|
+
pcode = {n: nm[n] for n in POW if n in nm}
|
|
141
|
+
padby = defaultdict(list)
|
|
142
|
+
prect = {}
|
|
143
|
+
pwr_codes = set(pcode.values())
|
|
144
|
+
|
|
145
|
+
root_children = sub_blocks(sub_blocks(txt, txt.index("(kicad_pcb"))[0], 0)
|
|
146
|
+
xs, ys = [], []
|
|
147
|
+
for b in root_children:
|
|
148
|
+
if b.startswith("gr_line") and "Edge.Cuts" in b:
|
|
149
|
+
for mm in re.finditer(r'\((?:start|end) ([-\d.]+) ([-\d.]+)\)', b):
|
|
150
|
+
xs.append(float(mm.group(1))); ys.append(float(mm.group(2)))
|
|
151
|
+
x0, x1, y0, y1 = min(xs), max(xs), min(ys), max(ys)
|
|
152
|
+
spec = "/home/lab/workspace/pcb-local-repro/solar_ray_v2/circuit/netlist.json"
|
|
153
|
+
pad_net = {}
|
|
154
|
+
for c in json.load(open(spec))["components"]:
|
|
155
|
+
for pin, net in c["pins"].items():
|
|
156
|
+
pad_net[(c["ref"], pin)] = net
|
|
157
|
+
pads = parse_pads(txt)
|
|
158
|
+
for pd_ in pads:
|
|
159
|
+
pd_["net"] = pd_["bnet"] or pad_net.get((pd_["ref"], pd_["num"]), "?")
|
|
160
|
+
padby[pd_["net"]].append(pd_)
|
|
161
|
+
for n_, ps in padby.items():
|
|
162
|
+
if n_ in pcode:
|
|
163
|
+
xs_ = [pd_["x"] - pd_["w"] / 2 for pd_ in ps] + [pd_["x"] + pd_["w"] / 2 for pd_ in ps]
|
|
164
|
+
ys_ = [pd_["y"] - pd_["h"] / 2 for pd_ in ps] + [pd_["y"] + pd_["h"] / 2 for pd_ in ps]
|
|
165
|
+
prect[n_] = (min(xs_) - 0.8, min(ys_) - 0.8, max(xs_) + 0.8, max(ys_) + 0.8)
|
|
166
|
+
gnd_name = "GND"
|
|
167
|
+
keep = []
|
|
168
|
+
nstrip = 0
|
|
169
|
+
fp_i = next(i for i, b in enumerate(root_children) if b.startswith("footprint"))
|
|
170
|
+
for b in root_children[fp_i:]:
|
|
171
|
+
if b.startswith("segment") or b.startswith("via"):
|
|
172
|
+
mn = re.search(r'\(net (\d+)\)', b)
|
|
173
|
+
if mn and int(mn.group(1)) in ({gnd_code} | pwr_codes):
|
|
174
|
+
nstrip += 1
|
|
175
|
+
continue
|
|
176
|
+
keep.append("(" + b + ")")
|
|
177
|
+
W = int((x1 - x0) / CELL) + 2
|
|
178
|
+
H = int((y1 - y0) / CELL) + 2
|
|
179
|
+
occ = bytearray(W * H)
|
|
180
|
+
rawpad = bytearray(W * H)
|
|
181
|
+
|
|
182
|
+
def cx(v): return int((v - x0) / CELL) + 1
|
|
183
|
+
def cy(v): return int((v - y0) / CELL) + 1
|
|
184
|
+
|
|
185
|
+
def mark_rect(xa, ya, xb, yb, r, arr):
|
|
186
|
+
gx0, gx1 = sorted((cx(xa), cx(xb)))
|
|
187
|
+
gy0, gy1 = sorted((cy(ya), cy(yb)))
|
|
188
|
+
pad = int(r / CELL) + 1
|
|
189
|
+
for gy in range(max(0, gy0 - pad), min(H, gy1 + pad + 1)):
|
|
190
|
+
for gx in range(max(0, gx0 - pad), min(W, gx1 + pad + 1)):
|
|
191
|
+
arr[gy * W + gx] = 1
|
|
192
|
+
for blk in keep:
|
|
193
|
+
if blk.startswith("(segment"):
|
|
194
|
+
m1 = re.search(r'\(start ([-\d.]+) ([-\d.]+)\) \(end ([-\d.]+) ([-\d.]+)\)', blk)
|
|
195
|
+
if m1:
|
|
196
|
+
xa, ya, xb, yb = (float(m1.group(i)) for i in (1, 2, 3, 4))
|
|
197
|
+
n_ = int(max(abs(xb - xa), abs(yb - ya)) / (CELL / 2)) + 1
|
|
198
|
+
for k in range(n_ + 1):
|
|
199
|
+
t = k / n_
|
|
200
|
+
mark_rect(xa + (xb - xa) * t - CELL, ya + (yb - ya) * t - CELL,
|
|
201
|
+
xa + (xb - xa) * t + CELL, ya + (yb - ya) * t + CELL, 0.15, occ)
|
|
202
|
+
elif blk.startswith("(via"):
|
|
203
|
+
m2 = re.search(r'\(via \(at ([-\d.]+) ([-\d.]+)\)', blk)
|
|
204
|
+
if m2:
|
|
205
|
+
mark_rect(float(m2.group(1)), float(m2.group(2)),
|
|
206
|
+
float(m2.group(1)), float(m2.group(2)), 0.55, occ)
|
|
207
|
+
for p_ in pads:
|
|
208
|
+
mark_rect(p_["x"] - p_["w"] / 2, p_["y"] - p_["h"] / 2,
|
|
209
|
+
p_["x"] + p_["w"] / 2, p_["y"] + p_["h"] / 2, 0.5, rawpad)
|
|
210
|
+
if p_["net"] != gnd_name:
|
|
211
|
+
if p_["drill"]:
|
|
212
|
+
mark_rect(p_["x"] - p_["w"] / 2, p_["y"] - p_["h"] / 2,
|
|
213
|
+
p_["x"] + p_["w"] / 2, p_["y"] + p_["h"] / 2, 0.5, occ)
|
|
214
|
+
else:
|
|
215
|
+
mark_rect(p_["x"] - p_["w"] / 2, p_["y"] - p_["h"] / 2,
|
|
216
|
+
p_["x"] + p_["w"] / 2, p_["y"] + p_["h"] / 2, 0.5, occ)
|
|
217
|
+
for blk in keep:
|
|
218
|
+
if blk.startswith("(segment"):
|
|
219
|
+
m1 = re.search(r'\(start ([-\d.]+) ([-\d.]+)\) \(end ([-\d.]+) ([-\d.]+)\)', blk)
|
|
220
|
+
if m1:
|
|
221
|
+
xa, ya, xb, yb = (float(m1.group(i)) for i in (1, 2, 3, 4))
|
|
222
|
+
nseg = int(max(abs(xb - xa), abs(yb - ya)) / (CELL / 2)) + 1
|
|
223
|
+
for k in range(nseg + 1):
|
|
224
|
+
t = k / nseg
|
|
225
|
+
px_, py_ = xa + (xb - xa) * t, ya + (yb - ya) * t
|
|
226
|
+
mark_rect(px_ - CELL, py_ - CELL, px_ + CELL, py_ + CELL, 0.1, occ)
|
|
227
|
+
bx0, by0, bx1, by1 = x0 + EDGE_INSET, y0 + EDGE_INSET, x1 - EDGE_INSET, y1 - EDGE_INSET
|
|
228
|
+
outline_pts = f"(xy {bx0:.4f} {by0:.4f}) (xy {bx1:.4f} {by0:.4f}) (xy {bx1:.4f} {by1:.4f}) (xy {bx0:.4f} {by1:.4f}) (xy {bx0:.4f} {by0:.4f})"
|
|
229
|
+
# fine-pitch connector keepout: no pour under USB-C-class parts (0.85mm rows)
|
|
230
|
+
kx = [pd_ for pd_ in pads if pd_["ref"] in {"J1"}]
|
|
231
|
+
KEEPOUT_RECT = None
|
|
232
|
+
if kx:
|
|
233
|
+
hx0 = min(p_["x"] - p_["w"] / 2 for p_ in kx) - 0.3
|
|
234
|
+
hy0 = min(p_["y"] - p_["h"] / 2 for p_ in kx) - 0.3
|
|
235
|
+
hx1 = max(p_["x"] + p_["w"] / 2 for p_ in kx) + 0.3
|
|
236
|
+
hy1 = max(p_["y"] + p_["h"] / 2 for p_ in kx) + 0.3
|
|
237
|
+
KEEPOUT_RECT = (hx0, hy0, hx1, hy1)
|
|
238
|
+
_ = KEEPOUT_RECT
|
|
239
|
+
outline = outline_pts
|
|
240
|
+
z = []
|
|
241
|
+
for LI, LAY in enumerate(["F.Cu", "B.Cu"]):
|
|
242
|
+
z += [f'(zone (net {gnd_code}) (net_name "GND") (layer "{LAY}") (uuid "5a000000-0000-0000-0000-00000000cafe{LI}")',
|
|
243
|
+
' (hatch edge 0.508)',
|
|
244
|
+
' (connect_pads (clearance 0))',
|
|
245
|
+
' (min_thickness 0.2)',
|
|
246
|
+
' (filled_areas_thickness no)',
|
|
247
|
+
' (fill yes (thermal_gap 0.4) (thermal_bridge_width 0.5))',
|
|
248
|
+
f' (polygon (pts {outline}))',
|
|
249
|
+
')']
|
|
250
|
+
def rectpts(xa, ya, xb, yb):
|
|
251
|
+
return f"(xy {xa:.4f} {ya:.4f}) (xy {xb:.4f} {ya:.4f}) (xy {xb:.4f} {yb:.4f}) (xy {xa:.4f} {yb:.4f}) (xy {xa:.4f} {ya:.4f})"
|
|
252
|
+
for zi, (n_, rc) in enumerate(sorted(prect.items())):
|
|
253
|
+
for LI, LAY in enumerate(["F.Cu", "B.Cu"]):
|
|
254
|
+
holes = []
|
|
255
|
+
# explicit holes ONLY for GND pads (protects GND fill corridors at the
|
|
256
|
+
# zone rim); power-vs-power carve stays native so a neighboring
|
|
257
|
+
# foreign rect never punches away a power pad's own connect island.
|
|
258
|
+
for pd_ in pads:
|
|
259
|
+
if pd_["net"] != "GND":
|
|
260
|
+
continue
|
|
261
|
+
hx0, hy0 = pd_["x"] - pd_["w"] / 2 - 0.30, pd_["y"] - pd_["h"] / 2 - 0.30
|
|
262
|
+
hx1, hy1 = pd_["x"] + pd_["w"] / 2 + 0.30, pd_["y"] + pd_["h"] / 2 + 0.30
|
|
263
|
+
if hx1 < rc[0] or hx0 > rc[2] or hy1 < rc[1] or hy0 > rc[3]:
|
|
264
|
+
continue
|
|
265
|
+
holes.append(rectpts(hx0, hy0, hx1, hy1))
|
|
266
|
+
pts = f"(pts {rectpts(*rc)}" + ("".join(" " + h for h in holes)) + ")"
|
|
267
|
+
z += [f'(zone (net {pcode[n_]}) (net_name "{n_}") (layer "{LAY}") (priority 2) (uuid "5a000000-0000-0000-0000-00000000cbfe{zi}{LI}")',
|
|
268
|
+
' (hatch edge 0.508)',
|
|
269
|
+
' (connect_pads (clearance 0))',
|
|
270
|
+
' (min_thickness 0.2)',
|
|
271
|
+
' (fill yes (thermal_gap 0.4) (thermal_bridge_width 0.5))',
|
|
272
|
+
f' (polygon {pts})',
|
|
273
|
+
')']
|
|
274
|
+
if KEEPOUT_RECT:
|
|
275
|
+
for LI, LAY in enumerate(["F.Cu", "B.Cu"]):
|
|
276
|
+
k = KEEPOUT_RECT
|
|
277
|
+
z += [f'(zone (net -1) (net_name "") (layer "{LAY}") (uuid "5a000000-0000-0000-0000-00000000cdfe{LI}")',
|
|
278
|
+
' (hatch edge 0.508)',
|
|
279
|
+
' (priority 15)',
|
|
280
|
+
f' (polygon (pts (xy {k[0]:.4f} {k[1]:.4f}) (xy {k[2]:.4f} {k[1]:.4f}) (xy {k[2]:.4f} {k[3]:.4f}) (xy {k[0]:.4f} {k[3]:.4f}) (xy {k[0]:.4f} {k[1]:.4f})))',
|
|
281
|
+
')']
|
|
282
|
+
stitch = 0
|
|
283
|
+
step = max(1, int(4.0 / CELL))
|
|
284
|
+
for gy in range(1, H - 1, step):
|
|
285
|
+
for gx in range(1, W - 1, step):
|
|
286
|
+
rr = int(0.55 / CELL)
|
|
287
|
+
vxc, vyc = x0 + (gx - 1) * CELL, y0 + (gy - 1) * CELL
|
|
288
|
+
if not (x0 + 1.0 < vxc < x1 - 1.0 and y0 + 1.0 < vyc < y1 - 1.0):
|
|
289
|
+
continue # board-edge via guard: copper_edge_clearance class
|
|
290
|
+
if any(rc[0] + 0.3 < vxc < rc[2] - 0.3 and rc[1] + 0.3 < vyc < rc[3] - 0.3 for rc in prect.values()):
|
|
291
|
+
continue # GND via must not land inside a power zone
|
|
292
|
+
if KEEPOUT_RECT and KEEPOUT_RECT[0] - 0.2 < vxc < KEEPOUT_RECT[2] + 0.2 and KEEPOUT_RECT[1] - 0.2 < vyc < KEEPOUT_RECT[3] + 0.2:
|
|
293
|
+
continue # fine-pitch connector keepout
|
|
294
|
+
if any(occ[max(0, min(H - 1, gy + dy2)) * W + max(0, min(W - 1, gx + dx2))] or
|
|
295
|
+
rawpad[max(0, min(H - 1, gy + dy2)) * W + max(0, min(W - 1, gx + dx2))]
|
|
296
|
+
for dx2 in range(-rr, rr + 1) for dy2 in range(-rr, rr + 1)):
|
|
297
|
+
continue
|
|
298
|
+
stitch += 1
|
|
299
|
+
z.append(f'(via (at {x0 + (gx - 1) * CELL:.4f} {y0 + (gy - 1) * CELL:.4f}) (size 0.6) (drill 0.3)'
|
|
300
|
+
f' (layers "F.Cu" "B.Cu") (net {gnd_code}) (uuid "00000000-0000-0000-0000-{stitch:012d}cafe0"))')
|
|
301
|
+
print(f"stripped={nstrip} pads={len(pads)} stitch-vias={stitch} zones={2 + 2 * len(prect)} (GND x2 + power x{2 * len(prect)}, native refill)")
|
|
302
|
+
# GND net-class -> solid pad connection (kills starved_thermal at pour rims)
|
|
303
|
+
head = re.sub(r'(\(net_class "GND"[^\n]*?)(\s*\)\s*)$',
|
|
304
|
+
r'\1 (pad_connection "solid")\2',
|
|
305
|
+
txt[:txt.index("(footprint")].rstrip("\n"))
|
|
306
|
+
body = "\n".join(keep if keep else [])
|
|
307
|
+
tail = "\n".join(z)
|
|
308
|
+
open(outp, "w").write(head + "\n" + body + "\n" + tail + "\n)\n")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
main(sys.argv[1], sys.argv[2])
|