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/route-ddr-sequential.ts
/** One bounded ordering, with all previously produced copper frozen conservatively.
* Router candidates are not physical/DDR acceptance. */
import {readFileSync,writeFileSync,mkdirSync} from 'node:fs'
import {resolve} from 'node:path'
import {createHash} from 'node:crypto'
import {verifyDdrCapture} from './ddr-capture-provenance'
import {prepareDdrHostSignalProjection,boundHostPlanningRegion} from './prepare-ddr-host-signal-projection'
import {selectVerifiedDdrSeed,resolveDdrTraceNet} from './ddr-verified-seed'
import {AutoroutingPipelineSolver4} from '@tscircuit/capacity-autorouter'
const layers=['top','inner1','inner2','bottom']
/** Raw solver candidates must retain every declared terminal. Branch traces may
* repeat a root in either orientation, but may not invent floating endpoints. */
export function checkHostTerminalContract(traces:any[],connections:any[],toleranceMm=.001){
if(!Number.isFinite(toleranceMm)||toleranceMm<0||toleranceMm>.001)throw Error('Terminal tolerance must be <= .001mm')
const violations:any[]=[]
for(const connection of connections){
const terminals=connection.pointsToConnect,covered=new Set<number>()
const members=traces.filter(t=>t.connection_name===connection.name)
for(const t of members){
for(const [end,p]of [['start',t.route?.[0]],['end',t.route?.at(-1)]] as const){
const matches=terminals.map((q:any,i:number)=>({q,i,distanceMm:Math.hypot(q.x-p?.x,q.y-p?.y)})).filter(({q,distanceMm}:any)=>p?.route_type==='wire'&&p.layer===q.layer&&distanceMm<=toleranceMm+1e-12)
if(matches.length===1)covered.add(matches[0].i)
else{
const nearest=terminals.map((q:any)=>({pointId:q.pointId,x:q.x,y:q.y,layer:q.layer,distanceMm:Math.hypot(q.x-p?.x,q.y-p?.y)})).sort((a:any,b:any)=>a.distanceMm-b.distanceMm)[0]
violations.push({net:connection.name,traceId:t.pcb_trace_id,end,reason:matches.length?'ambiguous-terminal':'moved-or-wrong-layer-terminal',actual:p,nearest})
}
}
}
terminals.forEach((p:any,i:number)=>{if(!covered.has(i))violations.push({net:connection.name,reason:'missing-declared-terminal',terminal:p})})
}
for(const t of traces)if(!connections.some(c=>c.name===t.connection_name))violations.push({traceId:t.pcb_trace_id,reason:'foreign-net'})
return {valid:!violations.length,toleranceMm,violations}
}
export function inspectHdTerminalProvenance(hdRoutes:any[],connections:any[],layerCount:number){
const layerAt=(z:number)=>z===0?'top':z===layerCount-1?'bottom':`inner${z}`
return connections.flatMap((c:any)=>c.pointsToConnect.map((terminal:any)=>{
const ends=hdRoutes.filter(r=>[r.connectionName,r.rootConnectionName].includes(c.name)).flatMap(r=>[r.route?.[0],r.route?.at(-1)]).filter(Boolean)
const nearest=ends.map(p=>({x:p.x,y:p.y,z:p.z,layer:layerAt(p.z),pcb_port_id:p.pcb_port_id,distanceMm:Math.hypot(p.x-terminal.x,p.y-terminal.y)})).filter(p=>p.layer===terminal.layer).sort((a,b)=>a.distanceMm-b.distanceMm)[0]
return {net:c.name,terminal,nearest,withinTolerance:!!nearest&&nearest.distanceMm<=.001+1e-12}
}))
}
export function restoreHdTerminalIds(hdRoutes:any[],connections:any[],layerCount:number){
const evidence=inspectHdTerminalProvenance(hdRoutes,connections,layerCount)
if(evidence.some(e=>!e.withinTolerance))throw Error('Terminal moved before GlobalDrc repair; refusing metadata restoration')
const result=structuredClone(hdRoutes)
const layerAt=(z:number)=>z===0?'top':z===layerCount-1?'bottom':`inner${z}`
for(const route of result){
const c=connections.find((c:any)=>[route.connectionName,route.rootConnectionName].includes(c.name));if(!c)throw Error('Unknown pre-repair HD net')
for(const p of [route.route[0],route.route.at(-1)]){
const matches=c.pointsToConnect.filter((q:any)=>q.layer===layerAt(p.z)&&Math.hypot(q.x-p.x,q.y-p.y)<=.001+1e-12)
if(matches.length!==1)throw Error('Ambiguous or moved pre-repair endpoint')
const id=matches[0].pcb_port_id??matches[0].pointId;if(!id)throw Error('Authoritative terminal has no ID')
p.pcb_port_id=id
}
}
return result
}
/** Installed Pipeline4 exposes these documented constructor options downstream
* but omits them from its defaults. Preserve terminal IDs through every stage. */
export function preservePipeline4Terminals(solver:any,onEvidence:(e:any)=>void=()=>{}){
const settings:Record<string,any>={portPointPathingSolver:{preserveTerminalPcbPortIds:true},highDensityRouteSolver:{preserveTerminalPcbPortIds:true},highDensityStitchSolver:{preserveTerminalPcbPortIds:true,preferSameLayerTerminalEndpoints:true},traceSimplificationSolver:{preserveRouteEndpoints:true}}
const seen=new Set<string>()
solver.pipelineDef=solver.pipelineDef.map((step:any)=>{
const options=settings[step.solverName];if(options)seen.add(step.solverName)
return {...step,getConstructorParams:(instance:any)=>{
const params=step.getConstructorParams(instance)
if(!options&&!Array.isArray(params[0]?.hdRoutes))return params
const first={...params[0],...options}
if(Array.isArray(first.hdRoutes)){
const connections=instance.srjWithPointPairs?.connections??instance.srj.connections
onEvidence({beforeStage:step.solverName,terminals:inspectHdTerminalProvenance(first.hdRoutes,connections,instance.srj.layerCount)})
if(step.solverName==='globalDrcForceImproveSolver')first.hdRoutes=restoreHdTerminalIds(first.hdRoutes,connections,instance.srj.layerCount)
}
return [first,...params.slice(1)]
}}
})
if(seen.size!==Object.keys(settings).length)throw Error('Installed Pipeline4 terminal preservation stages changed')
return settings
}
export function freezeHostCopper(traces:any[],stepMm=.025){
if(!Number.isFinite(stepMm)||stepMm<=0||stepMm>.25)throw Error('Subdivision must be in (0,.25] mm')
const obstacles:any[]=[]
const clean=(route:any[])=>{
const result:any[]=[]
for(const p of route){
result.push(p)
while(result.length>=3){
const [a,b,c]=result.slice(-3)
if([a,b,c].some(q=>q.route_type!=='wire')||a.layer!==b.layer||b.layer!==c.layer||a.width!==b.width||b.width!==c.width)break
const ux=b.x-a.x,uy=b.y-a.y,vx=c.x-b.x,vy=c.y-b.y
if(Math.abs(ux*vy-uy*vx)>1e-10||ux*vx+uy*vy<0)break
result.splice(result.length-2,1)
}
}
return result
}
for(const trace of traces){
const connectedTo=[trace.connection_name]
if(!connectedTo[0])throw Error('Host trace has no canonical net name')
const rect=(a:any,b:any,width:number,layer:string)=>obstacles.push({type:'rect',shape:'rect',center:{x:(a.x+b.x)/2,y:(a.y+b.y)/2},width:Math.abs(b.x-a.x)+width,height:Math.abs(b.y-a.y)+width,layers:[layer],connectedTo,netIsAssignable:false,offBoardConnectsTo:[]})
const route=clean(trace.route)
for(const [i,p] of route.entries()){
if(p.route_type==='via'){
const diameter=Math.max(.4572,Number(p.via_diameter??.4572))
obstacles.push({type:'rect',shape:'circle',center:{x:p.x,y:p.y},width:diameter,height:diameter,layers:[...layers],connectedTo,netIsAssignable:false,offBoardConnectsTo:[]})
}else if(p.route_type==='wire'){
const width=Math.max(.1016,Number(p.width)),a=route[i-1]
if(!Number.isFinite(width)||!layers.includes(p.layer))throw Error('Invalid wire geometry')
// Include every end cap even when the preceding point is a via.
rect(p,p,width,p.layer)
if(a?.route_type==='wire'){
if(a.layer!==p.layer)throw Error('Layer transition missing via')
// Axis-aligned rectangles are exact; only diagonal centerlines need subdivision.
const axisAligned=Math.abs(p.x-a.x)<1e-12||Math.abs(p.y-a.y)<1e-12
const n=axisAligned?1:Math.max(1,Math.ceil(Math.hypot(p.x-a.x,p.y-a.y)/stepMm))
for(let j=0;j<n;j++)rect({x:a.x+(p.x-a.x)*j/n,y:a.y+(p.y-a.y)*j/n},{x:a.x+(p.x-a.x)*(j+1)/n,y:a.y+(p.y-a.y)*(j+1)/n},Math.max(width,Number(a.width)),p.layer)
}
}else throw Error('Unknown route primitive')
}
}
return obstacles
}
const write=(path:string,value:unknown)=>writeFileSync(path,JSON.stringify(value,null,2)+'\n')
if(import.meta.main&&process.argv[2]==='--child'){
const input=JSON.parse(readFileSync(process.argv[3]!,'utf8')),output=process.argv[4]!,start=Date.now()
const solver=new AutoroutingPipelineSolver4(input,{effort:1})
const terminalProvenance:any[]=[]
preservePipeline4Terminals(solver,e=>{terminalProvenance.push(e);write(output+'.terminal-provenance.json',terminalProvenance)})
let phase='',iterations=0
while(!solver.solved&&!solver.failed){
if(solver.portPointPathingSolver)solver.portPointPathingSolver.MAX_ITERATIONS=1000000
const current=solver.getCurrentPhase()
if(current!==phase){phase=current;write(output,{status:'running',phase,iterations,elapsedSeconds:(Date.now()-start)/1000})}
try{solver.step()}catch(error){write(output,{status:'rejected-stage-terminal-contract',phase,error:String(error),iterations,elapsedSeconds:(Date.now()-start)/1000,traces:[]});process.exit(0)}iterations++
}
write(output,{status:solver.solved?'routed-unvalidated':'routing-failed',phase:solver.getCurrentPhase(),error:solver.error,iterations,elapsedSeconds:(Date.now()-start)/1000,traces:solver.solved?solver.getOutputSimplifiedPcbTraces():[]})
}else if(import.meta.main){
const dir=resolve(process.argv[2]??'dist/ddr-system/bus_grouped')
const option=(name:string,fallback:string)=>process.argv.find(a=>a.startsWith(`--${name}=`))?.slice(name.length+3)??fallback
const stepMm=Number(option('step-mm','.025')),groupSeconds=Number(option('group-seconds','10')),ordering=option('ordering','access-first'),runName=option('run-name',`sequential-${Date.now()}`)
if(!Number.isFinite(groupSeconds)||groupSeconds<=0||groupSeconds>120)throw Error('Group watchdog must be in (0,120] seconds')
if(!/^[A-Za-z0-9_-]+$/.test(runName))throw Error('Run name must be a simple directory name')
if(!['access-first','pairs-data-control','pairs-control-data'].includes(ordering))throw Error('Unknown routing ordering')
freezeHostCopper([],stepMm)
const out=resolve(dir,runName)
const {captureHash}=verifyDdrCapture(dir)
mkdirSync(out,{recursive:true})
const fullCopper=process.argv.includes('--full-copper'),dataOnly=process.argv.includes('--data-only'),singleLayer=process.argv.includes('--single-layer')
const routeWidth=Number(option('route-width','.1016')),routingClearance=Number(option('routing-clearance','.1016'))
if(!Number.isFinite(routeWidth)||routeWidth<.1016||routeWidth>.4572||!Number.isFinite(routingClearance)||routingClearance<.1016)throw Error('Invalid route width or clearance')
const sourceInputPath=resolve(dir,fullCopper?'routing-input.json':'merged-routing-input.json')
const projection=fullCopper?prepareDdrHostSignalProjection(dir):null
let input=projection?.input??JSON.parse(readFileSync(sourceInputPath,'utf8'))
if(projection)write(resolve(out,'projection-report.json'),projection.report)
const planningMaxX=option('planning-max-x',''),onlySignals=option('only-signals','').split(',').filter(Boolean)
if(planningMaxX){
if(!fullCopper)throw Error('Bounded planning requires actual copper projection')
const bounded=boundHostPlanningRegion(input,Number(planningMaxX),routingClearance);input=bounded.input
write(resolve(out,'planning-region-report.json'),bounded.report)
}
if(input.__ddrCaptureHash!==captureHash)throw Error('Merged routing input belongs to another capture')
const inputHash=createHash('sha256').update(readFileSync(sourceInputPath)).digest('hex')
const signalMap=JSON.parse(readFileSync(resolve(dir,'signal-map.json'),'utf8'))
if(input.layerCount!==4||input.connections.length!==50)throw Error('Requires canonical 50-net four-virtual-signal-layer input')
if(!fullCopper&&input.obstacles.length!==131)throw Error('Expected three module boxes plus 128 frozen exit leads')
const leads=fullCopper?[]:JSON.parse(readFileSync(resolve(dir,'corridor-leads.json'),'utf8'))
const seedFlag=process.argv.indexOf('--seed-physical')
const seedPath=process.argv.find(a=>a.startsWith('--seed-physical='))?.slice('--seed-physical='.length)??(seedFlag>=0?process.argv[seedFlag+1]:undefined)
if(seedFlag>=0&&(!seedPath||seedPath.startsWith('--')))throw Error('--seed-physical requires a candidate bundle path')
let seed:any={traces:[],connectedNetNames:[],seedTraceHash:null,report:null}
let seedBundleHash:string|null=null
if(seedPath){
const seedText=readFileSync(resolve(seedPath),'utf8')
seedBundleHash=createHash('sha256').update(seedText).digest('hex')
seed=selectVerifiedDdrSeed({captureHash,bundle:JSON.parse(seedText),original:JSON.parse(readFileSync(resolve(dir,'unrouted.circuit.json'),'utf8')),leads,connections:input.connections,expectedNetCount:50})
write(resolve(out,'seed.physical.json'),{captureHash,layerSpace:'physical',traces:seed.traces})
write(resolve(out,'seed-verification.json'),{captureHash,seedBundleHash,seedTraceHash:seed.seedTraceHash,connectedNetNames:seed.connectedNetNames,report:seed.report})
}
const seededNames=new Set<string>(seed.connectedNetNames)
const mapLayer=(physical:string)=>{
const virtual:Record<string,string>={top:'top',inner2:'inner1',inner4:'inner2',inner6:'bottom'}
if(!virtual[physical])throw Error(`Illegal physical seed signal layer ${physical}`)
return virtual[physical]!
}
const seedVirtual=seed.traces.map((t:any)=>({...structuredClone(t),connection_name:resolveDdrTraceNet(t,input.connections),route:t.route.map((p:any)=>p.route_type==='wire'?{...p,layer:mapLayer(p.layer)}:p.route_type==='via'?{...p,from_layer:mapLayer(p.from_layer),to_layer:mapLayer(p.to_layer)}:p)}))
const nameToSignal=new Map<string,string>()
for(const c of input.connections){
const maps=signalMap.filter((m:any)=>[c.name,...(c.mergedConnectionNames??[])].includes(m.sourceTraceId))
const names=[...new Set(maps.map((m:any)=>m.cpuSignal??m.cpuTarget.split('.').at(-1)))]
if(names.length!==1)throw Error(`Ambiguous canonical net ${c.name}`)
nameToSignal.set(c.name,names[0] as string)
}
for(const signal of onlySignals)if(![...nameToSignal.values()].includes(signal))throw Error(`Unknown requested signal ${signal}`)
const allPendingConnections=input.connections.filter((c:any)=>!seededNames.has(c.name))
const pendingConnections=allPendingConnections.filter((c:any)=>(!dataOnly||/^DDR_(D\d+|DQM[01])$/.test(nameToSignal.get(c.name)!))&&(!onlySignals.length||onlySignals.includes(nameToSignal.get(c.name)!)))
const intentionallyUnroutedNetNames=allPendingConnections.filter((c:any)=>!pendingConnections.includes(c)).map((c:any)=>c.name)
const bySignal=(names:string[])=>pendingConnections.filter((c:any)=>names.includes(nameToSignal.get(c.name)!))
const groups:any[]=[{name:'CK',connections:bySignal(['DDR_CK','DDR_CKn'])},{name:'DQS0',connections:bySignal(['DDR_DQS0','DDR_DQSn0'])},{name:'DQS1',connections:bySignal(['DDR_DQS1','DDR_DQSn1'])}]
const assigned=new Set(groups.flatMap(g=>g.connections.map((c:any)=>c.name)))
const data=pendingConnections.filter((c:any)=>!assigned.has(c.name)&&/^DDR_(D\d+|DQM[01])$/.test(nameToSignal.get(c.name)!))
const remaining=pendingConnections.filter((c:any)=>!assigned.has(c.name)&&!data.includes(c))
const accessScore=(c:any)=>{
let crowded=0
for(const p of c.pointsToConnect)for(const o of input.obstacles){
if(o.connectedTo.includes(c.name)||!o.layers.includes(p.layer)||o.width>2||o.height>2)continue
if(Math.hypot(o.center.x-p.x,o.center.y-p.y)<1.1)crowded++
}
const layerChanges=new Set(c.pointsToConnect.map((p:any)=>p.layer)).size-1
return crowded*10+layerChanges*5
}
const ordered=ordering==='pairs-control-data'?[...remaining,...data]:ordering==='pairs-data-control'?[...data,...remaining]:[...data,...remaining].sort((a,b)=>accessScore(b)-accessScore(a)||nameToSignal.get(a.name)!.localeCompare(nameToSignal.get(b.name)!))
for(const c of ordered)groups.push({name:nameToSignal.get(c.name),connections:[c]})
groups.splice(0,groups.length,...groups.filter(g=>g.connections.length))
if(groups.flatMap(g=>g.connections).length+seededNames.size+intentionallyUnroutedNetNames.length!==50)throw Error('Group coverage does not equal 50 canonical nets')
const routes:any[]=[...seedVirtual],results:any[]=[],solvedNames=new Set<string>(seededNames),start=Date.now()
let resumeGroupCount=0
if(process.argv.includes('--resume')){
const previous=JSON.parse(readFileSync(resolve(out,'report.json'),'utf8')),saved=JSON.parse(readFileSync(resolve(out,'candidate.virtual.json'),'utf8'))
for(const [key,value]of Object.entries({captureHash,inputHash,seedTraceHash:seed.seedTraceHash,seedBundleHash,stepMm,groupSeconds,orderingStrategy:ordering,planningMaxX:planningMaxX?Number(planningMaxX):null,dataOnly}))if(previous[key]!==value)throw Error(`Resume configuration mismatch: ${key}`)
if(saved.captureHash!==captureHash||saved.layerSpace!=='virtual'||JSON.stringify(saved.traces.slice(0,seedVirtual.length))!==JSON.stringify(seedVirtual))throw Error('Resume seed mismatch')
if(JSON.stringify(previous.ordering)!==JSON.stringify(groups.map(g=>g.name)))throw Error('Resume ordering mismatch')
results.push(...previous.results);resumeGroupCount=results.length
const recovered:any[]=[...seedVirtual]
for(const [i,r]of results.entries())if(r.status==='routed-unvalidated'){
const prior=JSON.parse(readFileSync(resolve(out,`${String(i).padStart(2,'0')}-${groups[i].name}.result.json`),'utf8'))
if(prior.status!==r.status)throw Error('Resume result mismatch')
recovered.push(...prior.traces);groups[i].connections.forEach((c:any)=>solvedNames.add(c.name))
}
if(JSON.stringify(recovered)!==JSON.stringify(saved.traces))throw Error('Resume candidate differs from saved group results')
routes.splice(0,routes.length,...recovered)
}
const report=()=>({status:solvedNames.size===50?'candidate-unvalidated':'partial-routing',expectedNetCount:50,routedNetCount:solvedNames.size,routedTraceCount:routes.length,seedNetCount:seededNames.size,seedTraceCount:seedVirtual.length,seedTraceHash:seed.seedTraceHash,seedBundleHash,captureHash,inputHash,planningMode:fullCopper?'actual-copper-projection':'solid-module-corridors',dataOnly,singleLayer,routeWidth,routingClearance,onlySignals,planningMaxX:planningMaxX?Number(planningMaxX):null,intentionallyUnroutedNetNames,stepMm,groupSeconds,orderingStrategy:ordering,elapsedSeconds:(Date.now()-start)/1000,ordering:groups.map(g=>g.name),results,scope:'Single sequential ordering; all prior host copper frozen, no geometry/angle/connectivity/timing/SI acceptance claimed'})
write(resolve(out,'candidate.virtual.json'),{captureHash,layerSpace:'virtual',traces:routes})
write(resolve(out,'host-routes.virtual.json'),routes)
write(resolve(out,'report.json'),report())
if(process.argv.includes('--prepare-only')){
const prepared={...report(),status:'prepared-not-routed'}
write(resolve(out,'report.json'),prepared)
console.log(JSON.stringify({status:prepared.status,seedNetCount:seededNames.size,seedTraceCount:seedVirtual.length,pendingNetCount:50-seededNames.size,groups:groups.map(g=>({name:g.name,nets:g.connections.map((c:any)=>c.name)})),report:resolve(out,'report.json')}))
process.exit(0)
}
for(const [i,group] of groups.entries()){
if(i<resumeGroupCount)continue
const frozen=freezeHostCopper(routes,stepMm),path=resolve(out,`${String(i).padStart(2,'0')}-${group.name}`)
let current={...input,connections:group.connections.map((c:any)=>({...c,pointsToConnect:c.pointsToConnect.map((p:any)=>({...p,pcb_port_id:p.pointId??p.pcb_port_id}))})),traces:[],obstacles:[...input.obstacles.map((o:any)=>({...o,netIsAssignable:false,offBoardConnectsTo:[]})),...frozen],buses:[{busId:group.name,connectionNames:group.connections.map((c:any)=>c.name),allowedLayers:layers}],defaultObstacleMargin:routingClearance,minTraceWidth:routeWidth,minTraceToPadEdgeClearance:routingClearance,minPadEdgeToPadEdgeClearance:routingClearance,minViaEdgeToPadEdgeClearance:routingClearance,minViaPadDiameter:.4572,minViaHoleDiameter:.254,allowBlindAndBuriedVias:false}
let soleLayer:string|undefined
if(singleLayer){
const requested=[...new Set<string>(group.connections.flatMap((c:any)=>c.pointsToConnect.map((p:any)=>p.layer)))]
if(requested.length!==1)throw Error('Single layer requires identical terminal layers')
soleLayer=requested[0]!
current={...current,layerCount:1,connections:current.connections.map((c:any)=>({...c,pointsToConnect:c.pointsToConnect.map((p:any)=>({...p,layer:'top'}))})),obstacles:current.obstacles.filter((o:any)=>o.layers.includes(soleLayer)).map((o:any)=>({...o,layers:['top']})),buses:current.buses.map((b:any)=>({...b,allowedLayers:['top']}))}
}
write(`${path}.input.json`,current)
const groupStart=Date.now()
const child=Bun.spawn(['bun',new URL(import.meta.url).pathname,'--child',`${path}.input.json`,`${path}.result.json`],{stdout:'ignore',stderr:'pipe'})
let timedOut=false;const timer=setTimeout(()=>{timedOut=true;child.kill('SIGKILL')},groupSeconds*1000)
const exit=await child.exited;clearTimeout(timer)
const stderr=await new Response(child.stderr).text()
let result:any
try{result=JSON.parse(readFileSync(`${path}.result.json`,'utf8'))}catch{result={status:'child-failed'}}
if(timedOut)result={...result,status:'external-time-limit',elapsedSeconds:(Date.now()-groupStart)/1000,traces:[]}
else if(exit!==0)result={...result,status:'child-failed',stderr,traces:[]}
if(result.status==='routed-unvalidated'&&soleLayer){
if(result.traces.some((t:any)=>t.route.some((p:any)=>p.route_type!=='wire'||p.layer!=='top')))result={...result,status:'rejected-single-layer',rejectedTraces:result.traces,traces:[]}
else result.traces=result.traces.map((t:any)=>({...t,route:t.route.map((p:any)=>({...p,layer:soleLayer}))}))
}
if(result.status==='routed-unvalidated'){
const terminalContract=checkHostTerminalContract(result.traces,group.connections)
result={...result,terminalContract}
if(!terminalContract.valid)result={...result,status:'rejected-terminal-contract',error:'Solver moved, omitted, or changed the layer of a declared endpoint beyond .001mm',rejectedTraces:result.traces,traces:[]}
}
if(result.status==='routed-unvalidated'&&planningMaxX){
const invalid=result.traces.some((t:any)=>t.route.some((p:any)=>!Number.isFinite(p.x)||!Number.isFinite(p.y)||p.x<input.bounds.minX||p.x>input.bounds.maxX||p.y<input.bounds.minY||p.y>input.bounds.maxY||(p.route_type==='wire'&&(!Number.isFinite(p.width)||p.width>.4572))||(p.route_type==='via'&&(!Number.isFinite(p.via_diameter??p.outer_diameter??.4572)||(p.via_diameter??p.outer_diameter??.4572)>.4572))))
if(invalid)result={...result,status:'rejected-planning-envelope',error:'Solver emitted copper outside conservative center bounds or radius envelope',rejectedTraces:result.traces,traces:[]}
}
if(result.status==='routed-unvalidated'){
const names=new Set(group.connections.map((c:any)=>c.name))
if(result.traces.some((t:any)=>!names.has(t.connection_name)))throw Error('Solver emitted a route for a foreign net')
if([...names].some(name=>!result.traces.some((t:any)=>t.connection_name===name)))throw Error('Solver success omitted a requested net')
routes.push(...result.traces);group.connections.forEach((c:any)=>solvedNames.add(c.name))
}
const summary={group:group.name,nets:group.connections.map((c:any)=>c.name),status:result.status,phase:result.phase,error:result.error,elapsedSeconds:result.elapsedSeconds,frozenHostObstacleCount:frozen.length}
results.push(summary);write(`${path}.result.json`,result)
verifyDdrCapture(dir)
if(seedPath&&createHash('sha256').update(readFileSync(resolve(seedPath))).digest('hex')!==seedBundleHash)throw Error('Seed source changed during sequential run')
if(createHash('sha256').update(readFileSync(sourceInputPath)).digest('hex')!==inputHash)throw Error('Merged input changed during sequential run')
write(resolve(out,'host-routes.virtual.json'),routes)
write(resolve(out,'candidate.virtual.json'),{captureHash,layerSpace:'virtual',traces:routes})
write(resolve(out,'report.json'),report())
console.log(JSON.stringify({...summary,totalRoutedNets:solvedNames.size}))
}
const final=report();console.log(JSON.stringify({status:final.status,routedNetCount:final.routedNetCount,expectedNetCount:final.expectedNetCount,routedTraceCount:final.routedTraceCount,elapsedSeconds:final.elapsedSeconds,report:resolve(out,'report.json')}))
}