velxio/frontend/src/simulation/spice/unionFind.ts

125 lines
3.7 KiB
TypeScript

/**
* Minimal Union-Find (disjoint set) with a `setCanonical()` extension:
*
* uf.add(key)
* uf.union(a, b)
* uf.find(key) → representative string
* uf.setCanonical(key, name) — pin the representative of key's component
* to an explicit name (e.g. "0" for ground)
*
* Used by the NetlistBuilder to collapse wired pins into named nets.
*/
export class UnionFind {
private parent = new Map<string, string>();
private rank = new Map<string, number>();
private canonical = new Map<string, string>(); // key → forced name
add(key: string): void {
if (!this.parent.has(key)) {
this.parent.set(key, key);
this.rank.set(key, 0);
}
}
find(key: string): string {
this.add(key);
while (this.parent.get(key) !== key) {
const p = this.parent.get(key)!;
this.parent.set(key, this.parent.get(p)!);
key = this.parent.get(key)!;
}
// If this representative has a forced canonical name, return that instead.
const canon = this.canonical.get(key);
return canon ?? key;
}
union(a: string, b: string): void {
this.add(a);
this.add(b);
const ra = this.findRoot(a);
const rb = this.findRoot(b);
if (ra === rb) return;
// If either root has a canonical name, the other adopts it.
const canonA = this.canonical.get(ra);
const canonB = this.canonical.get(rb);
const rankA = this.rank.get(ra)!;
const rankB = this.rank.get(rb)!;
let newRoot: string;
let oldRoot: string;
if (rankA < rankB) {
this.parent.set(ra, rb);
newRoot = rb;
oldRoot = ra;
} else if (rankA > rankB) {
this.parent.set(rb, ra);
newRoot = ra;
oldRoot = rb;
} else {
this.parent.set(rb, ra);
this.rank.set(ra, rankA + 1);
newRoot = ra;
oldRoot = rb;
}
// Propagate canonical name: whichever side had one wins. If both had
// different canonical names, the numerically-smaller / ground wins to
// guarantee determinism (gnd = "0").
const merged = pickCanonical(canonA, canonB);
if (merged !== undefined) this.canonical.set(newRoot, merged);
this.canonical.delete(oldRoot);
}
/**
* Force the set containing `key` to report `name` as its representative.
* Called e.g. with "0" for ground, "vcc_rail" for supply rails.
*/
setCanonical(key: string, name: string): void {
this.add(key);
const root = this.findRoot(key);
const prev = this.canonical.get(root);
this.canonical.set(root, prev !== undefined ? pickCanonical(prev, name)! : name);
}
has(key: string): boolean {
return this.parent.has(key);
}
/** Iterate every (key, representative) pair. */
*entries(): IterableIterator<[string, string]> {
for (const key of this.parent.keys()) {
yield [key, this.find(key)];
}
}
/** All distinct representatives (nets). */
nets(): Set<string> {
const s = new Set<string>();
for (const key of this.parent.keys()) s.add(this.find(key));
return s;
}
private findRoot(key: string): string {
while (this.parent.get(key) !== key) {
const p = this.parent.get(key)!;
this.parent.set(key, this.parent.get(p)!);
key = this.parent.get(key)!;
}
return key;
}
}
/** Ground ("0") always wins over vcc; vcc over auto-named. Deterministic pick. */
function pickCanonical(a: string | undefined, b: string | undefined): string | undefined {
if (a === undefined) return b;
if (b === undefined) return a;
if (a === b) return a;
if (a === '0') return a;
if (b === '0') return b;
if (a.startsWith('vcc')) return a;
if (b.startsWith('vcc')) return b;
// Lexicographically smaller wins (stable)
return a < b ? a : b;
}