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/prepare-ddr-host-signal-projection.ts

/** Planning-only projection of actual copper; interior taps remain accessible.
 * Off-signal copper and pours remain mandatory in final ten-layer validation. */
import {readFileSync,writeFileSync} from 'node:fs'
import {resolve} from 'node:path'
import {createHash} from 'node:crypto'
import {verifyDdrCapture} from './ddr-capture-provenance'
export const DDR_HOST_PHYSICAL_TO_VIRTUAL:Record<string,string>={top:'top',inner2:'inner1',inner4:'inner2',inner6:'bottom'}
const virtualLayers=Object.values(DDR_HOST_PHYSICAL_TO_VIRTUAL)
export function projectHostObstacles(obstacles:any[]){
 const projected:any[]=[],omitted:any[]=[]
 for(const [index,o]of obstacles.entries()){
  if(o.type!=='rect'||![o.center?.x,o.center?.y,o.width,o.height].every(Number.isFinite)||o.width<0||o.height<0||!Array.isArray(o.layers)||!o.layers.length)throw Error(`Unsupported obstacle ${index}`)
  const isVia=Boolean(o.circuitJsonMetadata?.pcb_via_id)||o.obstacleId?.startsWith('fixed_via_')
  const layers=isVia?[...virtualLayers]:[...new Set(o.layers.map((l:string)=>DDR_HOST_PHYSICAL_TO_VIRTUAL[l]).filter(Boolean))]
  if(layers.length){
   const theta=Number(o.ccwRotationDegrees??0)*Math.PI/180
   if(!Number.isFinite(theta))throw Error('Invalid obstacle rotation')
   const cosine=Math.cos(theta),sine=Math.sin(theta)
   // Pipeline4's capacity mesh cannot rely on rotation metadata. Partition the
   // true rotated rectangle, then enclose each slice, preserving all copper.
   const diagonal=Math.abs(sine*cosine)>1e-9
   const count=diagonal&&o.obstacleId?.includes('_segment')?Math.max(1,Math.ceil(o.width/.1)):1
   for(let part=0;part<count;part++){
    const width=o.width/count,offset=-o.width/2+(part+.5)*width
    projected.push({...structuredClone(o),center:{x:o.center.x+offset*cosine,y:o.center.y+offset*sine},width:Math.abs(cosine)*width+Math.abs(sine)*o.height,height:Math.abs(sine)*width+Math.abs(cosine)*o.height,ccwRotationDegrees:0,layers,netIsAssignable:false,offBoardConnectsTo:[],__originalObstacleIndex:index,__projectionSlice:part})
   }
  }
  else omitted.push({index,obstacleId:o.obstacleId,layers:o.layers,reason:'No host signal layer; retained in original capture for full-depth-via physical verification'})
 }
 return {projected,omitted}
}
export function boundHostPlanningRegion(input:any,maxX:number,clearanceMm=.1016){
 const bounds={...input.bounds,maxX},margin=.4572/2+clearanceMm
 if(!Number.isFinite(clearanceMm)||clearanceMm<.1016)throw Error('Unsafe planning clearance')
 if(!Number.isFinite(maxX)||maxX>input.bounds.maxX||maxX<=input.bounds.minX)throw Error('Invalid host planning maxX')
 for(const c of input.connections)for(const p of c.pointsToConnect)if(p.x<bounds.minX||p.x>bounds.maxX||p.y<bounds.minY||p.y>bounds.maxY)throw Error(`Terminal outside planning bounds: ${c.name}`)
 const omitted:any[]=[],obstacles=input.obstacles.filter((o:any)=>{
  if(o.type!=='rect'||Math.abs(o.ccwRotationDegrees??0)>1e-9)throw Error('Region culling requires projected axis rectangles')
  // Closed inflated center domain: any legal new wire/via copper plus its
  // required gap fits within this domain. Keep boundary-touching obstacles.
  const outside=o.center.x+o.width/2<bounds.minX-margin||o.center.x-o.width/2>bounds.maxX+margin||o.center.y+o.height/2<bounds.minY-margin||o.center.y-o.height/2>bounds.maxY+margin
  if(outside)omitted.push({obstacleId:o.obstacleId,index:o.__originalObstacleIndex,slice:o.__projectionSlice})
  return !outside
 })
 return {input:{...input,bounds,obstacles},report:{centerRoutingBounds:bounds,maximumNewViaDiameterMm:.4572,clearanceMm,expandedDomainMarginMm:margin,originalProjectedObstacleCount:input.obstacles.length,retainedObstacleCount:obstacles.length,culledObstacleCount:omitted.length,culledObstacles:omitted},obstacles}
}
export function prepareDdrHostSignalProjection(directory:string){
 const {captureHash,snapshot}=verifyDdrCapture(directory)
 if(JSON.stringify(snapshot.hostProfiles)!==JSON.stringify(['am3352-byte0','am3352-byte1']))throw Error('Actual-copper projection requires the two AM3352 host-tap profiles')
 const read=(name:string)=>JSON.parse(readFileSync(resolve(directory,name),'utf8'))
 const input=read('routing-input.json'),original=read('unrouted.circuit.json')
 if(input.__ddrCaptureHash!==captureHash||input.layerCount!==10)throw Error('Incorrect physical capture identity or layer count')
 if(input.obstacles.some((o:any)=>o.connectedTo.some((n:string)=>n.startsWith('solid_module_'))))throw Error('Host-tap projection must not use solid module boxes')
 const owner=new Map<string,number>(),parent=input.connections.map((_:any,i:number)=>i)
 const find=(i:number):number=>parent[i]===i?i:parent[i]=find(parent[i])
 const key=(p:any)=>p.pointId??`${p.x},${p.y},${p.layer}`
 for(const [i,c]of input.connections.entries())for(const p of c.pointsToConnect){const k=key(p);if(owner.has(k)){const a=find(i),b=find(owner.get(k)!);if(a!==b)parent[a]=b}else owner.set(k,i)}
 const groups=new Map<number,any[]>()
 for(const [i,c]of input.connections.entries()){const k=find(i);groups.set(k,[...(groups.get(k)??[]),c])}
 const alias=new Map<string,string>()
 const connections=[...groups.values()].map(g=>{
  const name=g[0].name;for(const c of g)alias.set(c.name,name)
  const points=[...new Map(g.flatMap(c=>c.pointsToConnect.map((p:any)=>[key(p),p]))).values()] as any[]
  return {...g[0],name,rootConnectionName:name,mergedConnectionNames:g.map(c=>c.name),pointsToConnect:points.map(p=>{const layer=DDR_HOST_PHYSICAL_TO_VIRTUAL[p.layer];if(!layer)throw Error(`Host terminal on forbidden physical layer ${p.layer}`);return {...p,layer}})}
 })
 if(connections.length!==50)throw Error('Expected complete 50-net canonical interface')
 const {projected,omitted}=projectHostObstacles(input.obstacles)
 const obstacles=projected.map(o=>({...o,connectedTo:[...new Set([...o.connectedTo.map((n:string)=>alias.get(n)).filter(Boolean),`fixed-physical-obstacle:${o.__originalObstacleIndex}`])]}))
 // Every original via is represented at full pad diameter on all virtual layers.
 const vias=original.filter((e:any)=>e.type==='pcb_via')
 for(const via of vias){
  const hits=projected.filter(o=>o.circuitJsonMetadata?.pcb_via_id===via.pcb_via_id)
  if(!hits.some(o=>Math.hypot(o.center.x-via.x,o.center.y-via.y)<1e-7&&o.width>=via.outer_diameter-1e-7&&o.height>=via.outer_diameter-1e-7&&virtualLayers.every(l=>o.layers.includes(l))))throw Error(`Missing full-barrel obstacle ${via.pcb_via_id}`)
 }
 // Capture generates one provenance-keyed obstacle per pad and every fixed
 // trace segment. Refuse an input missing actual selected-layer pad copper.
 for(const pad of original.filter((e:any)=>e.type==='pcb_smtpad'&&DDR_HOST_PHYSICAL_TO_VIRTUAL[e.layer])){
  if(!projected.some(o=>o.circuitJsonMetadata?.pcb_smtpad_id===pad.pcb_smtpad_id&&o.layers.includes(DDR_HOST_PHYSICAL_TO_VIRTUAL[pad.layer])))throw Error(`Missing selected-layer pad ${pad.pcb_smtpad_id}`)
 }
 const traceIds=new Set(original.filter((e:any)=>e.type==='pcb_trace'&&e.route.some((p:any)=>p.route_type==='wire'&&DDR_HOST_PHYSICAL_TO_VIRTUAL[p.layer])).map((e:any)=>e.pcb_trace_id))
 const originalObstaclesById=new Map<string,any>(input.obstacles.filter((o:any)=>o.obstacleId).map((o:any)=>[o.obstacleId,o]))
 let verifiedWireCaps=0,verifiedWireSegments=0
 for(const t of original.filter((e:any)=>e.type==='pcb_trace'&&traceIds.has(e.pcb_trace_id)))for(const [i,p]of t.route.entries()){
  if(p.route_type!=='wire'||!DDR_HOST_PHYSICAL_TO_VIRTUAL[p.layer])continue
  const cap=originalObstaclesById.get(`fixed_trace_${t.pcb_trace_id}_${i}_cap`)
  if(!cap||Math.hypot(cap.center.x-p.x,cap.center.y-p.y)>1e-7||cap.width<Number(p.width)-1e-7||cap.height<Number(p.width)-1e-7||!cap.layers.includes(p.layer))throw Error(`Missing actual trace cap ${t.pcb_trace_id}/${i}`)
  verifiedWireCaps++
  const q=t.route[i+1]
  if(q?.route_type!=='wire'||q.layer!==p.layer||Math.hypot(q.x-p.x,q.y-p.y)<1e-7)continue
  const seg=originalObstaclesById.get(`fixed_trace_${t.pcb_trace_id}_${i}_segment`),length=Math.hypot(q.x-p.x,q.y-p.y),angle=Math.atan2(q.y-p.y,q.x-p.x)*180/Math.PI
  if(!seg||Math.hypot(seg.center.x-(p.x+q.x)/2,seg.center.y-(p.y+q.y)/2)>1e-7||seg.width<length-1e-7||seg.height<Math.max(p.width,q.width)-1e-7||Math.abs(Math.sin(((seg.ccwRotationDegrees??0)-angle)*Math.PI/180))>1e-7||!seg.layers.includes(p.layer))throw Error(`Missing actual trace segment ${t.pcb_trace_id}/${i}`)
  verifiedWireSegments++
 }
 const report={captureHash,physicalLayerCount:10,virtualLayerCount:4,originalObstacleCount:input.obstacles.length,projectedObstacleCount:obstacles.length,offSignalObstacleCount:omitted.length,offSignalObstacles:omitted,verifiedFullDepthVias:vias.length,verifiedSelectedLayerTraces:traceIds.size,verifiedWireCaps,verifiedWireSegments,referencePours:original.filter((e:any)=>e.type==='pcb_copper_pour').length,canonicalNetCount:connections.length,scope:'Planning signal projection only. All ten physical layers, off-signal copper and reference pour holes must be checked against every new via before acceptance.'}
 return {input:{...input,layerCount:4,connections,obstacles,traces:[],allowJumpers:false,allowViaInPad:false,allowBlindAndBuriedVias:false,buses:[{busId:'DDR_HOST',connectionNames:connections.map(c=>c.name),allowedLayers:virtualLayers}],__ddrProjection:'actual-host-copper',__ddrSourceInputHash:createHash('sha256').update(readFileSync(resolve(directory,'routing-input.json'))).digest('hex')},report}
}
if(import.meta.main){const dir=resolve(process.argv[2]??'');const result=prepareDdrHostSignalProjection(dir);writeFileSync(resolve(dir,'projected-host-routing-input.json'),JSON.stringify(result.input,null,2)+'\n');writeFileSync(resolve(dir,'projection-report.json'),JSON.stringify(result.report,null,2)+'\n');console.log(JSON.stringify({...result.report,offSignalObstacles:undefined}))}