mohan-bee/four-gang-wifi-switch-layout

A tscircuit package created by mohan-bee.

Version
0.0.9
License
unset
Stars
0

validate.ts

PCBPCB preview for validate.ts
SchematicSchematic preview for validate.ts
import { readFile, writeFile } from "node:fs/promises"
import { createHash } from "node:crypto"
import { resolve } from "node:path"
import { runAllRoutingChecks, checkSourceTracesMatchPcbTraceThickness } from "@tscircuit/checks"
import { validateOriginalEndpointConnectivity, validateRoutedCopperDrc } from "@tscircuit/fanout-solver"

const folder = resolve(process.argv[2])
const raw = await readFile(`${folder}/circuit.json`)
const circuit = JSON.parse(raw.toString())
const events = JSON.parse(await readFile(`${folder}/phase-events.json`, "utf8"))
const unique = (items, key) => [...new Map(items.map(item=>[item[key],item])).values()]
const traces = unique(events.flatMap(event=>event.simpleRouteJson.traces), "pcb_trace_id")
const connections = unique(events.flatMap(event=>event.simpleRouteJson.connections), "name")
const sourceNets = new Map(circuit.filter(e=>e.type==="source_net").map(e=>[e.source_net_id,e.name]))
const physicalTraces = new Map(circuit.filter(e=>e.type==="pcb_trace").map(e=>[e.pcb_trace_id,e]))
const vias = circuit.filter(e=>e.type==="pcb_via")
const directionalMetadata = []
const compiledDirectionalMetadata = []
for (const trace of physicalTraces.values()) for(let i=0;i<trace.route.length;i++) {
  const p=trace.route[i]
  if(p.route_type!=="via") continue
  const before=trace.route[i-1],after=trace.route[i+1]
  if(!before||!after||before.route_type!=="wire"||after.route_type!=="wire"||before.layer===after.layer||
    [before,after].some(q=>Math.hypot(q.x-p.x,q.y-p.y)>1e-6)) throw new Error("Compiled route has an invalid physical layer transition")
  const span=new Set([before.layer,after.layer])
  if(!span.has(p.from_layer)||!span.has(p.to_layer)||!vias.some(v=>Math.hypot(v.x-p.x,v.y-p.y)<1e-6&&v.layers.length===2&&v.layers.every(l=>span.has(l))&&physicalTraces.get(v.pcb_trace_id)?.connection_name===trace.connection_name)) throw new Error("Compiled layer transition missing its physical via")
  if(p.from_layer!==before.layer||p.to_layer!==after.layer) compiledDirectionalMetadata.push({trace:trace.pcb_trace_id,index:i,raw:[p.from_layer,p.to_layer],wireLayers:[before.layer,after.layer]})
}
let viaReferences = 0
const normalized = structuredClone(traces)
for (const trace of normalized) {
  for (let i=0;i<trace.route.length;i++) {
    const p = trace.route[i]
    if (p.route_type!=="via") continue
    const before=trace.route[i-1], after=trace.route[i+1]
    if (!before || !after || before.route_type!=="wire" || after.route_type!=="wire" || before.layer===after.layer ||
      [before,after].some(q=>Math.hypot(q.x-p.x,q.y-p.y)>1e-6)) throw new Error(`Invalid via transition in ${trace.pcb_trace_id}`)
    const span = new Set([before.layer,after.layer])
    if (!span.has(p.from_layer)||!span.has(p.to_layer)||p.from_layer===p.to_layer) throw new Error("Wrong physical layer span")
    if (!vias.some(v=>Math.hypot(v.x-p.x,v.y-p.y)<1e-6 && v.layers.length===2 && v.layers.every(l=>span.has(l)) &&
      physicalTraces.get(v.pcb_trace_id)?.connection_name===trace.connection_name)) throw new Error("Route transition missing its physical via")
    viaReferences++
    if (p.from_layer!==before.layer||p.to_layer!==after.layer) {
      directionalMetadata.push({trace:trace.pcb_trace_id,index:i,raw:[p.from_layer,p.to_layer],wireLayers:[before.layer,after.layer]})
      p.from_layer=before.layer
      p.to_layer=after.layer
    }
  }
}
const padShapes=new Map(circuit.filter(e=>e.type==="pcb_plated_hole").map(e=>[e.pcb_plated_hole_id,e]))
const obstacles=events[0].simpleRouteJson.obstacles.map(o=>{
  const pad=padShapes.get(o.circuitJsonMetadata?.pcb_plated_hole_id)
  return pad?.shape==="circle"&&Math.abs(o.width-o.height)<1e-6?{...o,shape:"circle"}:o
})
const distanceToPad=(p,pad)=>{
  if(pad.shape==="circle")return Math.max(0,Math.hypot(p.x-pad.center.x,p.y-pad.center.y)-pad.width/2)
  const angle=-(pad.ccwRotationDegrees??0)*Math.PI/180,dx=p.x-pad.center.x,dy=p.y-pad.center.y
  const x=dx*Math.cos(angle)-dy*Math.sin(angle),y=dx*Math.sin(angle)+dy*Math.cos(angle)
  return Math.hypot(Math.max(0,Math.abs(x)-pad.width/2),Math.max(0,Math.abs(y)-pad.height/2))
}
// Audit drills against final physical pads, independently of router events and
// net exemptions. A same-net drill in an SMD pad is still rejected.
const physicalPads=circuit.filter(e=>e.type==="pcb_smtpad"||e.type==="pcb_plated_hole").map(pad=>{
  const id=pad.pcb_smtpad_id??pad.pcb_plated_hole_id
  const common={id,center:{x:pad.x,y:pad.y},layers:pad.layers??[pad.layer]}
  if(pad.shape==="circle"){
    const diameter=pad.type==="pcb_plated_hole"?pad.outer_diameter:2*pad.radius
    return {...common,shape:"circle",width:diameter,height:diameter}
  }
  if(pad.shape==="rect"||pad.shape==="rotated_rect")return {...common,shape:"rect",width:pad.width,height:pad.height,ccwRotationDegrees:pad.ccw_rotation??0}
  if(["pill_hole_with_rect_pad","rotated_pill_hole_with_rect_pad","circular_hole_with_rect_pad"].includes(pad.shape))return {...common,shape:"rect",width:pad.rect_pad_width,height:pad.rect_pad_height,ccwRotationDegrees:pad.rect_ccw_rotation??0}
  throw new Error(`Unsupported physical pad shape in drill audit: ${pad.shape} (${id})`)
})
const viaDrillPadIssues=[]
let viaDrillPadPairs=0
for(const via of vias)for(const pad of physicalPads){
  if(!pad.layers.some(layer=>via.layers.includes(layer)))continue
  viaDrillPadPairs++
  const radius=via.hole_diameter/2,distance=distanceToPad(via,pad)
  if(distance+1e-6<radius)viaDrillPadIssues.push({via:via.pcb_via_id,pad:pad.id,drill_radius_mm:radius,center_distance_to_pad_mm:distance,overlap_mm:radius-distance})
}
const viaDrillPadCheck={valid:!viaDrillPadIssues.length,checked_pairs:viaDrillPadPairs,issues:viaDrillPadIssues}
const base={...events[0].simpleRouteJson,obstacles,connections,traces:normalized}
const copper={...base,obstacles:base.obstacles.filter(o=>o.connectedTo.length>0)}
const keepouts={...base,obstacles:base.obstacles.filter(o=>o.connectedTo.length===0)}
const hvNames=new Set(connections.filter(c=>/^(AC_|OUT)/.test(sourceNets.get(c.name)??"")).map(c=>c.name))
const mains={...copper,connections:connections.filter(c=>hvNames.has(c.name)),traces:normalized.filter(t=>hvNames.has(t.connection_name))}
const rawMetadataReport=validateRoutedCopperDrc({inputSrj:copper,routedSrj:{...copper,traces},clearance:0.2,allowBlindAndBuriedVias:false})
const routedCopper=validateRoutedCopperDrc({inputSrj:copper,routedSrj:copper,clearance:0.2,allowBlindAndBuriedVias:false})
const keepoutCopper=validateRoutedCopperDrc({inputSrj:keepouts,routedSrj:keepouts,clearance:0,allowBlindAndBuriedVias:false})
const mainsCopper=validateRoutedCopperDrc({inputSrj:mains,routedSrj:mains,clearance:1.5,allowBlindAndBuriedVias:false})
const connectivity=validateOriginalEndpointConnectivity({inputSrj:base,routedSrj:base})
const nativeRouting=await runAllRoutingChecks(circuit)
const nativeErrors=nativeRouting.filter(e=>e.type.includes("error"))
const nativeWarnings=nativeRouting.filter(e=>!e.type.includes("error"))
const neckdownWarnings=checkSourceTracesMatchPcbTraceThickness(circuit,{maxPadNeckdownLength:0.5})
const nativeStored=circuit.filter(e=>e.type.includes("error"))
const widths={}
for(const trace of normalized) {
  const net=sourceNets.get(trace.connection_name)??trace.connection_name
  for(let i=0;i<trace.route.length-1;i++){
    const a=trace.route[i],b=trace.route[i+1]
    if(a.route_type==="wire"&&b.route_type==="wire"&&a.layer===b.layer&&Math.hypot(b.x-a.x,b.y-a.y)>1e-9)widths[net]=Math.min(widths[net]??Infinity,a.width)
  }
}
const floorFailures=circuit.filter(e=>e.type==="source_net"&&e.trace_width&&widths[e.name]+1e-6<e.trace_width).map(e=>({net:e.name,required:e.trace_width,actual:widths[e.name]}))
const phaseMinimums=new Map(events.flatMap(e=>e.simpleRouteJson.connections.map(c=>[c.name,e.simpleRouteJson.minTraceWidth])))
const hardWidthViolations=[]
for(const trace of normalized)for(let i=0;i<trace.route.length-1;i++){
  const a=trace.route[i],b=trace.route[i+1],required=phaseMinimums.get(trace.connection_name)
  if(a.route_type!=="wire"||b.route_type!=="wire"||a.layer!==b.layer||required===undefined||Math.hypot(b.x-a.x,b.y-a.y)<=1e-9)continue
  if(a.width+1e-5<required)hardWidthViolations.push({trace:trace.pcb_trace_id,segment:i,required_mm:required,actual_mm:a.width})
}
const terminalNeckdowns=[],widthViolations=[]
const nominal=new Map(circuit.filter(e=>e.type==="source_net").map(e=>[e.source_net_id,e.trace_width]))
for(const trace of normalized)for(let i=0;i<trace.route.length-1;i++){
  const a=trace.route[i],b=trace.route[i+1],target=nominal.get(trace.connection_name)
  if(!target||a.route_type!=="wire"||b.route_type!=="wire"||a.layer!==b.layer||a.width+1e-6>=target)continue
  const pads=obstacles.filter(o=>o.connectedTo.includes(trace.connection_name)&&(o.circuitJsonMetadata?.pcb_smtpad_id||o.circuitJsonMetadata?.pcb_plated_hole_id))
  const length=Math.hypot(b.x-a.x,b.y-a.y),steps=Math.max(1,Math.ceil(length/0.02))
  if(length<=1e-9)continue
  let maximum=0,valid=true
  for(let j=0;j<=steps;j++){
    const t=j/steps
    const p={x:a.x+(b.x-a.x)*t,y:a.y+(b.y-a.y)*t}
    const gap=Math.min(...pads.map(pad=>distanceToPad(p,pad)))
    maximum=Math.max(maximum,gap)
    // Distance-to-union is 1-Lipschitz; this margin covers between samples too.
    if(gap+length/(2*steps)>0.5+1e-6)valid=false
  }
  const item={trace:trace.pcb_trace_id,segment:i,nominal_mm:target,actual_width_mm:a.width,length_mm:length,maximum_centerline_distance_to_same_net_pad_mm:maximum}
  ;(valid?terminalNeckdowns:widthViolations).push(item)
}
const board=circuit.find(e=>e.type==="pcb_board")
const report={
  input_sha256:createHash("sha256").update(raw).digest("hex"),
  board_mm:[board.width,board.height],copper_layers:board.num_layers,
  components:circuit.filter(e=>e.type==="pcb_component").length,
  component_layers:[...new Set(circuit.filter(e=>e.type==="pcb_component").map(e=>e.layer))],
  trace_count:physicalTraces.size,via_count:vias.length,via_references:viaReferences,
  phase_proof:events.map(e=>({name:e.phaseName,router:e.autorouterName,cache:e.cacheStatus,connections:e.connectionCount,previous_traces:e.previousTraceCount,output_traces:e.simpleRouteJson.traces.length})),
  raw_direction_metadata_issues:directionalMetadata,compiled_direction_metadata_issues:compiledDirectionalMetadata,raw_metadata_validator:rawMetadataReport,
  metadata_resolution:"Only via direction labels are normalized for audit after proving physical via coordinates, net and layer span; raw Circuit JSON is unchanged.",
  nativeRouting,nativeErrors,nativeWarnings,nativeStored,routedCopper,keepoutCopper,mainsCopper,connectivity,viaDrillPadCheck,actual_minimum_widths_mm:widths,floorFailures,neckdownWarnings,terminalNeckdowns,nominalWidthRegionsOutsidePads:widthViolations,hardWidthViolations,
  phase_minimum_widths_mm:Object.fromEntries([...phaseMinimums].map(([id,value])=>[sourceNets.get(id)??id,value])),
  width_audit:"Each nonzero same-layer segment uses its outgoing vertex width, checked against its configured phase minimum with 10nm tolerance. Nominal widths are preferences; below-nominal segments and their pad distances are informational.",
  checks_are_not_current_or_mains_compliance_certification:true,
}
await writeFile(`${folder}/validation.json`,JSON.stringify(report,null,2)+"\n")
const valid=!nativeErrors.length&&!nativeStored.length&&routedCopper.valid&&keepoutCopper.valid&&mainsCopper.valid&&connectivity.valid&&viaDrillPadCheck.valid&&!hardWidthViolations.length&&report.component_layers.join()==="top"
console.log(JSON.stringify({valid,sha256:report.input_sha256,traces:report.trace_count,vias:report.via_count,native_errors:nativeErrors.length+nativeStored.length,native_warnings:nativeWarnings.length,copper:routedCopper.issues.length,keepouts:keepoutCopper.issues.length,mains:mainsCopper.issues.length,connectivity:connectivity.valid,via_drill_pad_overlaps:viaDrillPadIssues.length,metadata:directionalMetadata.length,nominal_width_exceptions:floorFailures,neckdown_warnings:neckdownWarnings.length,informational_terminal_neckdowns:terminalNeckdowns.length,width_violations:hardWidthViolations.length},null,2))
if(!valid) process.exitCode=1