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/normalize-ddr-host-routes.ts
import {readFileSync,writeFileSync} from 'node:fs'
import {auditRouteAngles} from './check-route-angles'
type Point={x:number;y:number;[key:string]:any}
const d=(a:Point,b:Point)=>Math.hypot(a.x-b.x,a.y-b.y)
const heading=(a:Point,b:Point)=>Math.atan2(b.y-a.y,b.x-a.x)*180/Math.PI
const turn=(a:number,b:number)=>Math.abs(((a-b+540)%360)-180)
const eq=(a:Point,b:Point)=>d(a,b)<1e-7
function simplify(points:Point[]){const out:Point[]=[];for(const p of points){if(out.length&&eq(out.at(-1)!,p)){out[out.length-1]={...out.at(-1)!,...p};continue;}while(out.length>=2&&turn(heading(out.at(-2)!,out.at(-1)!),heading(out.at(-1)!,p))<1e-5)out.pop();out.push(p)}return out}
/** Chamfer exactly 90-degree corners using equal setbacks, preserving octilinear
* headings. No numerical angle snapping, endpoint movement or via movement.
* Insufficient room or a turn over 90 degrees causes rejection.
*/
function chamfer(points:Point[],minimumLeg:number):Point[]|null{
const p=simplify(points),setbacks=p.map(()=>0)
for(let i=1;i<p.length-1;i++){const t=turn(heading(p[i-1]!,p[i]!),heading(p[i]!,p[i+1]!));if(t>90.0001)return null;if(t>45.0001)setbacks[i]=minimumLeg}
for(let i=1;i<p.length;i++)if(d(p[i-1]!,p[i]!)<setbacks[i-1]!+setbacks[i]!+minimumLeg-1e-7)return null
const out:Point[]=[p[0]!]
for(let i=1;i<p.length-1;i++){const c=p[i]!,s=setbacks[i]!;if(!s){out.push(c);continue}const a=p[i-1]!,b=p[i+1]!;out.push({...c,x:c.x+(a.x-c.x)*s/d(a,c),y:c.y+(a.y-c.y)*s/d(a,c)},{...c,x:c.x+(b.x-c.x)*s/d(b,c),y:c.y+(b.y-c.y)*s/d(b,c)})}
out.push(p.at(-1)!);return simplify(out)
}
function runCandidates(points:Point[],minimumLeg:number):Point[][]{
const clean=simplify(points);if(clean.length<2)return [clean]
let beam:Array<{points:Point[];cost:number}>=[{points:[clean[0]!],cost:0}]
for(let i=1;i<clean.length;i++){
const a=clean[i-1]!,b=clean[i]!,dx=b.x-a.x,dy=b.y-a.y,m=Math.min(Math.abs(dx),Math.abs(dy)),sx=Math.sign(dx),sy=Math.sign(dy)
const options=m<1e-7||Math.abs(Math.abs(dx)-Math.abs(dy))<1e-7?[[b]]:[[{...a,x:a.x+sx*m,y:a.y+sy*m},b],[{...a,x:b.x-sx*m,y:b.y-sy*m},b]]
const next:typeof beam=[]
for(const state of beam)for(const option of options){const proposed=simplify([...state.points,...option]);let cost=0,valid=true;for(let n=1;n<proposed.length-1;n++){const t=turn(heading(proposed[n-1]!,proposed[n]!),heading(proposed[n]!,proposed[n+1]!));if(t>90.0001){valid=false;break}cost+=t*t}if(valid)next.push({points:proposed,cost})}
beam=next.sort((a,b)=>a.cost-b.cost).slice(0,16);if(!beam.length)return []
}
return beam.map(s=>chamfer(s.points,minimumLeg)).filter((p):p is Point[]=>p!==null).filter(p=>auditRouteAngles([{route:p}]).valid)
}
export interface NormalizeOptions{
minimumLegMm?:number
/** Declared outward continuation vectors at actual corridor endpoints. */
terminalDirections?:Array<{x:number;y:number;layer:string;dx:number;dy:number}>
terminalCollarMm?:number
/** Optional complete-trace clearance/endpoint-join predicate. Rejection is
* explicit; this function never labels geometry electrically accepted. */
acceptCandidate?:(trace:any)=>boolean
}
export function normalizeDdrHostRoutes(traces:any[],options:NormalizeOptions={}){
const minimumLeg=options.minimumLegMm??.1016;if(!(minimumLeg>0))throw Error('Positive minimum leg required')
const collarLength=options.terminalCollarMm??.2
if(options.terminalDirections?.length&&(!Number.isFinite(collarLength)||collarLength<minimumLeg))throw Error('Terminal collar must be finite and at least the minimum leg')
for(const v of options.terminalDirections??[]){const angle=Math.atan2(v.dy,v.dx)*180/Math.PI;if(![v.x,v.y,v.dx,v.dy].every(Number.isFinite)||Math.hypot(v.dx,v.dy)<1e-7||Math.abs(angle/45-Math.round(angle/45))>1e-5)throw Error('Terminal direction must be a nonzero octilinear vector')}
const routes:any[]=[],rejected:Array<{traceId:string;reason:string}>=[]
for(const original of traces){const id=original.pcb_trace_id??original.connection_name;let alternatives:any[][]=[[]],run:Point[]=[]
const flush=()=>{if(!run.length)return
const originalStart=run[0]!,originalEnd=run.at(-1)!
const findDirection=(p:Point)=>options.terminalDirections?.find(v=>v.layer===p.layer&&Math.hypot(v.x-p.x,v.y-p.y)<1e-6)
const startDirection=findDirection(originalStart),endDirection=findDirection(originalEnd)
const collar=(p:Point,v:NonNullable<typeof startDirection>)=>{const {start_pcb_port_id,end_pcb_port_id,...q}=p;const n=Math.hypot(v.dx,v.dy);return {...q,x:p.x+v.dx/n*collarLength,y:p.y+v.dy/n*collarLength}}
if(run.length>1&&(startDirection||endDirection))run=[startDirection?collar(originalStart,startDirection):originalStart,...run.slice(1,-1),endDirection?collar(originalEnd,endDirection):originalEnd]
// Shortcuts are considered only with an explicit obstacle/join predicate.
// Preserve each layer-run endpoint, including via entry/exit coordinates.
const variants:Point[][]=[run]
if(options.acceptCandidate&&run.length>2){
variants.push([run[0]!,run.at(-1)!])
const picks=[...new Set([1,Math.floor((run.length-1)/4),Math.floor((run.length-1)/2),Math.floor(3*(run.length-1)/4),run.length-2])].filter(i=>i>0&&i<run.length-1)
for(const i of picks)variants.push([run[0]!,run[i]!,run.at(-1)!])
for(let i=0;i<picks.length-1;i++)variants.push([run[0]!,run[picks[i]!]!,run[picks[i+1]!]!,run.at(-1)!])
for(const stride of [2,3,4])variants.push(run.filter((_,i)=>i===0||i===run.length-1||i%stride===0))
}
const candidates:Point[][]=[],seen=new Set<string>()
// Round-robin variants prevents a 16-way first variant from starving
// useful shortcut families. Final acceptance always sees the full trace.
const families=variants.map(v=>runCandidates(v,minimumLeg).map(core=>[...startDirection?[originalStart]:[],...core,...endDirection?[originalEnd]:[]]).filter(candidate=>auditRouteAngles([{route:candidate}]).valid))
if(options.acceptCandidate&&(startDirection||endDirection)&&run.length>=2){
// Optional endpoint wraps: progress from the outward collar through
// diagonal, perpendicular and inward-diagonal headings. Each immutable
// prefix/suffix step turns 45 degrees; the physical predicate decides
// whether the detour has real space around the package.
const wraps=(p:Point,v:typeof startDirection):Point[][]=>{
const result:Point[][]=[[p]];if(!v)return result
const n=Math.hypot(v.dx,v.dy),ux=v.dx/n,uy=v.dy/n
for(const scale of [.2,.4,.8])for(const side of [-1,1]){
const px=-uy*side,py=ux*side
const a={...p,x:p.x+(ux+px)*scale,y:p.y+(uy+py)*scale}
const b={...p,x:a.x+px*scale,y:a.y+py*scale}
const c={...p,x:b.x+(-ux+px)*scale,y:b.y+(-uy+py)*scale}
result.push([p,a],[p,a,b],[p,a,b,c])
}return result
}
const starts=wraps(run[0]!,startDirection),ends=wraps(run.at(-1)!,endDirection),pairs:Array<[Point[],Point[]]>=[]
for(const a of starts.slice(1))pairs.push([a,ends[0]!])
for(const b of ends.slice(1))pairs.push([starts[0]!,b])
for(let i=1;i<Math.min(starts.length,ends.length);i++){pairs.push([starts[i]!,ends[i]!]);const other=i+((i-1)%6<3?3:-3);if(ends[other])pairs.push([starts[i]!,ends[other]!])}
for(const [a,b]of pairs){
const middleVariants=[[a.at(-1)!,b.at(-1)!]]
if(run.length>2)middleVariants.push([a.at(-1)!,run[Math.floor(run.length/2)]!,b.at(-1)!])
for(const middle of middleVariants)families.push(runCandidates(middle,minimumLeg).map(core=>[...startDirection?[originalStart]:[],...a.slice(0,-1),...core,...b.slice(0,-1).toReversed(),...endDirection?[originalEnd]:[]]).filter(candidate=>auditRouteAngles([{route:candidate}]).valid))
}
}
for(let choice=0;choice<16;choice++)for(const family of families){const candidate=family[choice];if(!candidate)continue;const key=JSON.stringify(candidate.map(p=>[p.x,p.y,p.layer]));if(!seen.has(key)){seen.add(key);candidates.push(candidate)}}
const next:any[][]=[];for(let i=0;i<Math.max(alternatives.length,candidates.length);i++)for(let j=0;j<Math.min(alternatives.length,candidates.length);j++){
const prefix=alternatives.length>=candidates.length?alternatives[i]:alternatives[j],candidate=alternatives.length>=candidates.length?candidates[j]:candidates[i]
if(prefix&&candidate)next.push([...prefix,...candidate]);if(next.length>=128)break
}
alternatives=next.slice(0,128);run=[]
}
for(const p of original.route){if(p.route_type==='wire'){if(run.length&&run[0]!.layer!==p.layer)flush();run.push(structuredClone(p))}else{flush();if(p.route_type!=='via'){alternatives=[];break}alternatives=alternatives.map(a=>[...a,structuredClone(p)])}}
flush()
const candidate=alternatives.map(route=>({...original,route})).find(t=>auditRouteAngles([t]).valid&&(!options.acceptCandidate||options.acceptCandidate(t)))
if(candidate)routes.push(candidate);else rejected.push({traceId:id,reason:'No bounded endpoint-preserving octilinear candidate passed bend/feature/predicate checks'})
}
return {complete:rejected.length===0,routes,rejected,acceptedPhysicalRouting:false,scope:'Candidate geometry only. Full original-copper, net connectivity, via barrel, joined-angle and timing checks remain required.'}
}
if(import.meta.main){const [input,output]=process.argv.slice(2);if(!output)throw Error('Usage: input-host-traces.json output-candidates.json');const result=normalizeDdrHostRoutes(JSON.parse(readFileSync(input!,'utf8')));writeFileSync(output,JSON.stringify(result,null,2)+'\n');console.log(JSON.stringify({complete:result.complete,candidates:result.routes.length,rejected:result.rejected.length}));if(!result.complete)process.exitCode=1}