0hmX/am3352-ddr-track-a-local-dfm-wip-20261007

Experimental RJ45-corrected board with isolated PG indication. 1005/1005 source joins pass; 15 expanded-guard physical errors, sourcing and qualification gates remain. Not fabrication-ready.

Version
0.0.14
License
unset
Stars
0

scripts/verify-ddr-candidate.tsx

/** Compile solver candidates against the real board. Run from the repository root:
 * bun scripts/verify-ddr-candidate.tsx work/ddr/experiments/{top,inner2,inner4,bottom}.json
 * This is a necessary geometry screen, not manufacturing or signal-integrity approval.
 */
import { Circuit } from '@tscircuit/core'
import * as checks from '@tscircuit/checks'
import { getFullConnectivityMapFromCircuitJson } from 'circuit-json-to-connectivity-map'
import { Children, cloneElement, createElement } from 'react'
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import Board from '../index'
import { ProvisionalDdrRoutes, provisionalDqs1Routes } from '../layout/ProvisionalDdrRoutes'
import { ProvisionalDdrAddressRoutes, provisionalAddressRoutes } from '../layout/ProvisionalDdrAddressRoutes'
import { ProvisionalDdrByte0Routes, provisionalByte0Routes } from '../layout/ProvisionalDdrByte0Routes'
import { ProvisionalDdrByte1Routes, provisionalByte1Routes } from '../layout/ProvisionalDdrByte1Routes'
import { ddrRoutes } from '../data/ddr-routes'
import { checkMeanderGap } from './check-meander-gap'

