imrishabh18/pedometer

This code defines and assembles a simple radio receiver hardware circuit using specific imported capacitors, inductors, RF connectors, and oscillator components with precise footprints and schematic attributes.

Version
1.1.3
License
unset
Stars
0

scripts/routing-style-report.ts

import fs from "node:fs";
import path from "node:path";

/** Usage: bun scripts/routing-style-report.ts baseline/circuit.json current/circuit.json
 * Writes JSON to stdout. This measures routed copper appearance, not DRC or
 * electrical equivalence. Copper pours and ratsnest connections are excluded.
 */
const filePaths = process.argv.slice(2);
if (!filePaths.length) {
  console.error("Usage: bun scripts/routing-style-report.ts <circuit.json> [<circuit.json> ...]");
  process.exit(2);
}

const minimumLongSegmentMm = 0.5;
const angleToleranceDegrees = 1;
const zeroLengthToleranceMm = 1e-9;
const round = (value: number) => Math.round(value * 1e6) / 1e6;

type SegmentTotals = {
  copperLengthMm: number;
  segments: number;
  longSegments: number;
  non45Segments: number;
  non45CopperLengthMm: number;
  longNon45Segments: number;
  longNon45CopperLengthMm: number;
};
const emptyTotals = (): SegmentTotals => ({
  copperLengthMm: 0, segments: 0, longSegments: 0,
  non45Segments: 0, non45CopperLengthMm: 0,
  longNon45Segments: 0, longNon45CopperLengthMm: 0,
});

function accumulate(totals: SegmentTotals, length: number, deviation: number) {
  totals.copperLengthMm += length;
  totals.segments++;
  const isLong = length > minimumLongSegmentMm;
  if (isLong) totals.longSegments++;
  if (deviation <= angleToleranceDegrees) return;
  totals.non45Segments++;
  totals.non45CopperLengthMm += length;
  if (isLong) {
    totals.longNon45Segments++;
    totals.longNon45CopperLengthMm += length;
  }
}

function finish(totals: SegmentTotals) {
  return {
    ...Object.fromEntries(Object.entries(totals).map(([key, value]) => [key, round(value)])),
    longNon45SegmentPercent: round(totals.longSegments
      ? 100 * totals.longNon45Segments / totals.longSegments : 0),
    non45CopperLengthPercent: round(totals.copperLengthMm
      ? 100 * totals.non45CopperLengthMm / totals.copperLengthMm : 0),
  };
}

function analyze(file: string) {
  const circuit: any[] = JSON.parse(fs.readFileSync(file, "utf8"));
  if (!Array.isArray(circuit)) throw new Error(`${file}: expected a Circuit JSON array`);
  const netNames = new Map(circuit.filter(e => e.type === "source_net")
    .map(e => [e.source_net_id, e.name]));
  const sourceTraces = new Map(circuit.filter(e => e.type === "source_trace")
    .map(e => [e.source_trace_id, e]));
  const traces = circuit.filter(e => e.type === "pcb_trace");
  const totals = emptyTotals();
  const layers = new Map<string, SegmentTotals>();
  const nets = new Map<string, SegmentTotals>();
  for (const trace of traces) {
    const source = sourceTraces.get(trace.source_trace_id);
    const netName = (source?.connected_source_net_ids ?? [])
      .map((id: string) => netNames.get(id) ?? id).join(", ")
      || netNames.get(trace.connection_name) || trace.connection_name || "unnamed";
    const netTotals = nets.get(netName) ?? emptyTotals();
    nets.set(netName, netTotals);
    const route = trace.route ?? [];
    for (let i = 1; i < route.length; i++) {
      const start = route[i - 1], end = route[i];
      // A planar copper segment is represented by consecutive wire points.
      // Via and through-pad transitions have no planar routing angle.
      if (start.route_type !== "wire" || end.route_type !== "wire"
        || start.layer !== end.layer) continue;
      const dx = end.x - start.x, dy = end.y - start.y;
      const length = Math.hypot(dx, dy);
      if (!Number.isFinite(length)) throw new Error(`${file}: invalid coordinates in ${trace.pcb_trace_id}`);
      if (length <= zeroLengthToleranceMm) continue;
      const angle = Math.atan2(dy, dx) * 180 / Math.PI;
      const deviation = Math.abs(angle - 45 * Math.round(angle / 45));
      const layerTotals = layers.get(start.layer) ?? emptyTotals();
      layers.set(start.layer, layerTotals);
      for (const target of [totals, layerTotals, netTotals]) accumulate(target, length, deviation);
    }
  }
  const board = circuit.find(e => e.type === "pcb_board");
  return {
    file: path.resolve(file),
    boardSizeMm: board ? { width: board.width, height: board.height } : null,
    components: circuit.filter(e => e.type === "pcb_component").length,
    traces: traces.length,
    vias: circuit.filter(e => e.type === "pcb_via").length,
    ...finish(totals),
    layers: Object.fromEntries([...layers].sort(([a], [b]) => a.localeCompare(b))
      .map(([layer, values]) => [layer, finish(values)])),
    nets: Object.fromEntries([...nets].sort(([a], [b]) => a.localeCompare(b))
      .map(([net, values]) => [net, finish(values)])),
  };
}

try {
  const reports = filePaths.map(analyze);
  console.log(JSON.stringify({
    schemaVersion: 1,
    definitions: {
      minimumLongSegmentMm,
      angleToleranceDegrees,
      thresholds: "strictly greater than the configured length and angular tolerance",
      copperLength: "sum of nonzero consecutive same-layer wire-to-wire segments; excludes pours and vertical transitions",
      non45: "deviation from the closest multiple of 45 degrees exceeds the angular tolerance",
      vias: "number of pcb_via records",
      comparisonScope: "routing appearance only; this is not a DRC or electrical-equivalence check",
    },
    reports,
  }, null, 2));
} catch (error) {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
}