0hmX/am3352

This code suite comprises TypeScript scripts that analyze, verify, and assemble complex DDR memory interface hardware, focusing on physical routing, via and pad placement, electrical clearance, and physical constraints, often involving precise geometric calculations and consistent provenance tracking.

Version
1.0.5
License
unset
Stars
0

scripts/plan-ddr-ram-decoupling.ts

/** Candidate placement only: local capacitor copper is checked, but power-plane
 * continuity, impedance and effective capacitance are separate acceptance gates. */
import React from 'react'
import assert from 'node:assert/strict'
import {createHash} from 'node:crypto'
import {auditRouteAngles} from './check-route-angles'
import {planDdrGroundAntipads} from './ddr-ground-plane-antipads'
import {RootCircuit}from'@tscircuit/core'
import{readFileSync,writeFileSync,mkdirSync,existsSync}from'node:fs'
import{resolve}from'node:path'
import{verifyDdrCapture}from'./ddr-capture-provenance'
type P={x:number;y:number};type S={a:P;b:P;r:number;layer:string;owner:string;rect?:{minX:number;maxX:number;minY:number;maxY:number}}
const distance=(a:P,b:P)=>Math.hypot(a.x-b.x,a.y-b.y)
function ps(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 distance(p,{x:a.x+t*dx,y:a.y+t*dy})}
function ss(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(ps(a,c,d),ps(b,c,d),ps(c,a,b),ps(d,a,b))}
function sr(a:P,b:P,r:NonNullable<S['rect']>){const inside=(p:P)=>p.x>=r.minX&&p.x<=r.maxX&&p.y>=r.minY&&p.y<=r.maxY;if(inside(a)||inside(b))return 0;const p=[{x:r.minX,y:r.minY},{x:r.maxX,y:r.minY},{x:r.maxX,y:r.maxY},{x:r.minX,y:r.maxY}];return Math.min(...p.map((v,i)=>ss(a,b,v,p[(i+1)%4]!)))}
const gap=(a:S,b:S)=>a.rect&&b.rect?Math.hypot(Math.max(0,a.rect.minX-b.rect.maxX,b.rect.minX-a.rect.maxX),Math.max(0,a.rect.minY-b.rect.maxY,b.rect.minY-a.rect.maxY)):(a.rect?sr(b.a,b.b,a.rect):b.rect?sr(a.a,a.b,b.rect):ss(a.a,a.b,b.a,b.b))-a.r-b.r
const layers=['top',...Array.from({length:8},(_,i)=>`inner${i+1}`),'bottom'],clearance=.1016
function shapes(json:any[]):S[]{const out:S[]=[];for(const e of json){if(e.type==='pcb_trace'){for(let i=1;i<e.route.length;i++){const a=e.route[i-1],b=e.route[i];if(a.route_type==='wire'&&b.route_type==='wire'&&a.layer===b.layer)out.push({a,b,r:Math.max(a.width,b.width)/2,layer:a.layer,owner:e.pcb_trace_id})}}
 if(e.type==='pcb_via')for(const layer of layers)out.push({a:e,b:e,r:e.outer_diameter/2,layer,owner:e.pcb_via_id})
 if(e.type==='pcb_smtpad'){if(e.shape==='circle')out.push({a:e,b:e,r:e.radius,layer:e.layer,owner:e.pcb_smtpad_id});else if(e.width&&e.height&&(!e.ccw_rotation||e.ccw_rotation%90===0)){const swap=e.ccw_rotation&&e.ccw_rotation%180!==0,w=swap?e.height:e.width,h=swap?e.width:e.height;out.push({a:e,b:e,r:0,layer:e.layer,owner:e.pcb_smtpad_id,rect:{minX:e.x-w/2,maxX:e.x+w/2,minY:e.y-h/2,maxY:e.y+h/2}})}else throw Error(`Unsupported pad ${e.shape}`)}}return out}
