astra/f1c100s
The code defines the physical pin layout, labels, and footprint for the F1C100S system-on-chip (SoC) component used in electronic devices.
- Version
- 0.9.2
- License
- unset
- Stars
- 0
scripts/optimize-routes.ts
import { checkCapacitorOrientation } from "./check-capacitor-orientation";
import { checkConventionalRouting } from "./check-conventions";
import { readFile, writeFile } from "node:fs/promises";
import { LAYOUT_PROFILES, assertLayoutProfile } from "../src/profiles";
import { validateCircuit, circuitMetrics } from "./validate";
type P = { x: number; y: number; [key: string]: any };
const distance = (a: P, b: P) => Math.hypot(a.x - b.x, a.y - b.y);
function pointSegment(p: P, a: P, b: P) {
const dx = b.x - a.x,
dy = b.y - a.y,
d = dx * dx + dy * dy;
const t = d
? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / d))
: 0;
return Math.hypot(p.x - a.x - t * dx, p.y - a.y - t * dy);
}
const cross = (a: P, b: P, c: P) =>
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
function segmentDistance(a: P, b: P, c: P, d: P) {
if (
cross(a, b, c) * cross(a, b, d) < 0 &&
cross(c, d, a) * cross(c, d, b) < 0
)
return 0;
return Math.min(
pointSegment(a, c, d),
pointSegment(b, c, d),
pointSegment(c, a, b),
pointSegment(d, a, b),
);
}
function rectangleDistance(a: P, b: P, p: P) {
const x = p.x - p.width / 2,
y = p.y - p.height / 2,
X = p.x + p.width / 2,
Y = p.y + p.height / 2;
if ([a, b].some((q) => q.x >= x && q.x <= X && q.y >= y && q.y <= Y))
return 0;
const v = [
{ x, y },
{ x: X, y },
{ x: X, y: Y },
{ x, y: Y },
];
return Math.min(
...v.map((c, i) => segmentDistance(a, b, c, v[(i + 1) % 4]!)),
);
}
function obstacles(json: any[]) {
const traces = json.filter((e) => e.type === "pcb_trace");
const netByPort = new Map(
json
.filter((e) => e.type === "source_trace")
.flatMap((e) =>
e.connected_source_port_ids.map((p: string) => [p, e.source_trace_id]),
),
);
const ports = new Map(
json
.filter((e) => e.type === "pcb_port")
.map((e) => [e.pcb_port_id, netByPort.get(e.source_port_id)]),
);
const pads = json
.filter((e) => e.type === "pcb_smtpad")
.map((e) => ({ ...e, net: ports.get(e.pcb_port_id) }));
const segments: any[] = [],
vias: any[] = json
.filter((e) => e.type === "pcb_plated_hole")
.map((e) => ({
...e,
net: ports.get(e.pcb_port_id),
r: e.outer_diameter / 2,
}));
for (const t of traces)
for (let i = 0; i < t.route.length; i++) {
const b = t.route[i],
a = t.route[i - 1];
if (b.route_type === "via")
vias.push({ ...b, net: t.source_trace_id, r: 0.225 });
if (
a?.route_type === "wire" &&
b.route_type === "wire" &&
a.layer === b.layer &&
distance(a, b) > 1e-7
)
segments.push({
a,
b,
layer: a.layer,
net: t.source_trace_id,
r: (Math.max(a.width, b.width) || 0.12) / 2,
minX: Math.min(a.x, b.x),
maxX: Math.max(a.x, b.x),
minY: Math.min(a.y, b.y),
maxY: Math.max(a.y, b.y),
});
}
return (a: P, b: P, net: string) => {
const gap = 0.10001,
r = Math.max(a.width || 0.12, b.width || 0.12) / 2;
const minX = Math.min(a.x, b.x),
maxX = Math.max(a.x, b.x),
minY = Math.min(a.y, b.y),
maxY = Math.max(a.y, b.y);
for (const p of pads)
if (p.net !== net && p.layer === a.layer) {
const margin = gap + r;
if (
p.x + p.width / 2 + margin < minX ||
p.x - p.width / 2 - margin > maxX ||
p.y + p.height / 2 + margin < minY ||
p.y - p.height / 2 - margin > maxY
)
continue;
if (rectangleDistance(a, b, p) < margin) return false;
}
for (const v of vias)
if (v.net !== net) {
const margin = gap + r + v.r;
if (
v.x + margin < minX ||
v.x - margin > maxX ||
v.y + margin < minY ||
v.y - margin > maxY
)
continue;
if (pointSegment(v, a, b) < margin) return false;
}
for (const s of segments)
if (s.net !== net && s.layer === a.layer) {
const margin = gap + r + s.r;
if (
s.maxX + margin < minX ||
s.minX - margin > maxX ||
s.maxY + margin < minY ||
s.minY - margin > maxY
)
continue;
if (segmentDistance(a, b, s.a, s.b) < margin) return false;
}
return true;
};
}
function refreshVias(json: any[]) {
const byKey = new Map<string, any>();
for (const t of json.filter((e) => e.type === "pcb_trace"))
for (const p of t.route)
if (p.route_type === "via") {
const key = `${p.x.toFixed(6)},${p.y.toFixed(6)}`;
if (!byKey.has(key))
byKey.set(key, {
type: "pcb_via",
pcb_via_id: `pcb_via_${byKey.size}`,
pcb_trace_id: t.pcb_trace_id,
x: p.x,
y: p.y,
hole_diameter: 0.2,
outer_diameter: 0.45,
layers: ["top", "inner1", "inner2", "bottom"],
from_layer: p.from_layer,
to_layer: p.to_layer,
subcircuit_id: t.subcircuit_id,
});
}
return [...json.filter((e) => e.type !== "pcb_via"), ...byKey.values()];
}
const isConventional = (a: P, b: P) => {
const dx = Math.abs(b.x - a.x),
dy = Math.abs(b.y - a.y);
return Math.min(dx, dy, Math.abs(dx - dy)) < 1e-7;
};
function conventionalPaths(a: P, b: P): P[][] {
if (isConventional(a, b)) return [[a, b]];
const dx = b.x - a.x,
dy = b.y - a.y,
d = Math.min(Math.abs(dx), Math.abs(dy));
const point = (x: number, y: number) => ({
route_type: "wire",
x,
y,
layer: a.layer,
width: a.width ?? 0.12,
});
return [
[a, point(a.x + Math.sign(dx) * d, a.y + Math.sign(dy) * d), b],
[a, point(b.x - Math.sign(dx) * d, b.y - Math.sign(dy) * d), b],
[a, point(a.x, b.y), b],
[a, point(b.x, a.y), b],
].map((path) =>
path.filter((p, i) => !i || distance(p, path[i - 1]!) > 1e-8),
);
}
function shortcut(
a: P,
b: P,
net: string,
clear: ReturnType<typeof obstacles>,
) {
return conventionalPaths(a, b).find((path) =>
path.every((p, i) => !i || clear(path[i - 1]!, p, net)),
);
}
const selectedProfile = process.argv[2];
if (selectedProfile) assertLayoutProfile(selectedProfile);
for (const profile of selectedProfile ? [selectedProfile] : LAYOUT_PROFILES) {
const file = `src/generated/${profile}.circuit.json`;
let json = JSON.parse(await readFile(file, "utf8"));
const before = circuitMetrics(json);
for (const t of json.filter((e: any) => e.type === "pcb_trace"))
for (let i = 0; i < t.route.length; i++) {
const p = t.route[i];
if (p.route_type !== "via") continue;
const a = t.route
.slice(0, i)
.findLast((q: any) => q.route_type === "wire");
const b = t.route.slice(i + 1).find((q: any) => q.route_type === "wire");
if (!a || !b) throw Error("Via without contacts");
p.from_layer = a.layer;
p.to_layer = b.layer;
}
for (let pass = 0; pass < 2; pass++) {
for (const t of json.filter((e: any) => e.type === "pcb_trace")) {
const clear = obstacles(json);
const old = t.route;
const result: P[] = [];
for (let i = 0; i < old.length; i++) {
const a = old[i];
result.push(a);
if (a.route_type !== "wire") continue;
for (let j = old.length - 1; j > i + 1; j--) {
const b = old[j];
if (b.route_type !== "wire" || b.layer !== a.layer) continue;
const path = shortcut(a, b, t.source_trace_id, clear);
if (path) {
result.push(...path.slice(1, -1));
i = j - 1;
break;
}
}
}
// Replace a short A→B→C detour with A→C at its existing second via.
for (let i = 1; i < result.length - 1; i++) {
const v = result[i],
a = result[i - 1];
if (v.route_type !== "via" || a.route_type !== "wire") continue;
let j = i + 1;
while (j < result.length && result[j]!.route_type !== "via") j++;
const w = result[j];
if (!w || distance(v, w) > 2 || v.from_layer === w.to_layer) continue;
const incoming = {
route_type: "wire",
x: w.x,
y: w.y,
layer: v.from_layer,
width: 0.12,
};
const path = shortcut(a, incoming, t.source_trace_id, clear);
if (path) {
result.splice(i, j - i + 1, ...path.slice(1), {
...w,
from_layer: v.from_layer,
});
}
}
t.route = result;
}
json = refreshVias(json);
}
// Exact imported pad centers are not all on the routing grid. Fix their
// tiny entry/exit offsets as well; never leave an arbitrary-angle segment.
for (const t of json.filter((e: any) => e.type === "pcb_trace")) {
const clear = obstacles(json),
route: P[] = [];
for (const b of t.route) {
const a = route.at(-1);
if (
a?.route_type === "wire" &&
b.route_type === "wire" &&
a.layer === b.layer &&
!isConventional(a, b)
) {
const path = shortcut(a, b, t.source_trace_id, clear);
if (!path) throw Error(`Cannot make ${t.pcb_trace_id} octilinear`);
route.push(...path.slice(1));
} else route.push(b);
}
t.route = route;
}
const errors = [
...(await validateCircuit(json)),
...checkConventionalRouting(json),
...checkCapacitorOrientation(json),
];
if (errors.length) {
console.log(profile, errors.slice(0, 5));
throw new Error(`${profile}: ${errors.length} DRC errors`);
}
const after = circuitMetrics(json);
await writeFile(file, JSON.stringify(json, null, 2) + "\n");
await writeFile(
`src/generated/${profile}.metrics.json`,
JSON.stringify(
{
...JSON.parse(
await readFile(`src/generated/${profile}.metrics.json`, "utf8"),
),
...after,
drcErrors: 0,
optimization: { before, after },
},
null,
2,
) + "\n",
);
console.log(profile, JSON.stringify({ before, after }));
}