astra/f1c100s

This code defines and renders a four-layer, top-mounted F1C100S module with detailed pin assignments, schematic annotations, and layout profiles, facilitating design, validation, and routing of the hardware including support for decoupling capacitors, crystal oscillators, and external connectors.

Version
0.11.0
License
unset
Stars
0

scripts/optimize-routes.ts

import { checkDecouplingPlacement } from "./check-decoupling-targets";
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);
	const capIds = new Set(
		json
			.filter(
				(e: any) => e.type === "source_component" && /^C_D\d+$/.test(e.name),
			)
			.map((e: any) => e.source_component_id),
	);
	const groundIds = new Set(
		json
			.filter(
				(e: any) =>
					e.type === "source_port" &&
					capIds.has(e.source_component_id) &&
					e.pin_number === 2,
			)
			.map((e: any) => e.source_port_id),
	);
	const groundPads = json.filter(
		(e: any) => e.type === "pcb_port" && groundIds.has(e.source_port_id),
	);
	const protectedVia = (p: any) =>
		p?.route_type === "via" &&
		groundPads.some((g: any) => distance(p, g) < 1.3);
	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;
					if (old.slice(i + 1, j).some(protectedVia)) 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 ||
					protectedVia(v) ||
					protectedVia(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),
		...checkDecouplingPlacement(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 }));
}