0hmX/mt41k512m8da-107-it-p-fanout
This code defines the physical footprint, routing, and layout configuration for a DDR3 memory module using surface-mount pads, vias, and copper traces on a printed circuit board (PCB).
- Version
- 0.2.3
- License
- unset
- Stars
- 0
scripts/validate.ts
import { strict as assert } from 'node:assert'
import { LAYOUT_PROFILES, PROFILE_LAYOUTS, BREAKOUT, PACKAGE_BODY, FANOUT_PADDING_MM, getExitLayer } from '../src/profiles'
import { MATCH_GROUPS, PAIRS, TUNING, PROVISIONAL_STACKUP, getEdgeY } from '../src/timing'
import { PIN_MAP } from '../src/pin-map'
import { getRamFanoutTracePaths } from '../src/saved-paths'
import { mkdirSync } from 'node:fs'
type P={x:number,y:number}
const dist=(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,t=Math.max(0,Math.min(1,((p.x-a.x)*dx+(p.y-a.y)*dy)/(dx*dx+dy*dy)||0));return dist(p,{x:a.x+t*dx,y:a.y+t*dy})}
function segmentDistance(a:P,b:P,c:P,d:P){const cross=(p:P,q:P,r:P)=>(q.x-p.x)*(r.y-p.y)-(q.y-p.y)*(r.x-p.x);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))}
mkdirSync('docs',{recursive:true})
for(const profile of LAYOUT_PROFILES){
const json:any[]=await Bun.file(`dist/${profile}/circuit.json`).json()
const pads=json.filter(x=>x.type==='pcb_smtpad'),vias=json.filter(x=>x.type==='pcb_via'),traces=json.filter(x=>x.type==='pcb_trace'),exits=json.filter(x=>x.type==='pcb_breakout_point')
assert.equal(pads.length,78);assert.equal(vias.length,50);assert.equal(traces.length,72);assert.equal(exits.length,72)
const portOwner=new Map(json.filter(x=>x.type==='pcb_port').map(x=>[x.pcb_port_id,x.source_port_id]))
const traceOwner=new Map(traces.map(x=>[x.pcb_trace_id,portOwner.get(x.connectsTo.find((c:string)=>c.startsWith('pcb_port_')))]))
const lengths:Record<string,number>={};const terminalRoutes:Record<string,any[]>={};const geometry:any[]=[]
for(const pad of pads){
const source=json.find(x=>x.source_port_id===portOwner.get(pad.pcb_port_id)&&x.type==='source_port')
const pin=PIN_MAP.find(x=>source.port_hints.includes(`ball_${x.ball}`))!
assert(pin);assert(dist(pad,pin)<1e-8);assert(Math.abs(pad.radius-pin.radius)<1e-9)
geometry.push({kind:'circle',p:pad,r:pad.radius,layer:'top',owner:source.source_port_id})
}
for(const via of vias)for(const layer of via.layers)geometry.push({kind:'circle',p:via,r:via.outer_diameter/2,layer,owner:traceOwner.get(via.pcb_trace_id)})
for(const trace of traces){
const owner=traceOwner.get(trace.pcb_trace_id);assert(owner)
const source=json.find(x=>x.source_port_id===owner&&x.type==='source_port');const pin=PIN_MAP.find(x=>source.port_hints.includes(`ball_${x.ball}`))!;assert(pin.routed)
const route=trace.route;assert(dist(route[0],pin)<1e-8)
let length=0
for(let i=1;i<route.length;i++){
const a=route[i-1],b=route[i];if(a.route_type==='wire'&&b.route_type==='wire'&&a.layer===b.layer&&dist(a,b)>1e-9){length+=dist(a,b);geometry.push({kind:'segment',a,b,r:Math.max(a.width,b.width)/2,layer:a.layer,owner})}
}
// Prevent mathematical length matching through tiny, sub-trace-width ripples.
for(let i=1;i<route.length;i++){
const a=route[i-1],b=route[i]
if(a.route_type==='wire'&&b.route_type==='wire'&&a.layer===b.layer&&Math.min(Math.abs(a.x),Math.abs(b.x))>=TUNING.startX-1e-8&&dist(a,b)>1e-8)
assert(dist(a,b)>=.12-1e-8,`${profile} ${pin.terminal}: sub-width tuning segment`)
}
// Reject square or sharper bends on EVERY copper layer, including via exits.
const layerSegments=route.slice(1).flatMap((b:any,i:number)=>{
const a=route[i]
return a.route_type==='wire'&&b.route_type==='wire'&&a.layer===b.layer&&dist(a,b)>1e-8?[{a,b}]:[]
})
for(let i=1;i<layerSegments.length;i++){
const before=layerSegments[i-1]!,after=layerSegments[i]!
if(before.b.layer!==after.a.layer||dist(before.b,after.a)>1e-8)continue
const ax=before.b.x-before.a.x,ay=before.b.y-before.a.y,bx=after.b.x-after.a.x,by=after.b.y-after.a.y
assert((ax*bx+ay*by)/(Math.hypot(ax,ay)*Math.hypot(bx,by))>=Math.SQRT1_2-1e-8,`${profile} ${pin.terminal}: ${before.b.layer} bend exceeds 45 degrees`)
}
lengths[pin.terminal]=length;terminalRoutes[pin.terminal]=route
const end=route.at(-1),layer=getExitLayer(pin.ball)
assert.equal(end.route_type,'wire');assert.equal(end.layer,layer)
assert(dist(end,{x:Math.sign(pin.x)*BREAKOUT.halfWidth,y:getEdgeY(pin)})<1e-8)
const exit=exits.find(e=>dist(e,end)<1e-8&&e.layer===layer);assert(exit,`Missing edge terminal ${pin.ball}`);assert.equal(exit.source_port_id,owner)
assert(length>0,`Dead-end escape ${pin.ball}`)
for(const point of route)assert(Math.abs(point.x)<=BREAKOUT.halfWidth+1e-8&&Math.abs(point.y)<=BREAKOUT.halfHeight-.06+1e-8,`Route outside fanout boundary: ${pin.ball}`)
for(let k=1;k<route.length;k++){
const a=route[k-1],b=route[k]
if(a.route_type==='via') {assert(dist(a,b)<1e-8);assert.equal(b.layer,a.to_layer)}
else if(b.route_type==='via') {assert(dist(a,b)<1e-8);assert.equal(a.layer,b.from_layer)}
else assert.equal(a.layer,b.layer,'Unconnected layer transition')
}
const via=vias.find(v=>v.pcb_trace_id===trace.pcb_trace_id)
if(layer==='top') {
assert(!via,`Unnecessary outer-column via ${pin.ball}`)
assert(route.every((p:any)=>p.route_type==='wire'&&p.layer==='top'))
continue
}
assert(via)
const spec=PROFILE_LAYOUTS[profile];const dx=(pin.x<0?-1:1)*(spec.xDirection==='outward'?1:-1)*.4;const dy=spec.yDirection==='north'?.4:-.4
assert(dist(via,{x:pin.x+dx,y:pin.y+dy})<1e-8)
assert.deepEqual([...via.layers].sort(),['top','inner1','inner2','bottom'].sort());assert.equal(via.hole_diameter,.2);assert.equal(via.outer_diameter,.45)
}
let minClearance=Infinity
for(let i=0;i<geometry.length;i++)for(let j=i+1;j<geometry.length;j++){
const a=geometry[i],b=geometry[j];if(a.layer!==b.layer||a.owner===b.owner)continue
const center=a.kind==='circle'?(b.kind==='circle'?dist(a.p,b.p):pointSegment(a.p,b.a,b.b)):(b.kind==='circle'?pointSegment(b.p,a.a,a.b):segmentDistance(a.a,a.b,b.a,b.b))
const clearance=center-a.r-b.r;minClearance=Math.min(minClearance,clearance);assert(clearance>=.1-1e-8,`${profile} clearance ${clearance}`)
}
const groupMetrics=Object.fromEntries(Object.entries(MATCH_GROUPS).map(([name,terminals])=>{
const values=terminals.map(n=>lengths[n]),skew=Math.max(...values)-Math.min(...values)
assert(skew<=TUNING.maxSkewMm,`${profile} ${name} skew ${skew}`)
return [name,{terminals,targetPlanarLengthMm:Math.max(...values),skewMm:skew}]
}))
const pairMetrics=PAIRS.map(([positive,negative])=>{
const a=terminalRoutes[positive!],b=terminalRoutes[negative!],ae=a.at(-1),be=b.at(-1)
assert.equal(ae.layer,be.layer)
assert(Math.abs(Math.abs(ae.y-be.y)-TUNING.pairCenterSpacingMm)<1e-8)
const pairedLength=Math.min(Math.abs(ae.x-a.at(-2).x),Math.abs(be.x-b.at(-2).x))
assert(pairedLength>=TUNING.minPairedTailMm)
let minConvergenceViaClearanceMm=Infinity
for(const route of [a,b]) {
const start=route.at(-3),finish=route.at(-2)
assert(Math.abs(Math.abs(start.x)-TUNING.pairConvergenceStartX)<1e-8)
// The complete convergence segment must be well outside every via annulus.
for(const via of vias){
const clearance=pointSegment(via,start,finish)-via.outer_diameter/2-.06
minConvergenceViaClearanceMm=Math.min(minConvergenceViaClearanceMm,clearance)
assert(clearance>=.5,`${profile}: pair convergence crowds a via`)
}
}
assert.equal(a.at(-2).y,ae.y);assert.equal(b.at(-2).y,be.y)
assert(Math.abs(lengths[positive!]-lengths[negative!])<=TUNING.maxSkewMm)
return {positive,negative,layer:ae.layer,gapMm:TUNING.pairGapMm,pairedTailLengthMm:pairedLength,convergenceStartAbsXmm:TUNING.pairConvergenceStartX,minConvergenceViaClearanceMm,skewMm:Math.abs(lengths[positive!]-lengths[negative!])}
})
const lane=['DQ0','DQ1','DQ2','DQ3','DQ4','DQ5','DQ6','DQ7','DM','DQS_P','DQS_N'].map(n=>lengths[n])
const metrics={profile,paddingMm:FANOUT_PADDING_MM,paddingReference:"component body",packageBodyMm:PACKAGE_BODY,breakoutSizeMm:{width:2*BREAKOUT.halfWidth,height:2*BREAKOUT.halfHeight},stackupAssumptions:PROVISIONAL_STACKUP,matchingBasis:"planar centerline length; layer/via delay and impedance unverified",groupMetrics,pairMetrics,padCount:78,savedTraceCount:72,throughViaCount:50,topOnlyRouteCount:22,unroutedBalls:PIN_MAP.filter(p=>!p.routed).map(p=>p.ball),traceWidthMm:.12,viaCopperMm:.45,viaDrillMm:.2,minForeignCopperClearanceMm:minClearance,minEscapeLengthMm:Math.min(...Object.values(lengths)),maxEscapeLengthMm:Math.max(...Object.values(lengths)),dqDmDqsEscapeSkewMm:Math.max(...lane)-Math.min(...lane),dqsPairEscapeSkewMm:Math.abs(lengths.DQS_P-lengths.DQS_N),clockPairEscapeSkewMm:Math.abs(lengths.CK_P-lengths.CK_N),lengthsMm:lengths}
await Bun.write(`src/generated/${profile}.metrics.json`,JSON.stringify(metrics,null,2)+'\n')
console.log(profile, 'PASS', '72 routes / 50 vias / 22 top-only; minimum clearance',minClearance.toFixed(6),'mm')
}
// Two translated/rotated instances must preserve the saved copper and local nets.
const j:any[]=await Bun.file('dist/tests/two-instances/circuit.json').json()
assert.equal(j.filter(x=>x.type==='pcb_via').length,100)
assert.equal(j.filter(x=>x.type==='pcb_trace'&&x.pcb_trace_id.startsWith('saved_fanout_')).length,144)
const parent=j.filter(x=>x.type==='pcb_trace'&&!x.pcb_trace_id.startsWith('saved_fanout_'));assert(parent.length>0,'Parent route was not emitted')
const exit={x:-10-BREAKOUT.halfWidth,y:PIN_MAP.find(p=>p.signal==='DQ0')!.y}
assert(parent.some(t=>t.route.some((p:any)=>p.route_type==='wire'&&p.layer===getExitLayer(PIN_MAP.find(p=>p.signal==='DQ0')!.ball)&&dist(p,exit)<1e-5)),'Parent route must contact saved bottom-layer exit')
const topExit={x:-10-BREAKOUT.halfWidth,y:PIN_MAP.find(p=>p.signal==='ODT')!.y}
assert(parent.some(t=>t.route.some((p:any)=>p.route_type==='wire'&&p.layer==='top'&&dist(p,topExit)<1e-5)),'Host must continue from the top-layer ODT edge exit')
const groups=j.filter(x=>x.type==='source_group');const ram1=groups.find(x=>x.name==='RAM1');assert(ram1)
let rotatedPadCount=0;const pads=j.filter(x=>x.type==='pcb_smtpad');for(const pad of pads){const c=j.find(x=>x.type==='pcb_component'&&x.pcb_component_id===pad.pcb_component_id);const sc=j.find(x=>x.type==='source_component'&&x.source_component_id===c.source_component_id);if(c.subcircuit_id!==ram1.subcircuit_id)continue;rotatedPadCount++;const port=j.find(x=>x.type==='pcb_port'&&x.pcb_port_id===pad.pcb_port_id);const sp=j.find(x=>x.type==='source_port'&&x.source_port_id===port.source_port_id);const pin=PIN_MAP.find(p=>sp.port_hints.includes(`ball_${p.ball}`))!;assert(dist(pad,{x:10-pin.y,y:pin.x})<1e-7)}
assert.equal(rotatedPadCount,78);
console.log('Two-instance rotation + parent continuation: PASS')
// The rotated instance must preserve every tuned bend and pair tail, not only pads.
const rotatedCache=getRamFanoutTracePaths('inward_south')
let checkedRotatedPaths=0
for(const trace of j.filter(x=>x.type==='pcb_trace'&&x.pcb_trace_id.startsWith('saved_fanout_'))){
const port=j.find(x=>x.type==='pcb_port'&&trace.connectsTo.includes(x.pcb_port_id))
const component=j.find(x=>x.type==='pcb_component'&&x.pcb_component_id===port.pcb_component_id)
if(component.subcircuit_id!==ram1.subcircuit_id)continue
const source=j.find(x=>x.type==='source_port'&&x.source_port_id===port.source_port_id)
const pin=PIN_MAP.find(p=>source.port_hints.includes(`ball_${p.ball}`))!
const cached=rotatedCache.find(p=>p.connection===`U1.ball_${pin.ball}`)!
assert.equal(trace.route.length,cached.route.length)
cached.route.forEach((p,i)=>{
const emitted=trace.route[i]
assert(dist(emitted,{x:10-Number(p.y),y:Number(p.x)})<1e-7)
assert.equal(emitted.route_type,p.route_type)
if(p.route_type==='wire')assert.equal(emitted.layer,p.layer)
})
checkedRotatedPaths++
}
assert.equal(checkedRotatedPaths,72)
console.log('Rotated tuning cells and differential tails: PASS')