type Point = { x: number; y: number }
const output = 'work/ddr/candidate-verification'
// Configurable trial region; these dimensions are not a fabrication requirement.
const keepout = { minX: -2, maxX: 2, minY: -19, maxY: -12 }
const width = 0.1, epsilon = 1e-5
const viaDimensions = { holeDiameterMm: 0.20, outerDiameterMm: 0.30 }
// Existing connector body overhangs: do not silently excuse new/worsened extents.
const baselineOverhangs = {
  J_MICROSD: { min_x: -28.28, max_x: -10.58, min_y: -21.38, max_y: -5.68 },
  J_HDMI: { min_x: 59.305, max_x: 73.155, min_y: -37, max_y: -20 },
}
const informational: unknown[] = []
const distance = (a: Point, b: Point) => Math.hypot(a.x - b.x, a.y - b.y)
// Ramer–Douglas–Peucker simplification, matching the exploratory audit tolerance.
function simplify(points: Point[], tolerance=0.03): Point[] {
  if (points.length<3) return points
  const a=points[0], b=points.at(-1)!, dx=b.x-a.x, dy=b.y-a.y, norm=dx*dx+dy*dy
  let maximum=0, index=0
  for (let i=1;i<points.length-1;i++) {
    const p=points[i], t=Math.max(0,Math.min(1,((p.x-a.x)*dx+(p.y-a.y)*dy)/(norm||1)))
    const deviation=Math.hypot(p.x-a.x-t*dx,p.y-a.y-t*dy)
    if (deviation>maximum) { maximum=deviation; index=i }
  }
  return maximum>tolerance ? [...simplify(points.slice(0,index+1),tolerance).slice(0,-1),...simplify(points.slice(index),tolerance)] : [a,b]
}
// Slab clipping tests complete segments, including a crossing with both ends outside.
function hitsKeepout(a: Point, b: Point) {
  let lo = 0, hi = 1
  for (const [p, delta, min, max] of [[a.x, b.x-a.x, keepout.minX-width/2, keepout.maxX+width/2], [a.y, b.y-a.y, keepout.minY-width/2, keepout.maxY+width/2]]) {
    if (Math.abs(delta) < 1e-12) { if (p < min || p > max) return false }
    else { const ts = [(min-p)/delta, (max-p)/delta].sort((x,y)=>x-y); lo=Math.max(lo,ts[0]); hi=Math.min(hi,ts[1]) }
  }
  return lo <= hi
}
const failures: { check: string; detail: unknown }[] = []
const fail = (check: string, detail: unknown) => failures.push({check, detail})
mkdirSync(output, {recursive:true})
try {
  const arguments_ = process.argv.slice(2), requireSeparateLayers = arguments_.includes('--separate-layers')
  const paths = arguments_.filter(arg => arg !== '--separate-layers')
  if (!paths.length) throw Error('Supply --mounted or solver JSON files containing traces; all DDR signals are required')
  const mounted = paths.length === 1 && paths[0] === '--mounted'
  const sourceRoutes = [...provisionalAddressRoutes, ...provisionalByte0Routes, ...provisionalByte1Routes, ...provisionalDqs1Routes.map(route => ({...route, layer:'bottom' as const}))]
  const candidates = mounted ? sourceRoutes.map(({name,layer,points})=>({connection_name:name,route:points.map(([x,y])=>({route_type:'wire',x,y,layer,width:0.1}))})) : paths.flatMap(path => {
    const value = JSON.parse(readFileSync(path, 'utf8'))
    if (!Array.isArray(value.traces)) throw Error(`${path}: expected traces array`)
    return value.traces
  })
  const seen = new Set<string>()
  for (const candidate of candidates) {
    const name = candidate.connection_name
    if (!ddrRoutes.some(r=>r.name===name) || seen.has(name)) throw Error(`Unknown or duplicate route: ${name}`)
    seen.add(name)
    if (!Array.isArray(candidate.route) || candidate.route.length < 2) throw Error(`${name}: incomplete geometry`)
    const layer = candidate.route[0].layer
    if (!['top','inner1','inner2','inner3','inner4','inner5','inner6','bottom'].includes(layer)) throw Error(`${name}: invalid layer`)
    if (candidate.route.some(p=>p.route_type!=='wire' || p.layer!==layer || !Number.isFinite(p.x) || !Number.isFinite(p.y) || Math.abs(p.width-width)>epsilon || !Number.isFinite(p.width)))
      throw Error(`${name}: requires finite, uniform 0.10 mm single-layer wire geometry; explicit vias/multilayer candidates are unsupported`)
  }
  const missing = ddrRoutes.filter(r=>!seen.has(r.name)).map(r=>r.name)
  if (missing.length) fail('complete DDR coverage', missing)
  const circuit = new Circuit({platform:{partsEngineDisabled:true,netlistDrcChecksDisabled:true}})
  circuit.schematicDisabled = true
  const board = Board(), children = Children.toArray(board.props.children).filter((child:any)=>![ProvisionalDdrRoutes, ProvisionalDdrAddressRoutes, ProvisionalDdrByte0Routes, ProvisionalDdrByte1Routes].includes(child.type))
  for (const candidate of candidates) {
    const meta = ddrRoutes.find(r=>r.name===candidate.connection_name)!
    const from = meta.from.replace('.U1', '.Processor > .U1'), to = meta.to.replace('.U3', '.DDR3 > .U3')
    const points = candidate.route.map(({x,y})=>({x,y})), layer = candidate.route[0].layer
    const pcbPath = layer==='top' ? [from,...points,to] : [from,{...points[0],via:true,fromLayer:'top',toLayer:layer},...points,{...points.at(-1),via:true,fromLayer:layer,toLayer:'top'},to]
    children.push(createElement('trace',{key:meta.name,name:meta.name,from,to,width,pcbPath}))
  }
  circuit.add(cloneElement(board,{fabricatorPreset:'jlcpcb_standard'},children))
  await circuit.renderUntilSettled()
  const records = circuit.getCircuitJson() as any[]
  writeFileSync(`${output}/board.circuit.json`,JSON.stringify(records))
  const renderErrors = records.filter(r=>r.type.endsWith('_error'))
  if (renderErrors.length) fail('native render',{count:renderErrors.length,examples:renderErrors.slice(0,10)})
  const nativeChecks = {}
  for (const name of ['checkTracesAreContiguous','checkEachPcbTraceNonOverlapping','checkPadTraceClearance','checkViaTraceClearance','checkViaPadClearance','checkDifferentNetViaSpacing','checkPcbTracesOutOfBoard','checkHoleTraceClearance','checkSameNetViaSpacing','checkCopperToBoardEdgeClearance','checkViasOffBoard','checkPadPadClearance','checkCopperPourShorts','checkPcbTraceSelfShorts','checkDanglingTraces','checkSourceTracesMatchPcbTraceThickness','checkPcbComponentOverlap','checkPcbComponentsOutOfBoard']) {
    let result = await checks[name](structuredClone(records))
    nativeChecks[name] = result.length
    if (name==='checkPcbComponentsOutOfBoard') result=result.filter(error=>{
      const componentName=records.find(r=>r.type==='source_component' && r.source_component_id===error.source_component_id)?.name
      const baseline=baselineOverhangs[componentName], bounds=error.component_bounds
      if (!baseline || !bounds || !Object.keys(baseline).every(key=>Number.isFinite(bounds[key]) && (key.startsWith('min') ? bounds[key]>=baseline[key]-epsilon : bounds[key]<=baseline[key]+epsilon))) return true
      informational.push({check:name,componentName,classification:'unchanged or reduced baseline connector body overhang',baselineBounds:baseline,actualBounds:bounds,message:error.message})
      return false
    })
    if (result.length) fail(name,{count:result.length,examples:result.slice(0,10)})
  }
  // Nominal fabricator preset, not a fabrication-confirmed stackup.
  const z = new Map<string,number>()
  let depth = 0
  for (const layer of records.find(r=>r.type==='pcb_stackup')?.layers ?? []) {
    if (layer.layer_type==='copper') z.set(layer.layer,depth+layer.thickness/2)
    depth+=layer.thickness
  }
  const logical = getFullConnectivityMapFromCircuitJson(records)
  function port(selector: string) {
    const [componentName, ball] = selector.split(' > ').map(s=>s.slice(1))
    const component = records.find(r=>r.type==='source_component' && r.name===componentName)
    const ports = records.filter(r=>r.type==='source_port' && r.source_component_id===component?.source_component_id && (r.name===ball || r.port_hints?.includes(ball)))
    if (ports.length!==1) throw Error(`Cannot uniquely resolve ${selector}`)
    const pcb = records.filter(r=>r.type==='pcb_port' && r.source_port_id===ports[0].source_port_id)
    if (pcb.length!==1) throw Error(`No unique physical port for ${selector}`)
    return pcb[0]
  }
  // Reserve the other DDR signals' planned via-in-pad lands as obstacles too.
  // The mounted board may contain only a subset of these vias. Candidates can
  // override a saved signal layer; all other assignments remain provisional.
  const copperLayers=['top','inner1','inner2','inner3','inner4','inner5','inner6','bottom']
  const plannedVias=ddrRoutes.flatMap(meta=>{
    const layer=candidates.find(c=>c.connection_name===meta.name)?.route[0]?.layer ?? meta.layer
    return layer==='top' ? [] : [port(meta.from),port(meta.to)].map(p=>({name:meta.name,layer,center:{x:p.x,y:p.y}}))
  })
  const pointSegmentDistance=(p:Point,a:Point,b:Point)=>{
    const dx=b.x-a.x,dy=b.y-a.y,den=dx*dx+dy*dy
    const t=Math.max(0,Math.min(1,((p.x-a.x)*dx+(p.y-a.y)*dy)/(den||1)))
    return Math.hypot(p.x-a.x-t*dx,p.y-a.y-t*dy)
  }
  const routes = candidates.map(candidate=>{
    const name=candidate.connection_name, meta=ddrRoutes.find(r=>r.name===name)!, layer=candidate.route[0].layer
    const from=port(meta.from), to=port(meta.to)
    if (!logical.areIdsConnected(from.source_port_id,to.source_port_id)) fail('cross-scope logical connectivity',{name})
    const traces=records.filter(r=>r.type==='pcb_trace' && r.route[0]?.start_pcb_port_id===from.pcb_port_id && r.route.at(-1)?.end_pcb_port_id===to.pcb_port_id)
    if (traces.length!==1) throw Error(`${name}: no unique native trace connecting expected physical pins`)
    const trace=traces[0], points=candidate.route.filter((p,i,a)=>!i || distance(p,a[i-1])>epsilon)
    if (points.length<2) throw Error(`${name}: degenerate route`)
    if (distance(points[0],from)>epsilon || distance(points.at(-1),to)>epsilon) fail('endpoints',{name,from,to})
    const vias=records.filter(r=>r.type==='pcb_via' && r.pcb_trace_id===trace.pcb_trace_id)
    if (vias.length!==(layer==='top'?0:2) || (layer!=='top' && [from,to].some(p=>!vias.some(v=>distance(p,v)<epsilon && v.layers.includes('top') && v.layers.includes(layer))))) fail('via-in-pad',{name,vias})
    if (vias.some(v=>!Number.isFinite(v.hole_diameter) || !Number.isFinite(v.outer_diameter) || Math.abs(v.hole_diameter-viaDimensions.holeDiameterMm)>epsilon || Math.abs(v.outer_diameter-viaDimensions.outerDiameterMm)>epsilon)) fail('via-in-pad dimensions',{name,expected:viaDimensions,actual:vias.map(v=>({holeDiameterMm:v.hole_diameter,outerDiameterMm:v.outer_diameter}))})
    let bends=0, maxTurnDegrees=0
    for (let i=1;i<points.length;i++) {
      if (hitsKeepout(points[i-1],points[i])) { fail('4x7 mm center keepout',{name,segment:i-1}); break }
    }
    for (const via of plannedVias) {
      if (via.name===name || copperLayers.indexOf(layer)>copperLayers.indexOf(via.layer)) continue
      const clearance=Math.min(...points.slice(1).map((p,i)=>pointSegmentDistance(via.center,points[i],p)))-(width+viaDimensions.outerDiameterMm)/2
      if (clearance<0.1-epsilon) fail('planned DDR via clearance',{name,via:via.name,center:via.center,clearanceMm:clearance,minimumMm:0.1})
    }
    const planarMm=points.slice(1).reduce((sum,p,i)=>sum+distance(p,points[i]),0)
    for (let i=1;i<points.length-1;i++) {
      const a=points[i-1], b=points[i], c=points[i+1]
      const ux=b.x-a.x, uy=b.y-a.y, vx=c.x-b.x, vy=c.y-b.y
      maxTurnDegrees=Math.max(maxTurnDegrees,Math.abs(Math.atan2(ux*vy-uy*vx,ux*vx+uy*vy))*180/Math.PI)
    }
    if (maxTurnDegrees>30+1e-4) fail('maximum 30-degree direction change',{name,maxTurnDegrees})
    // RDP + 10-degree turns is a geometric heuristic. Consecutive qualifying
    // turns in the same direction form one turn run, so a sampled rounded corner
    // does not inflate the count at the sampling scale. Turn runs separated by more
    // than 1 mm of path length count separately. This can undercount nearby real
    // corners or split a large rounded corner: it is a screen, not certification.
    const simplified=simplify(points)
    let previousDirection=0, previousTurnArc=-Infinity, arc=0
    for (let i=1;i<simplified.length-1;i++) {
      const a=simplified[i-1], b=simplified[i], c=simplified[i+1]
      arc+=distance(a,b)
      const ux=b.x-a.x, uy=b.y-a.y, vx=c.x-b.x, vy=c.y-b.y
      const cross=ux*vy-uy*vx, angle=Math.atan2(Math.abs(cross),ux*vx+uy*vy)
      if (angle<Math.PI/18) continue
      const direction=Math.sign(cross) || 1
      if (direction!==previousDirection || arc-previousTurnArc>1) bends++
      previousDirection=direction
      previousTurnArc=arc
    }
    if (bends<2) fail('at least two distinct turn runs',{name,bends})
    const returnGap=checkMeanderGap(points.map(p=>[p.x,p.y]),width,0.20)
    if (!returnGap.ok) fail('0.20 mm opposing return-leg gap',{name,...returnGap})
    const viaMm = layer==='top' ? 0 : z.has('top') && z.has(layer) ? 2*Math.abs(z.get('top')!-z.get(layer)!) : null
    if (viaMm===null) fail('missing nominal via depth',{name,layer})
    return {name,layer,planarMm,viaMm,totalMm:viaMm===null?null:planarMm+viaMm,bends,maxTurnDegrees,returnGap}
  })
  const groups = {
    BYTE0:[...Array.from({length:8},(_,i)=>`DDR_D${i}`),'DDR_DQM0','DDR_DQS0','DDR_DQSn0'],
    BYTE1:[...Array.from({length:8},(_,i)=>`DDR_D${i+8}`),'DDR_DQM1','DDR_DQS1','DDR_DQSn1'],
    ADDRESS:ddrRoutes.map(r=>r.name).filter(n=>!/^DDR_D/.test(n)),
    DQS0:['DDR_DQS0','DDR_DQSn0'],DQS1:['DDR_DQS1','DDR_DQSn1'],CK:['DDR_CK','DDR_CKn'],
  }
  const layerAssignments = (['BYTE0','BYTE1','ADDRESS'] as const).map(group=>{
    const names=groups[group], members=routes.filter(route=>names.includes(route.name))
    return {group,routed:members.length,expected:names.length,layers:[...new Set(members.map(route=>route.layer))].sort()}
  })
  const sharedLayers = layerAssignments.flatMap((a,i)=>layerAssignments.slice(i+1).flatMap(b=>a.layers.filter(layer=>b.layers.includes(layer)).map(layer=>({layer,groups:[a.group,b.group]}))))
  if (requireSeparateLayers && (layerAssignments.some(g=>g.routed!==g.expected || g.layers.length!==1) || sharedLayers.length)) fail('one dedicated layer per complete DDR group',{layerAssignments,sharedLayers})
  const skew = Object.entries(groups).map(([group,names])=>{
    const rows=routes.filter(r=>names.includes(r.name)), limitMm=['DQS0','DQS1','CK'].includes(group)?0.127:0.635
    const complete=rows.length===names.length
    const observedPlanarSkewMm=rows.length>1 ? Math.max(...rows.map(r=>r.planarMm))-Math.min(...rows.map(r=>r.planarMm)) : null
    const observedNominalTotalSkewMm=rows.length>1 && rows.every(r=>r.totalMm!==null) ? Math.max(...rows.map(r=>r.totalMm!))-Math.min(...rows.map(r=>r.totalMm!)) : null
    const planarSkewMm=complete ? observedPlanarSkewMm : null
    const nominalTotalSkewMm=complete ? observedNominalTotalSkewMm : null
    const result={group,complete,routed:rows.length,expected:names.length,planarSkewMm,nominalTotalSkewMm,observedPlanarSkewMm,observedNominalTotalSkewMm,limitMm}
    if (nominalTotalSkewMm===null || nominalTotalSkewMm>limitMm+epsilon) fail('nominal total length skew',result)
    return result
  })
  const report={status:failures.length?'FAIL':'INCOMPLETE',qualified:false,electricalSignoff:false,scope:'Necessary geometry screening only; never complete electrical or fabrication signoff',keepout,widthMm:width,viaDimensions,nativeChecks,informational,routes,skew,layerAssignments,sharedLayers,failures,
    bendHeuristic:'0.03 mm RDP; >=10 degree turns; same-direction turns <=1 mm apart along simplified path count as one run; may undercount close corners or split large rounded corners',
    unverified:['TI inter-net DDR spacing by signal class (including 3w/4w center-to-center rules); native overlap and clearance checks do not enforce these rules','Differential impedance and pair coupling, including separation consistency and uncoupled lengths','External-facing meander orientation','Fabrication-confirmed stackup (total lengths include nominal jlcpcb_standard copper-center via depths only)','Return-leg gap screen covers opposing sampled chords, not arbitrary curve geometry','Reference-plane continuity, impedance and signal integrity']}
  writeFileSync(`${output}/report.json`,JSON.stringify(report,null,2))
  console.log(JSON.stringify({status:report.status,routes:routes.length,nativeChecks,failures:failures.length,report:`${output}/report.json`},null,2))
  // Fail closed: even geometry with no detected violations is not fully qualified.
  process.exitCode=1
} catch (error) {
  writeFileSync(`${output}/report.json`,JSON.stringify({status:'FAIL',qualified:false,electricalSignoff:false,scope:'Incomplete geometry screening; no electrical or fabrication signoff',failures:[...failures,{check:'incomplete or unsupported geometry',detail:String(error)}]},null,2))
  console.error(error)
  process.exitCode=1
}