const dir=process.argv[2]??'dist/ddr-system/host-taps-54af3e346b5e',outDir=process.argv[3]??'dist/ddr-ram-decoupling-plan'
const {captureHash,snapshot}=verifyDdrCapture(dir),original=JSON.parse(readFileSync(resolve(dir,'unrouted.circuit.json'),'utf8')),fixed=shapes(original)
const originalCopperHash=createHash('sha256').update(JSON.stringify(original)).digest('hex')
mkdirSync(outDir,{recursive:true})
async function template(footprint:string){const c=new RootCircuit();c.add(React.createElement('board',{width:10,height:10,layers:10,schematicDisabled:true,routingDisabled:true},React.createElement('capacitor',{name:'C',capacitance:'100nF',footprint,layer:'bottom',pcbX:0,pcbY:0})));await c.renderUntilSettled();return c.getCircuitJson().filter((e:any)=>e.type==='pcb_smtpad')as any[]}
const small=await template('cap0201_nosilkscreen'),bulk=await template('cap0805_nosilkscreen')
const reservedPath=resolve(dir,'dqs-tuning-final/routes.json');if(existsSync(reservedPath)){const host=JSON.parse(readFileSync(reservedPath,'utf8'));const rows=Array.isArray(host)?host:host.traces;fixed.push(...shapes(rows.flatMap((t:any)=>[t,...t.route.filter((p:any)=>p.route_type==='via').map((v:any,i:number)=>({type:'pcb_via',pcb_via_id:`reserved_${t.pcb_trace_id}_${i}`,x:v.x,y:v.y,outer_diameter:v.via_diameter,layers}))])))}
const planes=original.filter((p:any)=>p.type==='pcb_copper_pour')
const planeCandidates=structuredClone(planes),antipadRadius=(.4572/2+clearance)/Math.cos(Math.PI/64),minPlaneWeb=.1524
const inside=(p:P,vs:P[])=>{let yes=false;for(let i=0,j=vs.length-1;i<vs.length;j=i++){const a=vs[i]!,b=vs[j]!;if((a.y>p.y)!==(b.y>p.y)&&p.x<(b.x-a.x)*(p.y-a.y)/(b.y-a.y)+a.x)yes=!yes}return yes}
const ringDistance=(p:P,vs:P[])=>Math.min(...vs.map((a,i)=>ps(p,a,vs[(i+1)%vs.length]!)))
function planAntipads(vias:any[]){const added:any[]=[]
 for(const [index,plane]of planeCandidates.entries()){
  const shape=plane.brep_shape;if(plane.shape!=='brep'||!shape)throw Error('Unsupported reference plane')
  for(const via of vias){if(via.net==='GND')continue
   const outer=shape.outer_ring.vertices,holes=shape.inner_rings??[],inOuter=inside(via,outer),distOuter=ringDistance(via,outer)
   const containing=holes.find((h:any)=>inside(via,h.vertices))
   if(!inOuter){if(distOuter<antipadRadius)return null;continue}
   if(containing){if(ringDistance(via,containing.vertices)<antipadRadius)return null;continue}
   if(distOuter<antipadRadius+minPlaneWeb||holes.some((h:any)=>ringDistance(via,h.vertices)<antipadRadius+minPlaneWeb))return null
   const vertices=Array.from({length:64},(_,i)=>({x:via.x+antipadRadius*Math.cos(i*Math.PI/32),y:via.y+antipadRadius*Math.sin(i*Math.PI/32)}))
   added.push({planeIndex:index,hole:{vertices},center:{x:via.x,y:via.y},radiusMm:antipadRadius})
  }
 }
 return added
}
const plans:any[]=[],accepted:S[]=[],bodies:Array<{x:number;y:number;w:number;h:number}>=[]
const make=(name:string,x:number,y:number,pads:any[],offset:number,rotation:number)=>{
 const angle=rotation*Math.PI/180,cos=Math.cos(angle),sin=Math.sin(angle),geometry:S[]=[],vias:any[]=[],routes:any[]=[],newPads=pads.map((p:any)=>({...p,x:p.x*cos-p.y*sin+x,y:p.x*sin+p.y*cos+y,width:rotation%180?p.height:p.width,height:rotation%180?p.width:p.height,pcb_smtpad_id:name+'_'+p.pcb_smtpad_id,owner:name}))
 for(const [i,p]of newPads.entries()){
  const sign=Math.sign(pads[i]!.x),v={x:x+sign*offset*cos,y:y+sign*offset*sin,diameter:.4572,drill:.254,net:i===0?'DDR_1V5':'GND'};vias.push(v)
  const owner=name+'_'+i
  geometry.push(...shapes([{...p,type:'pcb_smtpad'}]).map(s=>({...s,owner})))
  for(const layer of layers)geometry.push({a:v,b:v,r:.4572/2,layer,owner})
  geometry.push({a:p,b:v,r:.1016/2,layer:'bottom',owner});routes.push({from:{x:p.x,y:p.y},to:v,layer:'bottom',width:.1016})
 }
 return {geometry,vias,routes,pads:newPads}
}
for(const placement of snapshot.placements){const ram=placement.name,paths=snapshot.pathsByByte[ram==='RAM0'?0:1],terms=snapshot.terminalsByByte[ram==='RAM0'?0:1],balls=terms.filter((t:any)=>/^V(?:DD|SS)Q?_/.test(t.terminal)).map((t:any)=>{const p=paths.find((p:any)=>p.connection===`U1.ball_${t.ball}`).route[0];const a=placement.pcbRotation*Math.PI/180;return {ball:t.ball,terminal:t.terminal,x:placement.pcbX+p.x*Math.cos(a)-p.y*Math.sin(a),y:placement.pcbY+p.x*Math.sin(a)+p.y*Math.cos(a)}}),power=balls.filter((p:any)=>/^VDD/.test(p.terminal)),ground=balls.filter((p:any)=>/^VSS/.test(p.terminal))
 const localFixed=fixed.filter(s=>Math.max(s.a.x,s.b.x,s.rect?.maxX??-Infinity)>=placement.pcbX-7.5&&Math.min(s.a.x,s.b.x,s.rect?.minX??Infinity)<=placement.pcbX+7.5&&Math.max(s.a.y,s.b.y,s.rect?.maxY??-Infinity)>=placement.pcbY-11&&Math.min(s.a.y,s.b.y,s.rect?.minY??Infinity)<=placement.pcbY+11)
 for(const kind of ['HS','BULK']){const count=kind==='HS'?12:2,pads=kind==='HS'?small:bulk,offset=kind==='HS'?.9:1.65,bodyW=kind==='HS'?1.4:3.36,bodyH=kind==='HS'?.7:1.9
  for(let i=0;i<count;i++){
   const candidates:any[]=[]
   for(let xi=-40;xi<=40;xi++)for(let yi=-(kind==='HS'?56:80);yi<=(kind==='HS'?56:80);yi++){
    const x=placement.pcbX+xi*.1,y=placement.pcbY+yi*.1
    for(const rotation of [0,90,180,270]){const angle=rotation*Math.PI/180,cw=rotation%180?bodyH:bodyW,ch=rotation%180?bodyW:bodyH
    if(kind==='HS'&&Math.abs(x-placement.pcbX)>3.8)continue
    const near=power.map((p:any)=>({...p,distance:Math.hypot(p.x-x,p.y-y)})).sort((a:any,b:any)=>a.distance-b.distance)[0]
    const nearGround=ground.map((p:any)=>({...p,distance:Math.hypot(p.x-x,p.y-y)})).sort((a:any,b:any)=>a.distance-b.distance)[0]
    const viaPower=power.map((p:any)=>({...p,distance:Math.hypot(p.x-(x+offset*Math.cos(angle)),p.y-(y+offset*Math.sin(angle)))})).sort((a:any,b:any)=>a.distance-b.distance)[0]
    if(kind==='HS'&&(near.distance>3.81||nearGround.distance>3.81))continue
    if(bodies.some(b=>Math.abs(x-b.x)<(cw+b.w)/2+.05&&Math.abs(y-b.y)<(ch+b.h)/2+.05))continue
    candidates.push({x,y,rotation,cw,ch,near,nearGround,viaPower,score:Math.abs(x-placement.pcbX)*.2+Math.abs(y-placement.pcbY)*.04+(kind==='HS'?0:Math.abs(Math.abs(x-placement.pcbX)-3)*.3)})
    }
   }
   candidates.sort((a,b)=>a.score-b.score);let chosen:any
   for(const c of candidates){const candidate=make(`${ram}_${kind}${i+1}`,c.x,c.y,pads,offset,c.rotation);let minGap=Infinity,valid=true
    const all=[...localFixed,...accepted];for(const s of candidate.geometry){for(const b of all){if(s.layer!==b.layer)continue;const g=gap(s,b);minGap=Math.min(minGap,g);if(g<clearance-1e-8){valid=false;break}}if(!valid)break}
    if(valid){for(let a=0;a<candidate.geometry.length;a++)for(let b=a+1;b<candidate.geometry.length;b++){const u=candidate.geometry[a]!,v=candidate.geometry[b]!;if(u.owner===v.owner||u.layer!==v.layer)continue;if(gap(u,v)<clearance-1e-8)valid=false}}
    const antipads=valid?planAntipads(candidate.vias):null
    if(valid&&antipads){for(const a of antipads)planeCandidates[a.planeIndex].brep_shape.inner_rings.push(a.hole);chosen={antipads:antipads.map(({hole,...a})=>a),name:`${ram}_${kind}${i+1}`,ram,kind,x:c.x,y:c.y,rotation:c.rotation,footprint:kind==='HS'?'cap0201_nosilkscreen':'cap0805_nosilkscreen',capacitance:kind==='HS'?'100nF':'22uF',assignedPowerBall:c.near.ball,powerBallDistanceMm:c.near.distance,assignedGroundBall:c.nearGround.ball,groundBallDistanceMm:c.nearGround.distance,assignedViaPowerBall:c.viaPower.ball,powerBallToViaDistanceMm:c.viaPower.distance,minCopperGapMm:minGap,vias:candidate.vias,routes:candidate.routes};accepted.push(...candidate.geometry);bodies.push({x:c.x,y:c.y,w:c.cw,h:c.ch});break}
   }
   if(!chosen){plans.push({ram,kind,index:i+1,status:'no-clear-placement-in-tested-grid'});break}plans.push(chosen)
  }
 }
}
writeFileSync(resolve(outDir,'candidate-reference-planes.json'),JSON.stringify(planeCandidates,null,2)+'\n')
writeFileSync(resolve(outDir,'plan.json'),JSON.stringify({captureHash,scope:'New component pads, full ten-layer via barrels and local pad-to-via traces vs every original pad/trace/via; does NOT connect supplies or prove effective capacitance.',reservedHostRoutes:reservedPath,reservedHostRoutesSha256:existsSync(reservedPath)?createHash('sha256').update(readFileSync(reservedPath)).digest('hex'):null,planeChange:'Ground-plane outer rings and all original holes unchanged; contained, nonoverlapping 64-sided circumscribed new POWER-via antipads appended. New GND vias require explicit parent alias to existing RAM GND.',minimumAddedHoleCopperWebMm:minPlaneWeb,clearanceMm:clearance,hsRequirement:{countPerRam:12,nominalEachUf:.1,minimumTotalEffectiveUf:.85,minimumRetentionFraction:.85/1.2},bulkRequirement:{countPerRam:2,nominalEachUf:22,minimumTotalEffectiveUf:20,minimumRetentionFraction:20/44},plans},null,2)+'\n')
console.log(JSON.stringify(plans.map(({routes,vias,...p})=>p),null,2))
const placed=plans.filter(p=>p.name)
assert.deepEqual(planDdrGroundAntipads(planes,placed.flatMap(p=>p.vias)).planes,planeCandidates)
const circuit=new RootCircuit(),children:any[]=[]
for(const p of placed){children.push(React.createElement('capacitor',{name:p.name,capacitance:p.capacitance,footprint:p.footprint,maxVoltageRating:'6.3V',layer:'bottom',pcbX:p.x,pcbY:p.y,pcbRotation:p.rotation}))
 for(const [i,v]of p.vias.entries()){const name=`V_${p.name}_${i+1}`;children.push(React.createElement('via',{name,pcbX:v.x,pcbY:v.y,fromLayer:'top',toLayer:'bottom',outerDiameter:.4572,holeDiameter:.254,connectsTo:`net.${v.net}`}));children.push(React.createElement('trace',{name:`T_${p.name}_${i+1}`,from:`${p.name}.pin${i+1}`,to:`${name}.bottom`,pcbPath:[`${name}.bottom`],thickness:.1016}))}
}
circuit.add(React.createElement('board',{width:80,height:50,layers:10,schematicDisabled:true,routingDisabled:true,allowBlindAndBuriedVias:false,minPadEdgeToPadEdgeClearance:.1016,minViaEdgeToPadEdgeClearance:.1016,minTraceToPadEdgeClearance:.1016},...children))
await circuit.renderUntilSettled()
const rendered=circuit.getCircuitJson(),actual=shapes(rendered),renderErrors=rendered.filter(e=>e.type.endsWith('_error'))
let actualMin=Infinity;const actualViolations:any[]=[]
for(const a of actual)for(const b of fixed){if(a.layer!==b.layer)continue;const g=gap(a,b);actualMin=Math.min(actualMin,g);if(g<clearance-1e-8)actualViolations.push({a:a.owner,b:b.owner,layer:a.layer,gapMm:g})}
const planeAudit:any[]=[],planeViolations:any[]=[]
for(const [index,plane]of planeCandidates.entries()){
 const old=planes[index],oldShape=old.brep_shape,newShape=plane.brep_shape
 assert.deepEqual(newShape.outer_ring,oldShape.outer_ring)
 assert.deepEqual(newShape.inner_rings.slice(0,oldShape.inner_rings.length),oldShape.inner_rings)
 for(const p of placed)for(const via of p.vias){if(via.net==='GND')continue
  const inOuter=inside(via,newShape.outer_ring.vertices),hole=newShape.inner_rings.find((h:any)=>inside(via,h.vertices))
  const clearanceToPlane=(hole?ringDistance(via,hole.vertices):!inOuter?ringDistance(via,newShape.outer_ring.vertices):0)-.4572/2
  if(clearanceToPlane<clearance-1e-8)planeViolations.push({plane:plane.pcb_copper_pour_id,cap:p.name,via,clearanceToPlane})
 }
 planeAudit.push({planeId:plane.pcb_copper_pour_id,layer:plane.layer,originalHoleCount:oldShape.inner_rings.length,newHoleCount:newShape.inner_rings.length,outerRingUnchanged:true,originalHolesUnchanged:true,newHolesContainedAndDisjoint:true,minimumNewHoleWebMm:minPlaneWeb})
}
const launchAudit=snapshot.placements.flatMap((placement:any,index:number)=>snapshot.terminalsByByte[index].filter((t:any)=>/^V(?:DD|SS)Q?_/.test(t.terminal)).map((t:any)=>{const path=snapshot.pathsByByte[index].find((p:any)=>p.connection===`U1.ball_${t.ball}`),a=path.route[0],v=path.route.find((p:any)=>p.route_type==='via');return {ram:placement.name,terminal:t.terminal,ball:t.ball,hasConnectionVia:!!v,ballToViaMm:v?Math.hypot(a.x-v.x,a.y-v.y):null,passes60mil:!!v&&Math.hypot(a.x-v.x,a.y-v.y)<=1.524}}))
const angles=auditRouteAngles(rendered.filter(e=>e.type==='pcb_trace'))
const padToViaLengths=rendered.filter(e=>e.type==='pcb_trace').map((t:any)=>({trace:t.pcb_trace_id,lengthMm:t.trace_length,passes100mil:t.trace_length<=2.54}))
assert.equal(createHash('sha256').update(JSON.stringify(original)).digest('hex'),originalCopperHash)
const counts={capacitors:rendered.filter(e=>e.type==='source_component'&&(e as any).ftype==='simple_capacitor').length,pads:rendered.filter(e=>e.type==='pcb_smtpad').length,vias:rendered.filter(e=>e.type==='pcb_via').length,traces:rendered.filter(e=>e.type==='pcb_trace').length}
writeFileSync(resolve(outDir,'candidate.circuit.json'),JSON.stringify(rendered,null,2)+'\n')
writeFileSync(resolve(outDir,'rendered-audit.json'),JSON.stringify({captureHash,originalCopperSha256:originalCopperHash,originalCopperUnchanged:true,planeAudit,planeViolations,angles,padToViaLengths,launchAudit,originalSupplyLaunchGatePasses:launchAudit.every((p:any)=>p.passes60mil),counts,renderErrors,actualMinClearanceMm:actualMin,actualViolations,allViasTenLayer:rendered.filter(e=>e.type==='pcb_via').every((v:any)=>v.layers.length===10),completePlacement:placed.length===28,scope:'Rendered capacitor pads/local traces/full-depth vias versus original captured copper. Independent self-clearance checked during placement; no supply-to-DRAM/CPU plane connection is claimed.'},null,2)+'\n')
console.log(JSON.stringify({counts,renderErrors:renderErrors.length,actualViolations:actualViolations.length,actualMinClearanceMm:actualMin}))
if(renderErrors.length||actualViolations.length||planeViolations.length||!angles.valid||padToViaLengths.some(p=>!p.passes100mil)||placed.length!==28)process.exitCode=1