@wafertools/wafermap 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +16 -5
  2. package/CHANGELOG.md +101 -0
  3. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  4. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  5. package/dist/packages/canvas-adapter/dieList.d.ts +41 -0
  6. package/dist/packages/canvas-adapter/dieList.js +2 -0
  7. package/dist/packages/canvas-adapter/index.d.ts +2 -0
  8. package/dist/packages/canvas-adapter/index.js +1 -1
  9. package/dist/packages/canvas-adapter/maplessSummary.d.ts +34 -0
  10. package/dist/packages/canvas-adapter/maplessSummary.js +1 -0
  11. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +2 -18
  12. package/dist/packages/canvas-adapter/renderWaferGallery.js +8 -8
  13. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +17 -1
  14. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  15. package/dist/packages/canvas-adapter/summaryPanel.d.ts +2 -0
  16. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  17. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  18. package/dist/packages/canvas-adapter/userGuideHtml.js +1 -1
  19. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  20. package/dist/packages/canvas-adapter/version.js +1 -1
  21. package/dist/packages/canvas-adapter/warnings.d.ts +6 -1
  22. package/dist/packages/core/classify.d.ts +6 -2
  23. package/dist/packages/core/dies.d.ts +67 -7
  24. package/dist/packages/core/dies.js +1 -1
  25. package/dist/packages/core/probe.d.ts +5 -0
  26. package/dist/packages/core/probe.js +1 -1
  27. package/dist/packages/core/transforms.d.ts +4 -4
  28. package/dist/packages/core/transforms.js +1 -1
  29. package/dist/packages/renderer/buildView.d.ts +3 -3
  30. package/dist/packages/renderer/buildView.js +1 -1
  31. package/dist/packages/renderer/buildWaferMap.d.ts +25 -8
  32. package/dist/packages/renderer/buildWaferMap.js +1 -1
  33. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  34. package/dist/packages/stats/clusterDetection.d.ts +2 -2
  35. package/dist/packages/stats/patternClassification.d.ts +2 -2
  36. package/dist/packages/stats/regions.d.ts +6 -6
  37. package/dist/packages/stats/regions.js +1 -1
  38. package/dist/packages/stats/renderSummaryReport.js +17 -17
  39. package/llms.txt +1 -1
  40. package/package.json +1 -1
@@ -11,10 +11,19 @@ export interface DieSpec {
11
11
  }
12
12
  export interface Die {
13
13
  id: string;
14
- x: number;
15
- y: number;
16
- physX: number;
17
- physY: number;
14
+ /**
15
+ * Grid position. Absent (together with `y`/`physX`/`physY`) means this die
16
+ * has no reported spatial position at all — it still carries real
17
+ * measured data (`testValues`, `hbin`, …) and counts toward every
18
+ * non-spatial stat, but is never placed on a wafer map/gallery canvas and
19
+ * never enters ring/quadrant/sector/reticle/cluster/pattern analysis. Use
20
+ * `hasPosition()` rather than checking `x`/`y` individually — a die is
21
+ * either fully positioned or fully unpositioned, never half.
22
+ */
23
+ x?: number;
24
+ y?: number;
25
+ physX?: number;
26
+ physY?: number;
18
27
  width: number;
19
28
  height: number;
20
29
  /**
@@ -65,7 +74,52 @@ export interface DieEligibilityOptions {
65
74
  * silently drift apart. Both now call this.
66
75
  */
67
76
  /**
68
- * Return a stable string key for a die — guaranteed format `"x,y"`.
77
+ * Whether a die has a reported spatial position. The single source of truth
78
+ * every spatial function (region builders, cluster/pattern detection, the
79
+ * renderer's grid/geometry inference) filters on — never check `x`/`y`
80
+ * individually elsewhere, since a die is either fully positioned or fully
81
+ * unpositioned (`buildWaferMap` rejects the half-state at build time).
82
+ */
83
+ export declare function hasPosition<T extends {
84
+ x?: number | null;
85
+ y?: number | null;
86
+ }>(die: T): die is T & {
87
+ x: number;
88
+ y: number;
89
+ };
90
+ /**
91
+ * A `Die` known to carry a spatial position — every geometry-only function
92
+ * (ring/quadrant classification, probe-sequence spatial sort, cluster/pattern
93
+ * detection) takes this instead of plain `Die`, so the "caller must filter
94
+ * first" invariant is enforced at the type level rather than by convention.
95
+ * Get one via `dies.filter(isPositionedDie)`.
96
+ */
97
+ export type PositionedDie = Die & {
98
+ x: number;
99
+ y: number;
100
+ physX: number;
101
+ physY: number;
102
+ };
103
+ /**
104
+ * `hasPosition` narrowed for a real `Die`, yielding a `PositionedDie` — i.e.
105
+ * `physX`/`physY` are narrowed too, not just `x`/`y`.
106
+ *
107
+ * Both exist because `hasPosition` is also applied to inputs that carry `x`/`y`
108
+ * and no `physX` at all (a raw `DieResult`, before any layout has happened), so
109
+ * it cannot promise the physical pair. Anything working with built `Die`s wants
110
+ * this one: before it existed every such caller wrote
111
+ * `dies.filter(hasPosition) as PositionedDie[]`, asserting this same invariant
112
+ * by hand — six copies of one cast, each a place the type checker had been told
113
+ * to stop looking.
114
+ *
115
+ * The invariant is real, which is why the runtime check need only look at
116
+ * `x`/`y`: a die is either fully positioned or fully unpositioned, never half
117
+ * (`buildWaferMap` rejects the half-state at build time).
118
+ */
119
+ export declare function isPositionedDie<T extends Die>(die: T): die is T & PositionedDie;
120
+ /**
121
+ * Return a stable string key for a die — guaranteed format `"x,y"` for a
122
+ * positioned die, or `"id:<id>"` for an unpositioned one (see `hasPosition`).
69
123
  *
70
124
  * THE way to build a die key. Use it for Map keys and post-enrichment lookups
71
125
  * rather than an ad-hoc template literal: findings carry `dieKeys` in this exact
@@ -74,14 +128,20 @@ export interface DieEligibilityOptions {
74
128
  * it lives in `core/` — `renderer/`, `stats/` and `canvas-adapter/` all need to
75
129
  * agree on one format.
76
130
  *
131
+ * An unpositioned die has no `x`/`y` to key on — every one would collide on
132
+ * the same key without the `id:` fallback, since `id` is the only field
133
+ * guaranteed unique for a die with no position (`buildWaferMap` assigns
134
+ * unpositioned dies an id like `unpositioned_3`).
135
+ *
77
136
  * ```ts
78
137
  * const map = new Map(result.dies.map(d => [getDieKey(d), d]));
79
138
  * const die = map.get(getDieKey({ x: 3, y: -2 }));
80
139
  * ```
81
140
  */
82
141
  export declare function getDieKey(die: {
83
- x: number;
84
- y: number;
142
+ x?: number;
143
+ y?: number;
144
+ id?: string;
85
145
  }): string;
86
146
  export declare function isYieldEligibleDie(die: Die, options?: DieEligibilityOptions): boolean;
87
147
  /**
@@ -1 +1 @@
1
- export function getDieKey(e){return`${e.x},${e.y}`}export function isYieldEligibleDie(e,t={}){return!(!t.includePartial&&e.partial||!t.includeEdgeExcluded&&e.edgeExcluded)}export function generateDies(e,t){const{width:c,height:l,offset:u={x:0,y:0}}=t,n=t.gridSize??Math.ceil(e.radius/Math.min(c,l))+1,s=[];for(let r=-n;r<=n;r++)for(let i=-n;i<=n;i++){const o=e.center.x+i*c+u.x,d=e.center.y+r*l+u.y;s.push({id:`${i}_${r}`,x:i,y:r,physX:o,physY:d,width:c,height:l})}return s}
1
+ export function hasPosition(e){return e.x!=null&&e.y!=null}export function isPositionedDie(e){return e.x!=null&&e.y!=null}export function getDieKey(e){return hasPosition(e)?`${e.x},${e.y}`:`id:${e.id}`}export function isYieldEligibleDie(e,t={}){return!(!t.includePartial&&e.partial||!t.includeEdgeExcluded&&e.edgeExcluded)}export function generateDies(e,t){const{width:u,height:l,offset:o={x:0,y:0}}=t,i=t.gridSize??Math.ceil(e.radius/Math.min(u,l))+1,c=[];for(let n=-i;n<=i;n++)for(let r=-i;r<=i;r++){const s=e.center.x+r*u+o.x,x=e.center.y+n*l+o.y;c.push({id:`${r}_${n}`,x:r,y:n,physX:s,physY:x,width:u,height:l})}return c}
@@ -11,6 +11,11 @@ export interface ProbeSequenceConfig {
11
11
  * snake — alternating direction per row (boustrophedon)
12
12
  * column — top→bottom, left→right
13
13
  * custom — explicit die ID ordering
14
+ *
15
+ * An unpositioned die (see `hasPosition`) has no spatial order to assign for
16
+ * `row`/`column`/`snake` — it's carried through unchanged (no `probeIndex`
17
+ * set), appended after every positioned die. `custom` is unaffected, since
18
+ * it orders by `id`, not position.
14
19
  */
15
20
  export declare function applyProbeSequence(dies: Die[], config: ProbeSequenceConfig): Die[];
16
21
  //# sourceMappingURL=probe.d.ts.map
@@ -1 +1 @@
1
- export function applyProbeSequence(s,i){const{type:p,customOrder:d}=i;if(p==="custom"){if(!d)throw new Error('customOrder is required for type="custom"');const e=new Map(d.map((t,c)=>[t,c])),r=s.map(t=>({...t,probeIndex:e.get(t.id)})),o=r.filter(t=>t.probeIndex===void 0).map(t=>t.id);if(o.length>0)throw new Error(`applyProbeSequence: die IDs not found in customOrder: ${o.join(", ")}`);return r}if(p==="column")return[...s].sort((r,o)=>r.x-o.x||o.y-r.y).map((r,o)=>({...r,probeIndex:o}));const n=new Map;for(const e of s)n.has(e.y)||n.set(e.y,[]),n.get(e.y).push(e);const a=[...n.entries()].sort(([e],[r])=>r-e),u=[];return a.forEach(([,e],r)=>{const o=e.sort((t,c)=>t.x-c.x);u.push(...p==="snake"&&r%2===1?[...o].reverse():o)}),u.map((e,r)=>({...e,probeIndex:r}))}
1
+ import{hasPosition as a}from"./dies.js";export function applyProbeSequence(s,m){const{type:i,customOrder:c}=m;if(i==="custom"){if(!c)throw new Error('customOrder is required for type="custom"');const o=new Map(c.map((t,p)=>[t,p])),e=s.map(t=>({...t,probeIndex:o.get(t.id)})),r=e.filter(t=>t.probeIndex===void 0).map(t=>t.id);if(r.length>0)throw new Error(`applyProbeSequence: die IDs not found in customOrder: ${r.join(", ")}`);return e}const d=s.filter(a),u=s.filter(o=>!a(o));if(i==="column")return[...[...d].sort((e,r)=>e.x-r.x||r.y-e.y).map((e,r)=>({...e,probeIndex:r})),...u];const n=new Map;for(const o of d)n.has(o.y)||n.set(o.y,[]),n.get(o.y).push(o);const x=[...n.entries()].sort(([o],[e])=>e-o),f=[];return x.forEach(([,o],e)=>{const r=o.sort((t,p)=>t.x-p.x);f.push(...i==="snake"&&e%2===1?[...r].reverse():r)}),[...f.map((o,e)=>({...o,probeIndex:e})),...u]}
@@ -1,5 +1,5 @@
1
1
  import type { Wafer } from './wafer.js';
2
- import type { Die, DieSpec } from './dies.js';
2
+ import type { Die, DieSpec, PositionedDie } from './dies.js';
3
3
  export interface DataRow {
4
4
  [key: string]: string | number;
5
5
  }
@@ -120,16 +120,16 @@ export declare function clipDiesToWafer(dies: Die[], wafer: Wafer, dieConfig?: D
120
120
  * Rotate all die display coordinates by wafer.orientation around wafer.center.
121
121
  * Call this after clipping and data mapping so that i/j indices remain intact.
122
122
  */
123
- export declare function applyOrientation(dies: Die[], wafer: Wafer): Die[];
123
+ export declare function applyOrientation(dies: PositionedDie[], wafer: Wafer): PositionedDie[];
124
124
  /**
125
125
  * Apply interactive transforms (rotation + flip) to die display coordinates.
126
126
  * Rotation is around wafer.center; flip mirrors through wafer.center.
127
127
  * Call this at render time — baseDies already have applyOrientation baked in.
128
128
  */
129
- export declare function transformDies(dies: Die[], options: TransformOptions, center?: {
129
+ export declare function transformDies(dies: PositionedDie[], options: TransformOptions, center?: {
130
130
  x: number;
131
131
  y: number;
132
- }): Die[];
132
+ }): PositionedDie[];
133
133
  /**
134
134
  * Attach data values to dies.
135
135
  * matchBy='xy' — matches by wafer coordinates (v0.1 behaviour)
@@ -1 +1 @@
1
- import{getDieKey as u}from"./dies.js";export function rotatePoint(t,n,e,o=0,s=0){const c=e*Math.PI/180,a=Math.cos(c),i=Math.sin(c),f=t-o,r=n-s;return{x:o+f*a+r*i,y:s-f*i+r*a}}export function affineIdentity(){return{a:1,b:0,c:0,d:1,e:0,f:0}}export function affineRotation(t,n=0,e=0){const o=t*Math.PI/180,s=Math.cos(o),c=Math.sin(o);return{a:s,c,b:-c,d:s,e:n-(s*n+c*e),f:e-(-c*n+s*e)}}export function affineMirror(t,n,e=0,o=0){return{a:t?-1:1,c:0,b:0,d:n?-1:1,e:t?2*e:0,f:n?2*o:0}}export function affineCompose(t,n){return{a:t.a*n.a+t.c*n.b,b:t.b*n.a+t.d*n.b,c:t.a*n.c+t.c*n.d,d:t.b*n.c+t.d*n.d,e:t.a*n.e+t.c*n.f+t.e,f:t.b*n.e+t.d*n.f+t.f}}export function affineInvert(t){const n=t.a*t.d-t.b*t.c,e=t.d/n,o=-t.b/n,s=-t.c/n,c=t.a/n;return{a:e,b:o,c:s,d:c,e:-(e*t.e+s*t.f),f:-(o*t.e+c*t.f)}}export function affinePoint(t,n,e){return{x:t.a*n+t.c*e+t.e,y:t.b*n+t.d*e+t.f}}export function affineVector(t,n,e){return{x:t.a*n+t.c*e,y:t.b*n+t.d*e}}export function affineSwapsAxes(t){return Math.abs(t.a)<1e-9&&Math.abs(t.d)<1e-9&&Math.abs(t.b)>1e-9}function y(t,n){return Math.sqrt(t*t-(n/2)**2)}export function isInsideWafer(t,n,e){const o=t-e.center.x,s=n-e.center.y;if(o*o+s*s>e.radius*e.radius)return!1;if(e.notch){const c=y(e.radius,e.notch.length);if(e.notch.type==="bottom"&&s<-c||e.notch.type==="top"&&s>c||e.notch.type==="left"&&o<-c||e.notch.type==="right"&&o>c)return!1}return!0}export function clipDiesToWafer(t,n,e){const o=[];for(const s of t){const c=isInsideWafer(s.physX,s.physY,n);if(!e){c&&o.push({...s,insideWafer:!0,partial:!1});continue}const a=e.width/2,i=e.height/2,r=[[s.physX-a,s.physY-i],[s.physX+a,s.physY-i],[s.physX+a,s.physY+i],[s.physX-a,s.physY+i]].filter(([h,p])=>isInsideWafer(h,p,n)).length;!c&&r===0||o.push({...s,insideWafer:!0,partial:r<4})}return o}export function applyOrientation(t,n){if(n.orientation===0)return t;const e=affineRotation(n.orientation,n.center.x,n.center.y);return t.map(o=>{const s=affinePoint(e,o.physX,o.physY);return{...o,physX:s.x,physY:s.y}})}export function transformDies(t,n,e={x:0,y:0}){const{rotation:o=0,flipX:s=!1,flipY:c=!1}=n;if(o===0&&!s&&!c)return t;const a=affineCompose(affineMirror(s,c,e.x,e.y),affineRotation(o,e.x,e.y));return t.map(i=>{const f=affinePoint(a,i.physX,i.physY);return{...i,physX:f.x,physY:f.y}})}export function mapDataToDies(t,n,e){const{valueField:o,matchBy:s="xy"}=e,c=new Map,a=r=>{const h=c.get(u(r));if(h===void 0)return{...r};const p=r.testValues??{};return{...r,testValues:{...p,[Object.keys(p).length]:h}}};if(s==="ij"){const r=e.iField??"x",h=e.jField??"y";for(const p of n)c.set(`${+p[r]},${+p[h]}`,+p[o]);return t.map(a)}const i=e.xField??"x",f=e.yField??"y";for(const r of n)c.set(`${+r[i]},${+r[f]}`,+r[o]);return t.map(a)}
1
+ import{getDieKey as l}from"./dies.js";export function rotatePoint(t,n,e,o=0,s=0){const c=e*Math.PI/180,i=Math.cos(c),f=Math.sin(c),a=t-o,r=n-s;return{x:o+a*i+r*f,y:s-a*f+r*i}}export function affineIdentity(){return{a:1,b:0,c:0,d:1,e:0,f:0}}export function affineRotation(t,n=0,e=0){const o=t*Math.PI/180,s=Math.cos(o),c=Math.sin(o);return{a:s,c,b:-c,d:s,e:n-(s*n+c*e),f:e-(-c*n+s*e)}}export function affineMirror(t,n,e=0,o=0){return{a:t?-1:1,c:0,b:0,d:n?-1:1,e:t?2*e:0,f:n?2*o:0}}export function affineCompose(t,n){return{a:t.a*n.a+t.c*n.b,b:t.b*n.a+t.d*n.b,c:t.a*n.c+t.c*n.d,d:t.b*n.c+t.d*n.d,e:t.a*n.e+t.c*n.f+t.e,f:t.b*n.e+t.d*n.f+t.f}}export function affineInvert(t){const n=t.a*t.d-t.b*t.c,e=t.d/n,o=-t.b/n,s=-t.c/n,c=t.a/n;return{a:e,b:o,c:s,d:c,e:-(e*t.e+s*t.f),f:-(o*t.e+c*t.f)}}export function affinePoint(t,n,e){return{x:t.a*n+t.c*e+t.e,y:t.b*n+t.d*e+t.f}}export function affineVector(t,n,e){return{x:t.a*n+t.c*e,y:t.b*n+t.d*e}}export function affineSwapsAxes(t){return Math.abs(t.a)<1e-9&&Math.abs(t.d)<1e-9&&Math.abs(t.b)>1e-9}function d(t,n){return Math.sqrt(t*t-(n/2)**2)}export function isInsideWafer(t,n,e){const o=t-e.center.x,s=n-e.center.y;if(o*o+s*s>e.radius*e.radius)return!1;if(e.notch){const c=d(e.radius,e.notch.length);if(e.notch.type==="bottom"&&s<-c||e.notch.type==="top"&&s>c||e.notch.type==="left"&&o<-c||e.notch.type==="right"&&o>c)return!1}return!0}export function clipDiesToWafer(t,n,e){const o=[];for(const s of t){const c=s.physX,i=s.physY,f=isInsideWafer(c,i,n);if(!e){f&&o.push({...s,insideWafer:!0,partial:!1});continue}const a=e.width/2,r=e.height/2,p=[[c-a,i-r],[c+a,i-r],[c+a,i+r],[c-a,i+r]].filter(([h,y])=>isInsideWafer(h,y,n)).length;!f&&p===0||o.push({...s,insideWafer:!0,partial:p<4})}return o}export function applyOrientation(t,n){if(n.orientation===0)return t;const e=affineRotation(n.orientation,n.center.x,n.center.y);return t.map(o=>{const s=affinePoint(e,o.physX,o.physY);return{...o,physX:s.x,physY:s.y}})}export function transformDies(t,n,e={x:0,y:0}){const{rotation:o=0,flipX:s=!1,flipY:c=!1}=n;if(o===0&&!s&&!c)return t;const i=affineCompose(affineMirror(s,c,e.x,e.y),affineRotation(o,e.x,e.y));return t.map(f=>{const a=affinePoint(i,f.physX,f.physY);return{...f,physX:a.x,physY:a.y}})}export function mapDataToDies(t,n,e){const{valueField:o,matchBy:s="xy"}=e,c=new Map,i=r=>{const u=c.get(l(r));if(u===void 0)return{...r};const p=r.testValues??{};return{...r,testValues:{...p,[Object.keys(p).length]:u}}};if(s==="ij"){const r=e.iField??"x",u=e.jField??"y";for(const p of n)c.set(`${+p[r]},${+p[u]}`,+p[o]);return t.map(i)}const f=e.xField??"x",a=e.yField??"y";for(const r of n)c.set(`${+r[f]},${+r[a]}`,+r[o]);return t.map(i)}
@@ -1,5 +1,5 @@
1
1
  import type { Wafer } from '../core/wafer.js';
2
- import type { Die } from '../core/dies.js';
2
+ import type { Die, PositionedDie } from '../core/dies.js';
3
3
  import type { Reticle } from '../core/reticle.js';
4
4
  import type { DieMetadata, WaferMetadata } from '../core/metadata.js';
5
5
  import { type Affine } from '../core/transforms.js';
@@ -382,7 +382,7 @@ export interface MapTitleParts {
382
382
  * the bin on single-bin stacked cards.
383
383
  */
384
384
  export declare function buildMapTitle(view: View, fallbackFormat?: 'si' | 'engineering', binDefs?: BinDef[]): MapTitleParts;
385
- export declare function generateTextOverlay(dies: Die[], txCoords: Float64Array | null, options: {
385
+ export declare function generateTextOverlay(dies: PositionedDie[], txCoords: Float64Array | null, options: {
386
386
  plotMode: PlotMode;
387
387
  colorFns: ColorFns;
388
388
  normalize: (v: number) => number;
@@ -403,7 +403,7 @@ export declare function generateTextOverlay(dies: Die[], txCoords: Float64Array
403
403
  * buildView(wafer, dies, { plotMode: 'hardBin', reticles })
404
404
  * ```
405
405
  */
406
- export declare function buildView(wafer: Wafer, dies: Die[], options?: ViewOptions, binDefs?: {
406
+ export declare function buildView(wafer: Wafer, dies: PositionedDie[], options?: ViewOptions, binDefs?: {
407
407
  hbinDefs?: BinDef[];
408
408
  sbinDefs?: BinDef[];
409
409
  metadataFields?: MetadataFieldDef[];
@@ -1 +1 @@
1
- import{getReticleCell as Qt}from"../core/reticle.js";import{affineRotation as ft,affineMirror as Vt,affineCompose as at,affinePoint as X,affineVector as dt,affineSwapsAxes as Ut}from"../core/transforms.js";import{contrastTextColor as Y,SPEC_PASS_FILL as ht,SPEC_FAIL_LOW as Jt,SPEC_FAIL_HIGH as pt}from"./colorMap.js";import{getColorScheme as Zt}from"./colorSchemes.js";import{getDieTestValue as R,getTestPassStatus as G,isParametricTest as mt}from"./buildWaferMap.js";import{fmt as yt,fmtColorbarAxis as gt,fmtAggregationMethod as Rt}from"./fmt.js";import{metadataValueColor as te}from"./colorMap.js";import{compareNatural as ee,clamp01 as Ct}from"../core/utils.js";import{prettyKey as ne}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function findTestDef(t,e){return t?.find(n=>n.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,n)=>e-n)}const ae="#d3d6db",Nt="#e8e9ea",ie="#eceef0",w="#d6d9dd";function it(t,e){const n=e?t.metadata?.[e]:void 0;return n!=null&&(typeof n=="string"||typeof n=="number"||typeof n=="boolean")?String(n):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function se(t,e,n){const{x:i,y:s}=t.center,a=ft(t.orientation,i,s),o=at(Vt(e?.x??!1,e?.y??!1,i,s),ft(t.orientation,i,s)),l=at(Vt(n?.flipX??!1,n?.flipY??!1,i,s),ft(n?.rotation??0,i,s));return{bakedToScreen:l,physicalToScreen:at(l,a),gridToScreen:at(l,o)}}function _(t,e=!1){return{points:[t],closed:e}}function oe(t,e){const{center:n,radius:i,notch:s}=t;let a=n.x+i*Math.cos(e),o=n.y+i*Math.sin(e);if(!s)return{x:a,y:o};const l=Math.sqrt(i**2-(s.length/2)**2),f=s.length/2,y=a-n.x,m=o-n.y;return s.type==="bottom"&&m<-l?(o=n.y-l,a=n.x+Math.max(-f,Math.min(f,y))):s.type==="top"&&m>l?(o=n.y+l,a=n.x+Math.max(-f,Math.min(f,y))):s.type==="left"&&y<-l?(a=n.x-l,o=n.y+Math.max(-f,Math.min(f,m))):s.type==="right"&&y>l&&(a=n.x+l,o=n.y+Math.max(-f,Math.min(f,m))),{x:a,y:o}}function Ht(t,e){return t.map(e).join(" / ")}function re(t,e=256){const n=Array.from({length:e},(i,s)=>t(s/(e-1)));return i=>n[Math.min(e-1,Math.max(0,Math.round(i*(e-1))))]}function le(t,e){const n=Math.max(1,Math.min(t.width,t.height)),i=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(n*.55,i*1.8))))}function ce(t,e,n){if(e?.length){const i=e.flatMap(s=>{const a=s.testNumber;if(!mt(s)){const f=G(t,a,s);return f===void 0?[]:[{key:a,label:s.name,value:f?"Pass":"Fail"}]}const o=R(t,a);if(o===void 0)return[];const l=t.testPass?.[a]===!1?" <i>(recorded fail)</i>":"";return[{key:a,label:s.name,value:`${yt(o,s.unit,n)}${l}`}]});if(i.length)return i}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:yt(s,void 0,n)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,n={}){const{testDefs:i,hbinDefs:s,sbinDefs:a,fallbackFormat:o,aggrMethod:l,lotSize:f,waferMeta:y,activeTest:m,reticleConfig:$}=n,D=s?new Map(s.map(r=>[r.bin,r])):null,v=a?new Map(a.map(r=>[r.bin,r])):null,x=[`Die (${t.x}, ${t.y})`];if($){const r=Qt(t,$);x.push(`Reticle (${r.column}, ${r.row})`)}if(e==="stackedValues"){const r=R(t,0);if(r!==void 0){const c=i?.[0],p=c?.testNumber,h=c?.name??(p!=null?`Test ${p}`:"Value"),M=l?` (${l})`:"";x.push(`${h}${M}: ${yt(r,c?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const r=R(t,0),c=e==="stackedSoftBins"?t.sbin:t.hbin;if(r!==void 0){const p=e==="stackedSoftBins"?v:D,h=c!==void 0?p?.get(c)?.name?`${c} \xB7 ${p.get(c).name}`:`Bin ${c}`:"Bin";let M;if(l==="percent")M=`${r.toFixed(0)}%`;else{const b=f?` (${(r/f*100).toFixed(0)}% of lot)`:"";M=`${r}${b}`}const T=l?` [${Rt(l)}]`:"";x.push(`${h}: ${M}${T}`)}}else{const r=ce(t,i,o);if(e==="value"&&r.length){const{testNumber:c}=resolveTestNumber(m??0,i),p=findTestDef(i,c),h=R(t,c);let M=r.findIndex(A=>A.key===c);M<0&&(M=0);const T=r[M];let b=`<b>${T.label}: ${T.value}</b>`;if(T.key===c&&p){const A=classifySpec(h,p);(A==="failLow"||A==="failHigh")&&(b+=" <i>(out of spec)</i>")}x.push(b);const O=r.length-1;O>0&&x.push(`<i>+${O} more test${O===1?"":"s"}</i>`)}else if(r.length){const c=r.length;x.push(`<i>${c} test result${c===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const c=[];if(t.hbin!==void 0){const p=D?.get(t.hbin)?.name,h=p?`${t.hbin} \xB7 ${p}`:String(t.hbin);c.push(`HBin: ${h}`)}if(t.sbin!==void 0){const p=v?.get(t.sbin)?.name,h=p?`${t.sbin} \xB7 ${p}`:String(t.sbin);c.push(`SBin: ${h}`)}x.push(c.join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&x.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&x.push(`Site: ${t.siteNum}`),t.partId!==void 0&&x.push(`Part ID: ${t.partId}`),t.partial&&x.push("<i>partial die</i>"),t.probeIndex!==void 0&&x.push(`Probe: #${t.probeIndex}`);const k={...y??{},...t.metadata??{}};for(const[r,c]of Object.entries(k))c!=null&&x.push(`${r}: ${String(c)}`);return x.join("<br>")}export function buildMapTitle(t,e="engineering",n){const i=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=a=>{const o=n?.length===1?n[0]:void 0;return o===void 0?`${a} Bin`:o.name?`${a} Bin ${o.bin} \xB7 ${o.name}`:`${a} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:i};case"stackedSoftBins":return{primary:s("Soft"),secondary:i};case"metadata":{const a=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?ne(t.activeMetadataKey):"Metadata";return{primary:a?.label??o,secondary:""}}case"stackedValues":{const a=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=gt(o,a?.name,a?.unit,e),f=l||"Value",y=t.aggrMethod?` \xB7 ${Rt(t.aggrMethod)}`:"";return{primary:`${f}${y}`,secondary:i}}default:{const a=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=gt(o,a?.name,a?.unit,e),f=l||a?.name;if(t.passFailDisplay!=="off"){const y=a?.testNumber??t.activeTest,m=f?`${f} \xB7 #${y}`:`Test ${y}`,$=t.passFailDisplay==="spec"?"Spec pass/fail":a!==void 0&&!mt(a)?"Functional pass/fail":"Tester pass/fail";return{primary:m,secondary:$}}return{primary:f??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,n){const{plotMode:i,colorFns:s,normalize:a,activeTest:o,valueRange:l,testDefs:f,fallbackFormat:y,passFailDisplay:m="off",activeMetadataKey:$,metadataColorMap:D}=n,v=findTestDef(f,o),{tickFmt:x}=gt(l[1],v?.name,v?.unit,y),{testNumber:k}=resolveTestNumber(o,f);return t.flatMap((r,c)=>{let p="",h="#111111";if(i==="value"&&m==="test"){const b=G(r,k,v);if(b===void 0)return[];p=b?"P":"F",h=Y(b?ht:pt)}else if(i==="value"){const b=R(r,k);if(b===void 0)return[];p=Ht([b],x),h=Y(s.forValue(a(b)))}else if(i==="hardBin"||i==="softBin"){const b=i==="softBin"?r.sbin:r.hbin;if(b===void 0)return[];p=String(b),h=Y(s.forBin(b))}else if(i==="metadata"){const b=it(r,$);if(b===void 0)return[];p=b,h=Y(D?.get(p)??w)}else{const b=R(r,0);if(b===void 0)return[];p=Ht([b],x),h=Y(s.forValue(a(b)))}const M=e?e[c*2]:r.physX,T=e?e[c*2+1]:r.physY;return[{x:M,y:T,text:p,fontSize:le(r,p),color:h,align:"center"}]})}function Ot(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function ue(t,e,n=720){const{center:i,radius:s,notch:a}=t,o=k=>X(e,k.x,k.y);if(!(a!==void 0&&t.diameter>150)){const k=[];for(let r=0;r<=n;r++){const c=2*Math.PI*r/n;k.push(o(oe(t,c)))}return[{kind:"wafer-boundary",..._(k,!0),lineColor:"#888888",lineWidth:1}]}const f=1.25,y=a.length,m=Ot(a.type),$=Math.atan2(y,s),D=s-f,v=[];let x=!1;for(let k=0;k<=n;k++){const r=2*Math.PI*k/n;let c=r-m;for(;c>Math.PI;)c-=2*Math.PI;for(;c<-Math.PI;)c+=2*Math.PI;if(Math.abs(c)<=$){if(!x){x=!0;const p={x:i.x+s*Math.cos(m-$),y:i.y+s*Math.sin(m-$)},h={x:i.x+D*Math.cos(m),y:i.y+D*Math.sin(m)},M={x:i.x+s*Math.cos(m+$),y:i.y+s*Math.sin(m+$)};v.push(o(p),o(h),o(M))}continue}v.push(o({x:i.x+s*Math.cos(r),y:i.y+s*Math.sin(r)}))}return[{kind:"wafer-boundary",..._(v,!0),lineColor:"#888888",lineWidth:1}]}function fe(t,e,n,i=360){const s=[],a=Math.max(1,Math.floor(n));for(let o=1;o<a;o++){const l=t.radius*o/a,f=[];for(let y=0;y<=i;y++){const m=2*Math.PI*y/i,$={x:t.center.x+l*Math.cos(m),y:t.center.y+l*Math.sin(m)};f.push(X(e,$.x,$.y))}s.push({kind:"ring-boundary",..._(f,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function de(t,e,n,i){const{x:s,y:a}=t.center,o=t.radius,l=(r,c)=>X(e,r,c),f=n-s,y=Math.sqrt(Math.max(0,o*o-f*f)),m=l(n,a-y),$=l(n,a+y),D=i-a,v=Math.sqrt(Math.max(0,o*o-D*D)),x=l(s-v,i),k=l(s+v,i);return[{kind:"quadrant-boundary",..._([m,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",..._([x,k]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function he(t,e){return t.map(n=>{const i=n.width/2,s=n.height/2;return{kind:"reticle",points:[[[n.x-i,n.y-s],[n.x+i,n.y-s],[n.x+i,n.y+s],[n.x-i,n.y+s]].map(([o,l])=>X(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function pe(t){const e=t.filter(n=>n.probeIndex!==void 0).sort((n,i)=>(n.probeIndex??0)-(i.probeIndex??0));return e.length?[{kind:"probe-path",..._(e.map(n=>({x:n.physX,y:n.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function me(t,e,n,i,s,a,o,l,f,y,m,$,D,v="off",x,k,r){const c=e.width-o,p=e.height-o,h=a?p:c,M=a?c:p,T=B=>s==="softBin"?B.sbin:B.hbin,b=B=>it(B,x);if(e.partial){t.push({x:n,y:i,width:h,height:M,fill:ae,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:n,y:i,width:h,height:M,fill:ie,type:"stacked",metadata:e.metadata});return}if(f!==void 0&&(s==="hardBin"||s==="softBin")&&T(e)!==f){t.push({x:n,y:i,width:h,height:M,fill:Nt,type:"hardBin",metadata:e.metadata});return}if(r!==void 0&&s==="metadata"&&b(e)!==r){t.push({x:n,y:i,width:h,height:M,fill:Nt,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(v==="test"){const U=G(e,m,D),st=U===void 0?w:U?ht:pt;t.push({x:n,y:i,width:h,height:M,fill:st,type:"value",metadata:e.metadata});return}const B=R(e,m),P=classifySpec(B,D);let W,Q;B===void 0?W=w:v==="spec"?W=P==="failLow"?Jt:P==="failHigh"?pt:ht:(W=l.forValue(y(B)),(P==="failLow"||P==="failHigh")&&(Q=P)),t.push({x:n,y:i,width:h,height:M,fill:W,type:"value",specMark:Q,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const B=T(e),P=B!=null?l.forBin(B):w;t.push({x:n,y:i,width:h,height:M,fill:P,type:s,metadata:e.metadata});return}if(s==="metadata"){const B=b(e),P=B!==void 0?k?.get(B)??w:w;t.push({x:n,y:i,width:h,height:M,fill:P,type:"metadata",metadata:e.metadata});return}const O=R(e,0),A=O!==void 0?l.forValue(y(O)):w;t.push({x:n,y:i,width:h,height:M,fill:A,type:"value",metadata:e.metadata})}function ye(t,e,n){const i=t.radius*.15,s=dt(e,i,0),a=dt(e,0,i),o=s.x+a.x>=0?-1:1,l=s.y+a.y>=0?-1:1,f={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},y={x:f.x+s.x,y:f.y+s.y},m={x:f.x+a.x,y:f.y+a.y};return n.push({x:y.x+s.x*.35,y:y.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:m.x+a.x*.35,y:m.y+a.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",..._([f,y]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",..._([f,m]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,n={},i){const s=n.reticles??[],{plotMode:a="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:f=!1,ringCount:y=4,showRingBoundaries:m=!1,showQuadrantBoundaries:$=!1,showXYIndicator:D=!1,dieGap:v=1,colorScheme:x="default",highlightBin:k,highlightMetadataValue:r,interactiveTransform:c,valueRange:p,testDefs:h,activeTest:M=0,activeMetadataKey:T,fallbackFormat:b="engineering",aggregationMethod:O,lotSize:A,dataAxisFlip:B,isLotStack:P=!1,logScale:W,colorbarRangeMode:Q="spec",passFailDisplay:U,showPartialDies:st=!0}=n,xt=U??"off",bt=i?.hbinDefs,Mt=i?.sbinDefs,St=i?.metadataFields,At=St?.find(u=>u.key===T),Et={x:(B?.x??!1)!==(c?.flipX??!1),y:(B?.y??!1)!==(c?.flipY??!1)},_t=bt?new Map(bt.map(u=>[u.bin,u])):null,wt=Mt?new Map(Mt.map(u=>[u.bin,u])):null,ot=a==="softBin"?wt:_t,rt=Zt(x),$t={forValue:re(rt.forValue),forBin:x==="custom"&&ot?u=>ot.get(u)?.color??rt.forBin(u):rt.forBin};let{testNumber:H}=resolveTestNumber(M,h);if(!h?.length&&a==="value"&&!e.some(d=>d.testValues&&H in d.testValues)){const d=e.reduce((g,L)=>{if(!L.testValues)return g;const S=Object.keys(L.testValues).map(Number),K=S.length?Math.min(...S):void 0;return K!==void 0&&(g===void 0||K<g)?K:g},void 0);d!==void 0&&(H=d)}let J;if(Array.isArray(p))J=p;else if(p){const{testNumber:u}=resolveTestNumber(p.test,h);J=u===H?p.range:void 0}const I=findTestDef(h,H),kt=a==="value"&&I!==void 0&&!mt(I),Wt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let C="off";a==="value"&&(kt?C="test":xt==="spec"&&Wt?C="spec":xt==="test"&&e.some(u=>G(u,H,I)!==void 0)&&(C="test"));const Bt=C==="spec"?"spec":Q;let V,N,vt=!1;if(J)[V,N]=J;else{const u=a==="stackedValues"||a==="stackedBins"||a==="stackedSoftBins",d=!u&&a==="value"&&Bt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let g=1/0,L=-1/0,S=!0;for(const K of e){const q=u?R(K,0):R(K,H);q!==void 0&&(q<g&&(g=q),q>L&&(L=q),Number.isInteger(q)||(S=!1))}isFinite(g)||(S=!1),vt=S,d?(V=I.limitLow!==void 0?I.limitLow:isFinite(g)?g:0,N=I.limitHigh!==void 0?I.limitHigh:isFinite(L)?L:1):(V=isFinite(g)?g:0,N=isFinite(L)?L:1)}V===N&&(N=V+1);const Tt=kt?!1:W??I?.logScale??!1,It=Tt&&V>0&&N>0,jt=It;let Z;if(It){const u=Math.log10(V),d=Math.log10(N)-u;Z=g=>g<=0?0:Ct((Math.log10(g)-u)/d)}else Z=u=>Ct((u-V)/(N-V));const F=se(t,B,c);let Lt=null;if(t.notch){const u=Ot(t.notch.type),d=dt(F.physicalToScreen,Math.cos(u)*t.radius,Math.sin(u)*t.radius),g=Math.hypot(d.x,d.y);g>0&&(Lt={x:d.x/g,y:d.y/g})}let j=1/0;for(let u=0;u<e.length;u++){const d=e[u];d.width<j&&(j=d.width),d.height<j&&(j=d.height)}const zt=j!==1/0?Math.min(v,j*.12):v,Dt=[],lt=[],Kt=F.bakedToScreen.a===1&&F.bakedToScreen.b===0&&F.bakedToScreen.c===0&&F.bakedToScreen.d===1&&F.bakedToScreen.e===0&&F.bakedToScreen.f===0;let E=null;if(!Kt){E=new Float64Array(e.length*2);for(let u=0;u<e.length;u++){const d=X(F.bakedToScreen,e[u].physX,e[u].physY);E[u*2]=d.x,E[u*2+1]=d.y}}const qt=Ut(F.gridToScreen),Xt=a==="hardBin"||a==="softBin"||a==="stackedBins"||a==="stackedSoftBins",Yt=a==="softBin"||a==="stackedSoftBins";let tt=Xt?new Map:void 0;const ct=a==="value"&&C==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ut=a==="value"&&C==="test"?{pass:0,fail:0}:void 0,Ft=a==="metadata";let et=Ft?new Map:void 0,nt=null;if(Ft&&T){const u=new Set;for(const g of e){if(g.partial||g.edgeExcluded)continue;const L=it(g,T);L!==void 0&&u.add(L)}const d=[...u].sort(ee);nt=new Map(d.map((g,L)=>[g,At?.values?.find(S=>S.value===g)?.color??te(L)]))}for(let u=0;u<e.length;u++){const d=e[u],g=E?E[u*2]:d.physX,L=E?E[u*2+1]:d.physY;if(lt.push({x:g,y:L}),tt&&!d.partial&&!d.edgeExcluded){const S=Yt?d.sbin:d.hbin;S!=null&&tt.set(S,(tt.get(S)??0)+1)}if(ct&&!d.partial&&!d.edgeExcluded){const S=classifySpec(R(d,H),I);S&&ct[S]++}if(ut&&!d.partial&&!d.edgeExcluded){const S=G(d,H,I);S!==void 0&&ut[S?"pass":"fail"]++}if(et&&!d.partial&&!d.edgeExcluded){const S=it(d,T);S!==void 0&&et.set(S,(et.get(S)??0)+1)}d.partial&&!st||me(Dt,d,g,L,a,qt,zt,$t,k,Z,H,ot,I,C,T,nt,r)}const Pt=o?generateTextOverlay(e,E,{plotMode:a,colorFns:$t,normalize:Z,activeTest:M,valueRange:[V,N],testDefs:h,fallbackFormat:b,passFailDisplay:C,activeMetadataKey:T,metadataColorMap:nt}):[],z=ue(t,F.physicalToScreen);m&&z.push(...fe(t,F.physicalToScreen,y)),$&&z.push(...de(t,F.physicalToScreen,t.center.x,t.center.y)),l&&z.push(...he(s,F.gridToScreen)),f&&z.push(...pe(e)),D&&z.push(...ye(t,F.gridToScreen,Pt));const Gt=lt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Dt,hoverPoints:lt,texts:Pt,overlays:z,plotMode:a,colorScheme:x,metadata:t.metadata??null,dies:e,valueRange:[V,N],allIntegerValues:vt,testDefs:h,activeTest:M,logScale:jt,logScaleRequested:Tt,aggrMethod:O,lotSize:A,axisFlip:Et,dataAxisFlip:{x:B?.x??!1,y:B?.y??!1},gridToScreen:F.gridToScreen,rotation:((t.orientation+(c?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:P,passFailDisplay:C,colorBySpec:C==="spec",colorbarRangeMode:Bt,waferCenter:t.center,waferRadius:t.radius,notchDir:Lt,binCounts:tt,metadataCounts:et,metadataColorMap:nt,activeMetadataKey:T,metadataFields:St,specCounts:ct,passFailCounts:ut,dieBounds:Gt}}export{getDieKey}from"../core/dies.js";
1
+ import{hasPosition as Qt}from"../core/dies.js";import{getReticleCell as Ut}from"../core/reticle.js";import{affineRotation as ft,affineMirror as Vt,affineCompose as at,affinePoint as X,affineVector as dt,affineSwapsAxes as Jt}from"../core/transforms.js";import{contrastTextColor as Y,SPEC_PASS_FILL as ht,SPEC_FAIL_LOW as Zt,SPEC_FAIL_HIGH as pt}from"./colorMap.js";import{getColorScheme as te}from"./colorSchemes.js";import{getDieTestValue as R,getTestPassStatus as G,isParametricTest as mt}from"./buildWaferMap.js";import{fmt as yt,fmtColorbarAxis as gt,fmtAggregationMethod as Rt}from"./fmt.js";import{metadataValueColor as ee}from"./colorMap.js";import{compareNatural as ne,clamp01 as Ct}from"../core/utils.js";import{prettyKey as ae}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function findTestDef(t,e){return t?.find(n=>n.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,n)=>e-n)}const ie="#d3d6db",Nt="#e8e9ea",se="#eceef0",w="#d6d9dd";function it(t,e){const n=e?t.metadata?.[e]:void 0;return n!=null&&(typeof n=="string"||typeof n=="number"||typeof n=="boolean")?String(n):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function oe(t,e,n){const{x:i,y:s}=t.center,a=ft(t.orientation,i,s),o=at(Vt(e?.x??!1,e?.y??!1,i,s),ft(t.orientation,i,s)),l=at(Vt(n?.flipX??!1,n?.flipY??!1,i,s),ft(n?.rotation??0,i,s));return{bakedToScreen:l,physicalToScreen:at(l,a),gridToScreen:at(l,o)}}function _(t,e=!1){return{points:[t],closed:e}}function re(t,e){const{center:n,radius:i,notch:s}=t;let a=n.x+i*Math.cos(e),o=n.y+i*Math.sin(e);if(!s)return{x:a,y:o};const l=Math.sqrt(i**2-(s.length/2)**2),f=s.length/2,y=a-n.x,m=o-n.y;return s.type==="bottom"&&m<-l?(o=n.y-l,a=n.x+Math.max(-f,Math.min(f,y))):s.type==="top"&&m>l?(o=n.y+l,a=n.x+Math.max(-f,Math.min(f,y))):s.type==="left"&&y<-l?(a=n.x-l,o=n.y+Math.max(-f,Math.min(f,m))):s.type==="right"&&y>l&&(a=n.x+l,o=n.y+Math.max(-f,Math.min(f,m))),{x:a,y:o}}function Ht(t,e){return t.map(e).join(" / ")}function le(t,e=256){const n=Array.from({length:e},(i,s)=>t(s/(e-1)));return i=>n[Math.min(e-1,Math.max(0,Math.round(i*(e-1))))]}function ce(t,e){const n=Math.max(1,Math.min(t.width,t.height)),i=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(n*.55,i*1.8))))}function ue(t,e,n){if(e?.length){const i=e.flatMap(s=>{const a=s.testNumber;if(!mt(s)){const f=G(t,a,s);return f===void 0?[]:[{key:a,label:s.name,value:f?"Pass":"Fail"}]}const o=R(t,a);if(o===void 0)return[];const l=t.testPass?.[a]===!1?" <i>(recorded fail)</i>":"";return[{key:a,label:s.name,value:`${yt(o,s.unit,n)}${l}`}]});if(i.length)return i}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:yt(s,void 0,n)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,n={}){const{testDefs:i,hbinDefs:s,sbinDefs:a,fallbackFormat:o,aggrMethod:l,lotSize:f,waferMeta:y,activeTest:m,reticleConfig:$}=n,P=s?new Map(s.map(r=>[r.bin,r])):null,v=a?new Map(a.map(r=>[r.bin,r])):null,x=[`Die (${t.x}, ${t.y})`];if($&&Qt(t)){const r=Ut(t,$);x.push(`Reticle (${r.column}, ${r.row})`)}if(e==="stackedValues"){const r=R(t,0);if(r!==void 0){const c=i?.[0],p=c?.testNumber,h=c?.name??(p!=null?`Test ${p}`:"Value"),M=l?` (${l})`:"";x.push(`${h}${M}: ${yt(r,c?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const r=R(t,0),c=e==="stackedSoftBins"?t.sbin:t.hbin;if(r!==void 0){const p=e==="stackedSoftBins"?v:P,h=c!==void 0?p?.get(c)?.name?`${c} \xB7 ${p.get(c).name}`:`Bin ${c}`:"Bin";let M;if(l==="percent")M=`${r.toFixed(0)}%`;else{const b=f?` (${(r/f*100).toFixed(0)}% of lot)`:"";M=`${r}${b}`}const T=l?` [${Rt(l)}]`:"";x.push(`${h}: ${M}${T}`)}}else{const r=ue(t,i,o);if(e==="value"&&r.length){const{testNumber:c}=resolveTestNumber(m??0,i),p=findTestDef(i,c),h=R(t,c);let M=r.findIndex(A=>A.key===c);M<0&&(M=0);const T=r[M];let b=`<b>${T.label}: ${T.value}</b>`;if(T.key===c&&p){const A=classifySpec(h,p);(A==="failLow"||A==="failHigh")&&(b+=" <i>(out of spec)</i>")}x.push(b);const O=r.length-1;O>0&&x.push(`<i>+${O} more test${O===1?"":"s"}</i>`)}else if(r.length){const c=r.length;x.push(`<i>${c} test result${c===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const c=[];if(t.hbin!==void 0){const p=P?.get(t.hbin)?.name,h=p?`${t.hbin} \xB7 ${p}`:String(t.hbin);c.push(`HBin: ${h}`)}if(t.sbin!==void 0){const p=v?.get(t.sbin)?.name,h=p?`${t.sbin} \xB7 ${p}`:String(t.sbin);c.push(`SBin: ${h}`)}x.push(c.join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&x.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&x.push(`Site: ${t.siteNum}`),t.partId!==void 0&&x.push(`Part ID: ${t.partId}`),t.partial&&x.push("<i>partial die</i>"),t.probeIndex!==void 0&&x.push(`Probe: #${t.probeIndex}`);const k={...y??{},...t.metadata??{}};for(const[r,c]of Object.entries(k))c!=null&&x.push(`${r}: ${String(c)}`);return x.join("<br>")}export function buildMapTitle(t,e="engineering",n){const i=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=a=>{const o=n?.length===1?n[0]:void 0;return o===void 0?`${a} Bin`:o.name?`${a} Bin ${o.bin} \xB7 ${o.name}`:`${a} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:i};case"stackedSoftBins":return{primary:s("Soft"),secondary:i};case"metadata":{const a=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?ae(t.activeMetadataKey):"Metadata";return{primary:a?.label??o,secondary:""}}case"stackedValues":{const a=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=gt(o,a?.name,a?.unit,e),f=l||"Value",y=t.aggrMethod?` \xB7 ${Rt(t.aggrMethod)}`:"";return{primary:`${f}${y}`,secondary:i}}default:{const a=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=gt(o,a?.name,a?.unit,e),f=l||a?.name;if(t.passFailDisplay!=="off"){const y=a?.testNumber??t.activeTest,m=f?`${f} \xB7 #${y}`:`Test ${y}`,$=t.passFailDisplay==="spec"?"Spec pass/fail":a!==void 0&&!mt(a)?"Functional pass/fail":"Tester pass/fail";return{primary:m,secondary:$}}return{primary:f??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,n){const{plotMode:i,colorFns:s,normalize:a,activeTest:o,valueRange:l,testDefs:f,fallbackFormat:y,passFailDisplay:m="off",activeMetadataKey:$,metadataColorMap:P}=n,v=findTestDef(f,o),{tickFmt:x}=gt(l[1],v?.name,v?.unit,y),{testNumber:k}=resolveTestNumber(o,f);return t.flatMap((r,c)=>{let p="",h="#111111";if(i==="value"&&m==="test"){const b=G(r,k,v);if(b===void 0)return[];p=b?"P":"F",h=Y(b?ht:pt)}else if(i==="value"){const b=R(r,k);if(b===void 0)return[];p=Ht([b],x),h=Y(s.forValue(a(b)))}else if(i==="hardBin"||i==="softBin"){const b=i==="softBin"?r.sbin:r.hbin;if(b===void 0)return[];p=String(b),h=Y(s.forBin(b))}else if(i==="metadata"){const b=it(r,$);if(b===void 0)return[];p=b,h=Y(P?.get(p)??w)}else{const b=R(r,0);if(b===void 0)return[];p=Ht([b],x),h=Y(s.forValue(a(b)))}const M=e?e[c*2]:r.physX,T=e?e[c*2+1]:r.physY;return[{x:M,y:T,text:p,fontSize:ce(r,p),color:h,align:"center"}]})}function Ot(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function fe(t,e,n=720){const{center:i,radius:s,notch:a}=t,o=k=>X(e,k.x,k.y);if(!(a!==void 0&&t.diameter>150)){const k=[];for(let r=0;r<=n;r++){const c=2*Math.PI*r/n;k.push(o(re(t,c)))}return[{kind:"wafer-boundary",..._(k,!0),lineColor:"#888888",lineWidth:1}]}const f=1.25,y=a.length,m=Ot(a.type),$=Math.atan2(y,s),P=s-f,v=[];let x=!1;for(let k=0;k<=n;k++){const r=2*Math.PI*k/n;let c=r-m;for(;c>Math.PI;)c-=2*Math.PI;for(;c<-Math.PI;)c+=2*Math.PI;if(Math.abs(c)<=$){if(!x){x=!0;const p={x:i.x+s*Math.cos(m-$),y:i.y+s*Math.sin(m-$)},h={x:i.x+P*Math.cos(m),y:i.y+P*Math.sin(m)},M={x:i.x+s*Math.cos(m+$),y:i.y+s*Math.sin(m+$)};v.push(o(p),o(h),o(M))}continue}v.push(o({x:i.x+s*Math.cos(r),y:i.y+s*Math.sin(r)}))}return[{kind:"wafer-boundary",..._(v,!0),lineColor:"#888888",lineWidth:1}]}function de(t,e,n,i=360){const s=[],a=Math.max(1,Math.floor(n));for(let o=1;o<a;o++){const l=t.radius*o/a,f=[];for(let y=0;y<=i;y++){const m=2*Math.PI*y/i,$={x:t.center.x+l*Math.cos(m),y:t.center.y+l*Math.sin(m)};f.push(X(e,$.x,$.y))}s.push({kind:"ring-boundary",..._(f,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function he(t,e,n,i){const{x:s,y:a}=t.center,o=t.radius,l=(r,c)=>X(e,r,c),f=n-s,y=Math.sqrt(Math.max(0,o*o-f*f)),m=l(n,a-y),$=l(n,a+y),P=i-a,v=Math.sqrt(Math.max(0,o*o-P*P)),x=l(s-v,i),k=l(s+v,i);return[{kind:"quadrant-boundary",..._([m,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",..._([x,k]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function pe(t,e){return t.map(n=>{const i=n.width/2,s=n.height/2;return{kind:"reticle",points:[[[n.x-i,n.y-s],[n.x+i,n.y-s],[n.x+i,n.y+s],[n.x-i,n.y+s]].map(([o,l])=>X(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function me(t){const e=t.filter(n=>n.probeIndex!==void 0).sort((n,i)=>(n.probeIndex??0)-(i.probeIndex??0));return e.length?[{kind:"probe-path",..._(e.map(n=>({x:n.physX,y:n.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function ye(t,e,n,i,s,a,o,l,f,y,m,$,P,v="off",x,k,r){const c=e.width-o,p=e.height-o,h=a?p:c,M=a?c:p,T=B=>s==="softBin"?B.sbin:B.hbin,b=B=>it(B,x);if(e.partial){t.push({x:n,y:i,width:h,height:M,fill:ie,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:n,y:i,width:h,height:M,fill:se,type:"stacked",metadata:e.metadata});return}if(f!==void 0&&(s==="hardBin"||s==="softBin")&&T(e)!==f){t.push({x:n,y:i,width:h,height:M,fill:Nt,type:"hardBin",metadata:e.metadata});return}if(r!==void 0&&s==="metadata"&&b(e)!==r){t.push({x:n,y:i,width:h,height:M,fill:Nt,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(v==="test"){const U=G(e,m,P),st=U===void 0?w:U?ht:pt;t.push({x:n,y:i,width:h,height:M,fill:st,type:"value",metadata:e.metadata});return}const B=R(e,m),F=classifySpec(B,P);let W,Q;B===void 0?W=w:v==="spec"?W=F==="failLow"?Zt:F==="failHigh"?pt:ht:(W=l.forValue(y(B)),(F==="failLow"||F==="failHigh")&&(Q=F)),t.push({x:n,y:i,width:h,height:M,fill:W,type:"value",specMark:Q,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const B=T(e),F=B!=null?l.forBin(B):w;t.push({x:n,y:i,width:h,height:M,fill:F,type:s,metadata:e.metadata});return}if(s==="metadata"){const B=b(e),F=B!==void 0?k?.get(B)??w:w;t.push({x:n,y:i,width:h,height:M,fill:F,type:"metadata",metadata:e.metadata});return}const O=R(e,0),A=O!==void 0?l.forValue(y(O)):w;t.push({x:n,y:i,width:h,height:M,fill:A,type:"value",metadata:e.metadata})}function ge(t,e,n){const i=t.radius*.15,s=dt(e,i,0),a=dt(e,0,i),o=s.x+a.x>=0?-1:1,l=s.y+a.y>=0?-1:1,f={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},y={x:f.x+s.x,y:f.y+s.y},m={x:f.x+a.x,y:f.y+a.y};return n.push({x:y.x+s.x*.35,y:y.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:m.x+a.x*.35,y:m.y+a.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",..._([f,y]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",..._([f,m]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,n={},i){const s=n.reticles??[],{plotMode:a="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:f=!1,ringCount:y=4,showRingBoundaries:m=!1,showQuadrantBoundaries:$=!1,showXYIndicator:P=!1,dieGap:v=1,colorScheme:x="default",highlightBin:k,highlightMetadataValue:r,interactiveTransform:c,valueRange:p,testDefs:h,activeTest:M=0,activeMetadataKey:T,fallbackFormat:b="engineering",aggregationMethod:O,lotSize:A,dataAxisFlip:B,isLotStack:F=!1,logScale:W,colorbarRangeMode:Q="spec",passFailDisplay:U,showPartialDies:st=!0}=n,xt=U??"off",bt=i?.hbinDefs,Mt=i?.sbinDefs,St=i?.metadataFields,At=St?.find(u=>u.key===T),Et={x:(B?.x??!1)!==(c?.flipX??!1),y:(B?.y??!1)!==(c?.flipY??!1)},_t=bt?new Map(bt.map(u=>[u.bin,u])):null,wt=Mt?new Map(Mt.map(u=>[u.bin,u])):null,ot=a==="softBin"?wt:_t,rt=te(x),$t={forValue:le(rt.forValue),forBin:x==="custom"&&ot?u=>ot.get(u)?.color??rt.forBin(u):rt.forBin};let{testNumber:H}=resolveTestNumber(M,h);if(!h?.length&&a==="value"&&!e.some(d=>d.testValues&&H in d.testValues)){const d=e.reduce((g,L)=>{if(!L.testValues)return g;const S=Object.keys(L.testValues).map(Number),K=S.length?Math.min(...S):void 0;return K!==void 0&&(g===void 0||K<g)?K:g},void 0);d!==void 0&&(H=d)}let J;if(Array.isArray(p))J=p;else if(p){const{testNumber:u}=resolveTestNumber(p.test,h);J=u===H?p.range:void 0}const I=findTestDef(h,H),kt=a==="value"&&I!==void 0&&!mt(I),Wt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let C="off";a==="value"&&(kt?C="test":xt==="spec"&&Wt?C="spec":xt==="test"&&e.some(u=>G(u,H,I)!==void 0)&&(C="test"));const Bt=C==="spec"?"spec":Q;let V,N,vt=!1;if(J)[V,N]=J;else{const u=a==="stackedValues"||a==="stackedBins"||a==="stackedSoftBins",d=!u&&a==="value"&&Bt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let g=1/0,L=-1/0,S=!0;for(const K of e){const q=u?R(K,0):R(K,H);q!==void 0&&(q<g&&(g=q),q>L&&(L=q),Number.isInteger(q)||(S=!1))}isFinite(g)||(S=!1),vt=S,d?(V=I.limitLow!==void 0?I.limitLow:isFinite(g)?g:0,N=I.limitHigh!==void 0?I.limitHigh:isFinite(L)?L:1):(V=isFinite(g)?g:0,N=isFinite(L)?L:1)}V===N&&(N=V+1);const Tt=kt?!1:W??I?.logScale??!1,It=Tt&&V>0&&N>0,jt=It;let Z;if(It){const u=Math.log10(V),d=Math.log10(N)-u;Z=g=>g<=0?0:Ct((Math.log10(g)-u)/d)}else Z=u=>Ct((u-V)/(N-V));const D=oe(t,B,c);let Lt=null;if(t.notch){const u=Ot(t.notch.type),d=dt(D.physicalToScreen,Math.cos(u)*t.radius,Math.sin(u)*t.radius),g=Math.hypot(d.x,d.y);g>0&&(Lt={x:d.x/g,y:d.y/g})}let j=1/0;for(let u=0;u<e.length;u++){const d=e[u];d.width<j&&(j=d.width),d.height<j&&(j=d.height)}const zt=j!==1/0?Math.min(v,j*.12):v,Pt=[],lt=[],Kt=D.bakedToScreen.a===1&&D.bakedToScreen.b===0&&D.bakedToScreen.c===0&&D.bakedToScreen.d===1&&D.bakedToScreen.e===0&&D.bakedToScreen.f===0;let E=null;if(!Kt){E=new Float64Array(e.length*2);for(let u=0;u<e.length;u++){const d=X(D.bakedToScreen,e[u].physX,e[u].physY);E[u*2]=d.x,E[u*2+1]=d.y}}const qt=Jt(D.gridToScreen),Xt=a==="hardBin"||a==="softBin"||a==="stackedBins"||a==="stackedSoftBins",Yt=a==="softBin"||a==="stackedSoftBins";let tt=Xt?new Map:void 0;const ct=a==="value"&&C==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ut=a==="value"&&C==="test"?{pass:0,fail:0}:void 0,Dt=a==="metadata";let et=Dt?new Map:void 0,nt=null;if(Dt&&T){const u=new Set;for(const g of e){if(g.partial||g.edgeExcluded)continue;const L=it(g,T);L!==void 0&&u.add(L)}const d=[...u].sort(ne);nt=new Map(d.map((g,L)=>[g,At?.values?.find(S=>S.value===g)?.color??ee(L)]))}for(let u=0;u<e.length;u++){const d=e[u],g=E?E[u*2]:d.physX,L=E?E[u*2+1]:d.physY;if(lt.push({x:g,y:L}),tt&&!d.partial&&!d.edgeExcluded){const S=Yt?d.sbin:d.hbin;S!=null&&tt.set(S,(tt.get(S)??0)+1)}if(ct&&!d.partial&&!d.edgeExcluded){const S=classifySpec(R(d,H),I);S&&ct[S]++}if(ut&&!d.partial&&!d.edgeExcluded){const S=G(d,H,I);S!==void 0&&ut[S?"pass":"fail"]++}if(et&&!d.partial&&!d.edgeExcluded){const S=it(d,T);S!==void 0&&et.set(S,(et.get(S)??0)+1)}d.partial&&!st||ye(Pt,d,g,L,a,qt,zt,$t,k,Z,H,ot,I,C,T,nt,r)}const Ft=o?generateTextOverlay(e,E,{plotMode:a,colorFns:$t,normalize:Z,activeTest:M,valueRange:[V,N],testDefs:h,fallbackFormat:b,passFailDisplay:C,activeMetadataKey:T,metadataColorMap:nt}):[],z=fe(t,D.physicalToScreen);m&&z.push(...de(t,D.physicalToScreen,y)),$&&z.push(...he(t,D.physicalToScreen,t.center.x,t.center.y)),l&&z.push(...pe(s,D.gridToScreen)),f&&z.push(...me(e)),P&&z.push(...ge(t,D.gridToScreen,Ft));const Gt=lt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Pt,hoverPoints:lt,texts:Ft,overlays:z,plotMode:a,colorScheme:x,metadata:t.metadata??null,dies:e,valueRange:[V,N],allIntegerValues:vt,testDefs:h,activeTest:M,logScale:jt,logScaleRequested:Tt,aggrMethod:O,lotSize:A,axisFlip:Et,dataAxisFlip:{x:B?.x??!1,y:B?.y??!1},gridToScreen:D.gridToScreen,rotation:((t.orientation+(c?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:F,passFailDisplay:C,colorBySpec:C==="spec",colorbarRangeMode:Bt,waferCenter:t.center,waferRadius:t.radius,notchDir:Lt,binCounts:tt,metadataCounts:et,metadataColorMap:nt,activeMetadataKey:T,metadataFields:St,specCounts:ct,passFailCounts:ut,dieBounds:Gt}}export{getDieKey}from"../core/dies.js";
@@ -9,10 +9,19 @@ import { type View, type ViewOptions, type PlotMode } from './buildView.js';
9
9
  * such as −7, 0, 5. They are NOT millimetre values.
10
10
  */
11
11
  export interface DieResult {
12
- /** Die grid X position (prober step coordinate). */
13
- x: number;
14
- /** Die grid Y position (prober step coordinate). */
15
- y: number;
12
+ /**
13
+ * Die grid X position (prober step coordinate). Omit (together with `y`)
14
+ * when this die has no reported spatial position at all — it still counts
15
+ * toward every non-spatial stat (yield, bin counts, per-test
16
+ * histograms/correlation/scatter) but is never placed on a wafer
17
+ * map/gallery canvas and never enters ring/quadrant/sector/reticle/
18
+ * cluster/pattern analysis. A die must be either fully positioned (both
19
+ * `x` and `y`) or fully unpositioned (neither) — `buildWaferMap` rejects
20
+ * the half-state.
21
+ */
22
+ x?: number;
23
+ /** Die grid Y position (prober step coordinate). See `x`. */
24
+ y?: number;
16
25
  /**
17
26
  * Test values keyed by test number — a stable per-test identity such as
18
27
  * STDF TEST_NUM or an equivalent application-defined integer. Keying by test
@@ -498,13 +507,21 @@ export interface WaferMapResult {
498
507
  warnings: WaferWarning[];
499
508
  /** Die population statistics. */
500
509
  dataCoverage: {
501
- /** Dies inside the wafer boundary that have at least one value or bin. */
510
+ /** Positioned dies inside the wafer boundary that have at least one value or bin. */
502
511
  filledDies: number;
503
- /** Total dies inside the wafer boundary (including partial). */
512
+ /** Total positioned dies inside the wafer boundary (including partial). Excludes unpositioned dies — see `unpositionedDies`. */
504
513
  totalDies: number;
505
- /** Dies falling within the edge exclusion zone. */
514
+ /** Positioned dies falling within the edge exclusion zone. */
506
515
  edgeExcludedDies: number;
507
- /** `filledDies / totalDies` in [0, 1]. */
516
+ /**
517
+ * Dies with no reported x/y position at all (see `Die.x`/`hasPosition`).
518
+ * Always present, `0` when every die is positioned. These dies are
519
+ * counted in `dies` and in every non-spatial stat (yield, bin counts,
520
+ * per-test analysis) but never in `totalDies`/`filledDies`/`ratio`
521
+ * above, and never drawn on the map/gallery canvas.
522
+ */
523
+ unpositionedDies: number;
524
+ /** `filledDies / totalDies` in [0, 1] — `0` when there are no positioned dies, even if `unpositionedDies` is large. */
508
525
  ratio: number;
509
526
  };
510
527
  /** Yield statistics computed against `passBins`. */
@@ -1 +1 @@
1
- import{createWafer as $}from"../core/wafer.js";import{isYieldEligibleDie as Q,getDieKey as I}from"../core/dies.js";import{applyOrientation as A,transformDies as Z}from"../core/transforms.js";import{affineRotation as ee,affineMirror as te,affineCompose as ne,affinePoint as ie}from"../core/transforms.js";import{inferWaferFromXY as se}from"../core/inference/wafer.js";import{resolveGridPitch as oe}from"../core/inference/pitch.js";import{assignGridIndices as re}from"../core/inference/grid.js";import{generateReticleGrid as ae}from"../core/reticle.js";import{buildView as T}from"./buildView.js";import{modeOf as fe}from"../core/utils.js";import{aggregateValues as L,aggregateBinCounts as de}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function ce(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],testDefs:t.testDefs,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function le(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function ue(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let o=1/0,r=-1/0,h=1/0,f=-1/0;for(const i of t)i.x<o&&(o=i.x),i.x>r&&(r=i.x),i.y<h&&(h=i.y),i.y>f&&(f=i.y);return{offsetX:Math.round((r+o)/2),offsetY:Math.round((f+h)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function he(t,n,e,o,r,h){if(h)return{colMidX:(Math.round(h.x)-n)*o,colMidY:(Math.round(h.y)-e)*r,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let f=1/0,i=-1/0,d=1/0,a=-1/0;for(const s of t){const c=Math.round(s.x)-n,p=Math.round(s.y)-e;c<f&&(f=c),c>i&&(i=c),p<d&&(d=p),p>a&&(a=p)}return{colMidX:(f+i)/2*o,colMidY:(d+a)/2*r,anchored:!1}}function pe(t,n,e){let o=1/0,r=-1/0,h=1/0,f=-1/0,i=0,d=0;for(const c of t){const p=Math.round(c.x)-n,u=Math.round(c.y)-e;i+=p,d+=u,p<o&&(o=p),p>r&&(r=p),u<h&&(h=u),u>f&&(f=u)}const a=t.length,s=(c,p,u)=>{const g=(u-p)/2;return g<=0?0:Math.abs(c/a-(p+u)/2)/g};return s(i,o,r)>.11||s(d,h,f)>.11}function me(t,n){let e=t?.xAxisDirection==="left",o=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(o=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:o}}function ye(t,n){const{results:e,method:o,targetBin:r}=t;if(o==="mean"||o==="median"||o==="stddev"||o==="min"||o==="max"||o==="count"){const h=new Set((n??[]).filter(s=>!isParametricTest(s)).map(s=>s.testNumber)),f=new Set;for(const s of e)for(const c of s)if(c.testValues)for(const p of Object.keys(c.testValues)){const u=Number(p);h.has(u)||f.add(u)}if(f.size===0)return L(e,o);const i=[...f],d=i.map(s=>L(e,o,s)),a=new Map;for(let s=0;s<i.length;s++){const c=i[s];for(const p of d[s]){const u=I(p);a.has(u)||a.set(u,{template:p,testValues:{}});const g=a.get(u),m=p.testValues?.[0];m!==void 0&&(g.testValues[c]=m)}}return[...a.values()].map(({template:s,testValues:c})=>({...s,testValues:Object.keys(c).length>0?c:void 0}))}if(o==="countBin"||o==="percent"){if(r===void 0)return[];const h=de(e,r,"hard");if(o==="countBin")return h;const f=e.length;return h.map(i=>({...i,testValues:{0:f>0?(i.testValues?.[0]??0)/f*100:0}}))}if(o==="mode"){const h=new Map;for(const i of e)for(const d of i){const a=I(d);h.has(a)||h.set(a,[]),h.get(a).push(d)}const f=[];for(const[i,d]of h){const a=i.split(","),s=Number(a[0]),c=Number(a[1]),p=d.map(g=>g.hbin).filter(g=>g!==void 0),u=fe(p);u!==null&&f.push({x:s,y:c,hbin:u})}return f}return[]}function W(t){const n=t.length,e=t.filter(r=>r.edgeExcluded).length,o=t.filter(r=>dieHasTestData(r)||r.hbin!==void 0||r.sbin!==void 0).length;return{filledDies:o,totalDies:n,edgeExcludedDies:e,ratio:n>0?o/n:0}}function N(t,n,e="exclude"){const o=new Set(n),h=t.filter(u=>!u.partial).filter(u=>u.edgeExcluded).length,f=t.filter(u=>u.partial).length;let i=0,d=0,a=!1;for(const u of t){if(!Q(u))continue;const g=u.hbin??u.sbin;g!==void 0&&(a=!0,o.has(g)?i++:d++)}const s=i+d,c=a&&s>0?i/s*100:null;let p=null;if(e==="denominator-only"){const u=s+h;p=a&&u>0?i/u*100:null}return{passDies:i,failDies:d,edgeExcludedDies:h,partialDies:f,totalDies:s,yieldPercent:c,yieldPercentGross:p}}function _(t,n,e,o,r,h=0,f=0,i=0,d=0,a=0,s=!1,c=!1){if(!t)return[];const p=t.anchorDie??{x:0,y:0},u=ae(n,{width:t.width,height:t.height,diePitchX:o,diePitchY:r,anchorDie:{x:p.x-h,y:p.y-f},gridOrigin:{x:-i,y:-d}}),g=ne(te(s,c,n.center.x,n.center.y),ee(a,n.center.x,n.center.y));return u.filter(m=>{const x=m.width/2,O=m.height/2,S=[[m.x-x,m.y-O],[m.x+x,m.y-O],[m.x+x,m.y+O],[m.x-x,m.y+O]].map(([y,C])=>ie(g,y,C)),R=Math.min(...S.map(y=>y.x)),Y=Math.max(...S.map(y=>y.x)),E=Math.min(...S.map(y=>y.y)),D=Math.max(...S.map(y=>y.y));return e.some(y=>y.physX>=R&&y.physX<Y&&y.physY>=E&&y.physY<D)})}function ge(t,n,e){const o=(n.radius-e)**2;return t.map(r=>{const h=r.physX-n.center.x,f=r.physY-n.center.y;return h*h+f*f>o?{...r,edgeExcluded:!0}:r})}function xe(t,n,e){const o=new Map;for(const i of t){let d=o.get(i.x);d||(d=new Map,o.set(i.x,d)),d.set(i.y,(d.get(i.y)??0)+1)}const r=new Set(e);function h(i,d){const a=i.hbin,s=d.hbin;if(a===void 0||s===void 0)return!1;const c=r.has(a),p=r.has(s);return n==="best"?p!==c?p:s<a:p!==c?c:s>a}const f=new Map;for(const i of t){const d=I(i),a=f.get(d);n==="first"&&a||(n==="best"||n==="worst")&&a&&!h(a,i)||f.set(d,i)}return Array.from(f.values()).map(i=>{const a=o.get(i.x)?.get(i.y)??1;return a>1?{...i,retestCount:a}:i})}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const o=t.testPass?.[n];if(o!==void 0)return o;if(e!==void 0&&!isParametricTest(e)){const r=t.testValues?.[n];if(r===0||r===1)return r===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function G(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function K(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function z(t){return(t.warnings??[]).map(n=>({code:we(n),message:n,severity:"error",confidence:t.wafer.confidence}))}function we(t){return t.includes(j)?"geometry-conflict":t.includes(H)?"inferred-pitch":"partial-coverage"}const j="do not fit inside the supplied",H="The die pitch was inferred as";export function buildWaferMap(t,n){const e=ce(t),{debug:o,...r}=n??{},h=e.lotStackOpts?ye(e.lotStackOpts,e.testDefs):e.results;for(let l=0;l<Math.min(5,h.length);l++){const M=h[l];if(typeof M.x=="string"||typeof M.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}const f=xe(h,e.retestPolicy,e.passBins),i={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let l=e.explicitDies;if(f.length>0){const J=new Map(f.map(P=>[I(P),P]));l=l.map(P=>{const X=J.get(I(P));return X?G(P,X):P})}const M=e.waferOpts?.diameter??300,b=$({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});b.orientation!==0&&(l=A(l,b));const v=_(e.reticleOpts,b,l,1,1,0,0,0,0,b.orientation),q=r.showReticle??e.reticleOpts!==void 0,V=T(b,l,{...r,reticles:v,showReticle:q,plotMode:K(f,r),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields});return{wafer:b,dies:l,view:V,reticleConfig:e.reticleOpts,units:"mm",inference:i,warnings:z(i),plotMode:V.plotMode,metadata:V.metadata,isLotStack:!1,dataCoverage:W(l),yield:N(l,e.passBins,e.edgeDieYieldMode),reticles:v,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const d=f.map(l=>({x:l.x,y:l.y})),a=oe(d,e.dieOpts,e.waferOpts?.diameter);i.diePitch={confidence:a.confidence,units:a.units};const{pitchX:s,pitchY:c}=a,p=a.units,u=le(f,e.dieOpts),g=re(d);i.grid={confidence:g.confidence};const{offsetX:m,offsetY:x}=ue(d,u,g),{flipX:O,flipY:S}=me(e.dieOpts,u),{colMidX:R,colMidY:Y,anchored:E}=he(d,m,x,s,c,e.waferOpts?.center);let D=e.waferOpts?.diameter;const y=d.map(l=>({x:(Math.round(l.x)-m)*s-R,y:(Math.round(l.y)-x)*c-Y})),C=y.length>0?Math.max(...y.map(({x:l,y:M})=>Math.hypot(Math.abs(l)+s/2,Math.abs(M)+c/2))):0;if(D===void 0)if(d.length>0)if(p==="mm"){const l=se(y,{minRadius:C});D=l.diameter,i.wafer={confidence:l.confidence,method:l.method}}else D=C*2,i.wafer={confidence:a.confidence*.8,method:"extent"};else D=p==="mm"?300:30,i.wafer={confidence:0,method:"default"};if(!E&&d.length>0&&pe(d,m,x)&&(i.wafer.method="inferred-partial",(i.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0){const l=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(l&&C>D/2+1e-9){const M=y.filter(({x:b,y:v})=>Math.hypot(Math.abs(b)+s/2,Math.abs(v)+c/2)>D/2+1e-9).length;(i.warnings??=[]).push(`${M} of ${y.length} probed die positions ${j} ${D} mm wafer at the supplied die pitch of ${s} \xD7 ${c} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(C*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}else l||(i.warnings??=[]).push(`${H} ${s.toFixed(3)} \xD7 ${c.toFixed(3)} mm by assuming the die grid fits within the ${D} mm wafer. The assumed aspect ratio may not match the true die shape whenever edge dies are absent from the data (a reticle-complete map, or partial dies filtered out upstream). Supply dieConfig.width and dieConfig.height \u2014 the die pitch, not the diameter, is what fixes placement.`)}const k=$({diameter:D,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata}),Me={width:s,height:c};let w=f.map(l=>{const M=Math.round(l.x)-m,b=Math.round(l.y)-x,v={id:`${M}_${b}`,x:M,y:b,physX:M*s-R,physY:b*c-Y,width:s,height:c,insideWafer:!0,partial:!1};return G(v,l)});(m!==0||x!==0)&&(w=w.map(l=>({...l,x:l.x+m,y:l.y+x,id:`${l.x+m}_${l.y+x}`}))),w=A(w,k),(O||S)&&(w=Z(w,{flipX:O,flipY:S},k.center)),e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(w=ge(w,k,e.waferOpts.edgeExclusion));const B=_(e.reticleOpts,k,w,s,c,m,x,R,Y,k.orientation,O,S),U=r.showReticle??e.reticleOpts!==void 0,F=T(k,w,{...r,reticles:B,showReticle:U,plotMode:K(f,r),testDefs:e.testDefs,dataAxisFlip:{x:O,y:S},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields});return{wafer:k,dies:w,view:F,reticleConfig:e.reticleOpts,units:p,inference:i,warnings:z(i),plotMode:F.plotMode,metadata:F.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:W(w),yield:N(w,e.passBins,e.edgeDieYieldMode),reticles:B,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:F.aggrMethod,lotSize:F.lotSize}}
1
+ import{createWafer as z}from"../core/wafer.js";import{isYieldEligibleDie as ae,getDieKey as B,hasPosition as S}from"../core/dies.js";import{applyOrientation as j,transformDies as fe}from"../core/transforms.js";import{affineRotation as de,affineMirror as ce,affineCompose as le,affinePoint as ue}from"../core/transforms.js";import{inferWaferFromXY as he}from"../core/inference/wafer.js";import{resolveGridPitch as pe}from"../core/inference/pitch.js";import{assignGridIndices as me}from"../core/inference/grid.js";import{generateReticleGrid as ye}from"../core/reticle.js";import{buildView as H}from"./buildView.js";import{modeOf as ge}from"../core/utils.js";import{aggregateValues as U,aggregateBinCounts as xe}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function we(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],testDefs:t.testDefs,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function be(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Me(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let u=1/0,d=-1/0,f=1/0,l=-1/0;for(const o of t)o.x<u&&(u=o.x),o.x>d&&(d=o.x),o.y<f&&(f=o.y),o.y>l&&(l=o.y);return{offsetX:Math.round((d+u)/2),offsetY:Math.round((l+f)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function De(t,n,e,u,d,f){if(f)return{colMidX:(Math.round(f.x)-n)*u,colMidY:(Math.round(f.y)-e)*d,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let l=1/0,o=-1/0,h=1/0,p=-1/0;for(const i of t){const c=Math.round(i.x)-n,s=Math.round(i.y)-e;c<l&&(l=c),c>o&&(o=c),s<h&&(h=s),s>p&&(p=s)}return{colMidX:(l+o)/2*u,colMidY:(h+p)/2*d,anchored:!1}}function Oe(t,n,e){let u=1/0,d=-1/0,f=1/0,l=-1/0,o=0,h=0;for(const c of t){const s=Math.round(c.x)-n,r=Math.round(c.y)-e;o+=s,h+=r,s<u&&(u=s),s>d&&(d=s),r<f&&(f=r),r>l&&(l=r)}const p=t.length,i=(c,s,r)=>{const m=(r-s)/2;return m<=0?0:Math.abs(c/p-(s+r)/2)/m};return i(o,u,d)>.11||i(h,f,l)>.11}function Se(t,n){let e=t?.xAxisDirection==="left",u=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(u=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:u}}function ke(t,n){const{method:e,targetBin:u}=t,d=t.results.map(f=>f.filter(S));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const f=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),l=new Set;for(const i of d)for(const c of i)if(c.testValues)for(const s of Object.keys(c.testValues)){const r=Number(s);f.has(r)||l.add(r)}if(l.size===0)return U(d,e);const o=[...l],h=o.map(i=>U(d,e,i)),p=new Map;for(let i=0;i<o.length;i++){const c=o[i];for(const s of h[i]){const r=B(s);p.has(r)||p.set(r,{template:s,testValues:{}});const m=p.get(r),y=s.testValues?.[0];y!==void 0&&(m.testValues[c]=y)}}return[...p.values()].map(({template:i,testValues:c})=>({...i,testValues:Object.keys(c).length>0?c:void 0}))}if(e==="countBin"||e==="percent"){if(u===void 0)return[];const f=xe(d,u,"hard");if(e==="countBin")return f;const l=d.length;return f.map(o=>({...o,testValues:{0:l>0?(o.testValues?.[0]??0)/l*100:0}}))}if(e==="mode"){const f=new Map;for(const o of d)for(const h of o){const p=B(h);f.has(p)||f.set(p,[]),f.get(p).push(h)}const l=[];for(const[o,h]of f){const p=o.split(","),i=Number(p[0]),c=Number(p[1]),s=h.map(m=>m.hbin).filter(m=>m!==void 0),r=ge(s);r!==null&&l.push({x:i,y:c,hbin:r})}return l}return[]}function q(t){const n=t.filter(S),e=n.length,u=n.filter(f=>f.edgeExcluded).length,d=n.filter(f=>dieHasTestData(f)||f.hbin!==void 0||f.sbin!==void 0).length;return{filledDies:d,totalDies:e,edgeExcludedDies:u,unpositionedDies:t.length-n.length,ratio:e>0?d/e:0}}function J(t,n,e="exclude"){const u=new Set(n),f=t.filter(r=>!r.partial).filter(r=>r.edgeExcluded).length,l=t.filter(r=>r.partial).length;let o=0,h=0,p=!1;for(const r of t){if(!ae(r))continue;const m=r.hbin??r.sbin;m!==void 0&&(p=!0,u.has(m)?o++:h++)}const i=o+h,c=p&&i>0?o/i*100:null;let s=null;if(e==="denominator-only"){const r=i+f;s=p&&r>0?o/r*100:null}return{passDies:o,failDies:h,edgeExcludedDies:f,partialDies:l,totalDies:i,yieldPercent:c,yieldPercentGross:s}}function Q(t,n,e,u,d,f=0,l=0,o=0,h=0,p=0,i=!1,c=!1){if(!t)return[];const s=t.anchorDie??{x:0,y:0},r=ye(n,{width:t.width,height:t.height,diePitchX:u,diePitchY:d,anchorDie:{x:s.x-f,y:s.y-l},gridOrigin:{x:-o,y:-h}}),m=le(ce(i,c,n.center.x,n.center.y),de(p,n.center.x,n.center.y));return r.filter(y=>{const k=y.width/2,w=y.height/2,b=[[y.x-k,y.y-w],[y.x+k,y.y-w],[y.x+k,y.y+w],[y.x-k,y.y+w]].map(([g,M])=>ue(m,g,M)),R=Math.min(...b.map(g=>g.x)),P=Math.max(...b.map(g=>g.x)),F=Math.min(...b.map(g=>g.y)),E=Math.max(...b.map(g=>g.y));return e.some(g=>g.physX>=R&&g.physX<P&&g.physY>=F&&g.physY<E)})}function ve(t,n,e){const u=(n.radius-e)**2;return t.map(d=>{const f=d.physX-n.center.x,l=d.physY-n.center.y;return f*f+l*l>u?{...d,edgeExcluded:!0}:d})}function Ce(t,n,e){const u=t.filter(S),d=t.filter(i=>!S(i)),f=new Map;for(const i of u){let c=f.get(i.x);c||(c=new Map,f.set(i.x,c)),c.set(i.y,(c.get(i.y)??0)+1)}const l=new Set(e);function o(i,c){const s=i.hbin,r=c.hbin;if(s===void 0||r===void 0)return!1;const m=l.has(s),y=l.has(r);return n==="best"?y!==m?y:r<s:y!==m?m:r>s}const h=new Map;for(const i of u){const c=B(i),s=h.get(c);n==="first"&&s||(n==="best"||n==="worst")&&s&&!o(s,i)||h.set(c,i)}return[...Array.from(h.values()).map(i=>{const s=f.get(i.x)?.get(i.y)??1;return s>1?{...i,retestCount:s}:i}),...d]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const u=t.testPass?.[n];if(u!==void 0)return u;if(e!==void 0&&!isParametricTest(e)){const d=t.testValues?.[n];if(d===0||d===1)return d===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function T(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function Z(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function ee(t){return(t.warnings??[]).map(n=>({code:Re(n),message:n,severity:"error",confidence:t.wafer.confidence}))}function Re(t){return t.includes(te)?"geometry-conflict":t.includes(ne)?"inferred-pitch":"partial-coverage"}const te="do not fit inside the supplied",ne="The die pitch was inferred as";export function buildWaferMap(t,n){const e=we(t),{debug:u,...d}=n??{},f=e.lotStackOpts?ke(e.lotStackOpts,e.testDefs):e.results;for(let a=0;a<Math.min(5,f.length);a++){const x=f[a];if(typeof x.x=="string"||typeof x.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const a of f)if(a.x==null!=(a.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(a.x)}, y=${String(a.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const l=Ce(f,e.retestPolicy,e.passBins),o={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let a=e.explicitDies;const x=l.filter(S),C=l.filter(A=>!S(A));if(x.length>0){const A=new Map(x.map(O=>[B(O),O]));a=a.map(O=>{const K=A.get(B(O));return K?T(O,K):O})}const V=e.waferOpts?.diameter??300,Y=z({diameter:V,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});Y.orientation!==0&&(a=j(a,Y));const G=Q(e.reticleOpts,Y,a,1,1,0,0,0,0,Y.orientation),oe=d.showReticle??e.reticleOpts!==void 0,L=H(Y,a,{...d,reticles:G,showReticle:oe,plotMode:Z(l,d),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),re=C.map((A,O)=>T({id:`unpositioned_${O}`,width:a[0]?.width??1,height:a[0]?.height??1},A)),_=[...a,...re];return{wafer:Y,dies:_,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:o,warnings:ee(o),plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:q(_),yield:J(_,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const h=l.filter(S),p=l.filter(a=>!S(a)),i=h.map(a=>({x:a.x,y:a.y})),c=pe(i,e.dieOpts,e.waferOpts?.diameter);o.diePitch={confidence:c.confidence,units:c.units};const{pitchX:s,pitchY:r}=c,m=c.units,y=be(h,e.dieOpts),k=me(i);o.grid={confidence:k.confidence};const{offsetX:w,offsetY:b}=Me(i,y,k),{flipX:R,flipY:P}=Se(e.dieOpts,y),{colMidX:F,colMidY:E,anchored:g}=De(i,w,b,s,r,e.waferOpts?.center);let M=e.waferOpts?.diameter;const I=i.map(a=>({x:(Math.round(a.x)-w)*s-F,y:(Math.round(a.y)-b)*r-E})),X=I.length>0?Math.max(...I.map(({x:a,y:x})=>Math.hypot(Math.abs(a)+s/2,Math.abs(x)+r/2))):0;if(M===void 0)if(i.length>0)if(m==="mm"){const a=he(I,{minRadius:X});M=a.diameter,o.wafer={confidence:a.confidence,method:a.method}}else M=X*2,o.wafer={confidence:c.confidence*.8,method:"extent"};else M=m==="mm"?300:30,o.wafer={confidence:0,method:"default"};if(!g&&i.length>0&&Oe(i,w,b)&&(o.wafer.method="inferred-partial",(o.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0){const a=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(a&&X>M/2+1e-9){const x=I.filter(({x:C,y:V})=>Math.hypot(Math.abs(C)+s/2,Math.abs(V)+r/2)>M/2+1e-9).length;(o.warnings??=[]).push(`${x} of ${I.length} probed die positions ${te} ${M} mm wafer at the supplied die pitch of ${s} \xD7 ${r} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(X*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}else a||(o.warnings??=[]).push(`${ne} ${s.toFixed(3)} \xD7 ${r.toFixed(3)} mm by assuming the die grid fits within the ${M} mm wafer. The assumed aspect ratio may not match the true die shape whenever edge dies are absent from the data (a reticle-complete map, or partial dies filtered out upstream). Supply dieConfig.width and dieConfig.height \u2014 the die pitch, not the diameter, is what fixes placement.`)}const v=z({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata}),Pe={width:s,height:r};let D=h.map(a=>{const x=Math.round(a.x)-w,C=Math.round(a.y)-b,V={id:`${x}_${C}`,x,y:C,physX:x*s-F,physY:C*r-E,width:s,height:r,insideWafer:!0,partial:!1};return T(V,a)});(w!==0||b!==0)&&(D=D.map(a=>({...a,x:a.x+w,y:a.y+b,id:`${a.x+w}_${a.y+b}`}))),D=j(D,v),(R||P)&&(D=fe(D,{flipX:R,flipY:P},v.center)),e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(D=ve(D,v,e.waferOpts.edgeExclusion));const N=Q(e.reticleOpts,v,D,s,r,w,b,F,E,v.orientation,R,P),ie=d.showReticle??e.reticleOpts!==void 0,$=H(v,D,{...d,reticles:N,showReticle:ie,plotMode:Z(l,d),testDefs:e.testDefs,dataAxisFlip:{x:R,y:P},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),se=p.map((a,x)=>T({id:`unpositioned_${x}`,width:s,height:r},a)),W=[...D,...se];return{wafer:v,dies:W,view:$,reticleConfig:e.reticleOpts,units:m,inference:o,warnings:ee(o),plotMode:$.plotMode,metadata:$.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:q(W),yield:J(W,e.passBins,e.edgeDieYieldMode),reticles:N,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:$.aggrMethod,lotSize:$.lotSize}}
@@ -1 +1 @@
1
- import{isYieldEligibleDie as X,getDieKey as E}from"../core/dies.js";import{buildWaferMap as ve,getTestPassStatus as ce,isParametricTest as q}from"../renderer/buildWaferMap.js";import{buildQuadrantRegions as $e,buildReticlePositionRegions as ke,buildRingRegions as De,buildSectorRegions as ze,buildTestSiteRegions as Me,sectorCompassNames as ue,areQuadrantsAdjacent as Re,parseRegionKey as j}from"./regions.js";import{buildClusterFindings as Be}from"./clusterDetection.js";import{classifyPattern as Fe}from"./patternClassification.js";import{normalCdf as de}from"./math.js";import{quantile as Z}from"./math.js";import{mean as G,clamp01 as ge}from"../core/utils.js";const Pe={ringCount:4,passBins:[1],significanceLevel:.05,minimumEffectSize:.2,minimumRelativeEffect:1,minimumSampleSize:5,includePartial:!1,includeEdgeExcluded:!1,enableYieldAnalysis:!0,enableHardBinAnalysis:!0,enableSoftBinAnalysis:!0,enableTestValueAnalysis:!1,computePerTestStats:!1,enableReticlePositionAnalysis:!0,enableClusterAnalysis:!0,enableAngularAnalysis:!0,sectorCount:8,enablePatternClassification:!0,minimumClusterSize:5};function Ve(s,a){const o={...s};return o.minimumClusterSize=Math.max(5,Math.round(a*.003)),o}function Le(s){return"wafer"in s&&"dies"in s&&"view"in s?s:ve(s)}function Ne(s,a){return X(s,a)?s.hbin!==void 0||s.sbin!==void 0||s.testValues!==void 0&&Object.keys(s.testValues).length>0:!1}function Ce(s,a,o){if(!s||a)return;const e=(o??[]).filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(e.length!==0)return n=>{for(const f of e){const r=f.testNumber,i=n.testValues?.[r];if(i!==void 0&&(f.limitLow!==void 0&&i<f.limitLow||f.limitHigh!==void 0&&i>f.limitHigh))return!0}return!1}}function Ke(s,a,o){const e=new Set,n=new Set,f=new Set,r=new Map,i=new Map;for(const t of s){if(t.testValues)for(const l of Object.keys(t.testValues))e.add(Number(l));if(t.testPass)for(const l of Object.keys(t.testPass))e.add(Number(l));t.hbin!==void 0&&n.add(t.hbin),t.sbin!==void 0&&f.add(t.sbin),X(t)&&(t.hbin!==void 0&&r.set(t.hbin,(r.get(t.hbin)??0)+1),t.sbin!==void 0&&i.set(t.sbin,(i.get(t.sbin)??0)+1))}return{totalDies:s.length,analyzedDies:a,excludedDies:s.length-a,yieldPercent:o,testsConsidered:[...e].sort((t,l)=>t-l),hardBinsConsidered:[...n].sort((t,l)=>t-l),softBinsConsidered:[...f].sort((t,l)=>t-l),...r.size?{hardBinCounts:Object.fromEntries(r)}:{},...i.size?{softBinCounts:Object.fromEntries(i)}:{}}}function Ae(s,a){if(!a?.length)return;const o=a.filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(!o.length)return;const e=[];for(const n of o){const f=n.testNumber;let r=0,i=0,t=0,l=0;for(const u of s){if(u.partial||u.edgeExcluded)continue;const b=u.testValues?.[f];b!==void 0&&(l++,n.limitLow!==void 0&&b<n.limitLow?i++:n.limitHigh!==void 0&&b>n.limitHigh?t++:r++)}e.push({testNumber:f,label:n.name,passDies:r,failLowDies:i,failHighDies:t,totalDies:l,yieldPercent:l>0?r/l*100:null})}return e.length?e:void 0}export function computeFunctionalYield(s,a){if(!a?.length)return;const o=a.filter(n=>!q(n));if(!o.length)return;const e=[];for(const n of o){const f=n.testNumber;let r=0,i=0;for(const l of s){if(l.partial||l.edgeExcluded)continue;const u=ce(l,f,n);u!==void 0&&(u?r++:i++)}const t=r+i;e.push({testNumber:f,label:n.name,passDies:r,failDies:i,totalDies:t,passRatePercent:t>0?r/t*100:null})}return e.length?e:void 0}function Ee(s,a,o,e){const n=[];for(const f of a){const r=[];for(const b of s){if(b.partial||b.edgeExcluded)continue;const g=b.testValues?.[f];g!==void 0&&r.push(g)}if(r.length<e)continue;const i=G(r),t=Math.sqrt(ae(r,i)),l=r.slice().sort((b,g)=>b-g),u=o?.find(b=>b.testNumber===f)?.name??String(f);n.push({testNumber:f,label:u,count:r.length,min:l[0],max:l[l.length-1],mean:i,stddev:t,median:Z(l,.5),q1:Z(l,.25),q3:Z(l,.75)})}return n.length?n:void 0}function H(s,a,o,e){const n=(s+o)/(a+e),f=n*(1-n)*(1/a+1/e);if(!Number.isFinite(f)||f<=0)return 1;const r=(s/a-o/e)/Math.sqrt(f);return ge(2*(1-de(Math.abs(r))))}function ee(s){const a=new Map;for(const o of s){const e=`${o.variable.kind}:${o.comparison.family}`,n=a.get(e)??[];n.push(o),a.set(e,n)}for(const o of a.values()){const e=[...o].filter(r=>r.stats.pValue!==void 0).sort((r,i)=>(r.stats.pValue??1)-(i.stats.pValue??1)),n=e.length;let f=1;for(let r=n-1;r>=0;r--){const i=e[r],t=i.stats.pValue??1,l=Math.min(1,Math.min(f,t*n/(r+1)));i.stats.adjustedPValue=l,f=l}}return s}function te(s,a,o){const e=Math.abs(a),n=o!==void 0?Math.abs(o):0;return s<=.01&&(e>=.3||n>=2.5)?"unusual":s<=.05&&(e>=.2||n>=1.5)?"notable":"info"}function ne(s,a){return s<=.01&&Math.abs(a)>=.5?"unusual":s<=.05&&Math.abs(a)>=.15?"notable":"info"}function J(s){return s==="reticle-position"?"other reticle positions":s==="test-site"?"other test sites":"the rest of the map"}function O(s){return s==="reticle-position"?"Other reticle positions":s==="test-site"?"Other test sites":"Rest of map"}function he(s,a,o){const e=(Math.abs(a)*100).toFixed(1);return`${s} yield is ${e} percentage points ${a>0?"higher":"lower"} than ${J(o)}`}function ie(s,a){return a==="quadrant"&&!s.startsWith("Quadrant")?`quadrant ${s}`:s}function me(s,a,o,e){const n=ie(s,e),f=(Math.abs(o)*100).toFixed(1);return`${n} has ${a} occurrence ${f} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function xe(s,a,o,e){const n=ie(s,e),f=(Math.abs(o)*100).toFixed(1);return`${n} has ${a} pass rate ${f} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function be(s,a,o,e,n,f){const r=ie(s,n),i=J(n),t=o>0?"higher":"lower";if(e!==void 0&&Number.isFinite(e)){const u=(Math.abs(e)*100).toFixed(1);return`${r} mean ${a} is ${u}% ${t} than ${i}`}const l=f?` ${f}`:"";return`${r} mean ${a} is ${Math.abs(o).toPrecision(3)}${l} ${t} than ${i}`}function pe(s,a,o){const e=a?.find(n=>n.bin===s);return e?.name?`${o} ${s} (${e.name})`:`${o} ${s}`}function Te(s,a){const o=a?.find(e=>e.testNumber===s);return{label:o?.name??`Test ${s}`,unit:o?.unit}}function se(s,a){const o=new Map(s.map(n=>[E(n),n])),e=new Map;for(const n of a){const f=[];for(const r of n.dieKeys){const i=o.get(r);i&&f.push(i)}e.set(n.key,f)}return e}function oe(s,a){return ee(s),s.filter(o=>{const e=o.stats.adjustedPValue??o.stats.pValue??1,n=Math.abs(o.effect.absoluteDelta??0),f=Math.abs(o.effect.relativeDelta??0);return e<=a.significanceLevel&&(n>=a.minimumEffectSize||f>=a.minimumRelativeEffect)}).map(o=>({...o,severity:te(o.stats.adjustedPValue??o.stats.pValue??1,o.effect.absoluteDelta??0,o.effect.relativeDelta)}))}function Y(s,a,o,e){const n=new Set(o),f=se(s,a),r=new Map,i=new Map;for(const[l,u]of f){let b=0,g=0;for(const d of u)d.hbin!==void 0&&(g++,n.has(d.hbin)&&b++);r.set(l,b),i.set(l,g)}const t=[];for(const l of a){const u=i.get(l.key)??0,b=r.get(l.key)??0;let g=0,d=0;for(const[$,p]of i)$!==l.key&&(g+=p,d+=r.get($)??0);if(u<e.minimumSampleSize||g<e.minimumSampleSize)continue;const h=b/u,m=d/g,S=h-m,D=H(b,u,d,g);t.push({id:`yield:${l.key}`,level:"wafer",severity:"info",variable:{kind:"yield",label:"Yield"},comparison:{family:l.family,left:l.label,right:O(l.family)},effect:{direction:S===0?"different":S>0?"higher":"lower",absoluteDelta:S,relativeDelta:m===0?void 0:S/m,effectSize:S},stats:{method:"two-proportion-z",pValue:D,sampleSizeLeft:u,sampleSizeRight:g},summary:he(l.label,S,l.family),highlight:{kind:"region",regionFamily:l.family,regionKeys:[l.key],dieKeys:[...l.dieKeys]}})}return oe(t,e)}function L(s,a,o,e,n,f){const r=d=>o==="soft"?d.sbin:d.hbin,i=[...new Set(s.map(r).filter(d=>d!==void 0))].sort((d,h)=>d-h),t=se(s,a),l=new Map,u=new Map;for(const[d,h]of t){const m=new Map;for(const S of h){const D=r(S);D!==void 0&&m.set(D,(m.get(D)??0)+1)}l.set(d,m),u.set(d,h.length)}const b=[],g=n==="hardBin"?"HBin":"SBin";for(const d of a){const h=u.get(d.key)??0,m=l.get(d.key);let S=0;const D=new Map;for(const[$,p]of l)if($!==d.key){S+=u.get($)??0;for(const[w,k]of p)D.set(w,(D.get(w)??0)+k)}if(!(h<f.minimumSampleSize||S<f.minimumSampleSize))for(const $ of i){const p=m.get($)??0,w=D.get($)??0,k=p/h,c=w/S,y=k-c,z=H(p,h,w,S),v=pe($,e,g);b.push({id:`${n}:${$}:${d.key}`,level:"wafer",severity:"info",variable:{kind:n,bin:$,label:v},comparison:{family:d.family,left:d.label,right:O(d.family)},effect:{direction:y===0?"different":y>0?"higher":"lower",absoluteDelta:y,relativeDelta:c===0?void 0:y/c,effectSize:y},stats:{method:"two-proportion-z",pValue:z,sampleSizeLeft:h,sampleSizeRight:S},summary:me(d.label,v,y,d.family),highlight:{kind:"bin",bin:$,regionKeys:[d.key],dieKeys:[...d.dieKeys]}})}}return oe(b,f)}function Q(s,a,o,e){const n=(o??[]).filter(u=>!q(u));if(!n.length)return[];const f=n.map(u=>u.testNumber),r=se(s,a),i=new Map,t=new Map;for(const[u,b]of r){const g=new Uint32Array(n.length),d=new Uint32Array(n.length);for(const h of b)for(let m=0;m<n.length;m++){const S=ce(h,f[m],n[m]);S!==void 0&&(d[m]++,S&&g[m]++)}i.set(u,g),t.set(u,d)}const l=[];for(const u of a){const b=i.get(u.key),g=t.get(u.key);for(let d=0;d<n.length;d++){let h=0,m=0;for(const[y,z]of t)y!==u.key&&(m+=z[d],h+=i.get(y)[d]);const S=g[d],D=b[d];if(S<e.minimumSampleSize||m<e.minimumSampleSize)continue;const $=D/S,p=h/m,w=$-p,k=H(D,S,h,m),c=f[d];l.push({id:`functional:${c}:${u.key}`,level:"wafer",severity:"info",variable:{kind:"functionalTest",index:c,label:`${n[d].name} pass rate`},comparison:{family:u.family,left:u.label,right:O(u.family)},effect:{direction:w===0?"different":w>0?"higher":"lower",absoluteDelta:w,relativeDelta:p===0?void 0:w/p,effectSize:w},stats:{method:"two-proportion-z",pValue:k,sampleSizeLeft:S,sampleSizeRight:m},summary:xe(u.label,n[d].name,w,u.family),highlight:{kind:"region",regionFamily:u.family,regionKeys:[u.key],dieKeys:[...u.dieKeys]}})}}return oe(l,e)}function ae(s,a){return s.length<2?0:s.reduce((o,e)=>o+(e-a)**2,0)/(s.length-1)}function ye(s,a,o,e,n,f){const r=Math.sqrt(o/s+f/e),i=a-n;if(!Number.isFinite(r)||r===0)return{pValue:1,effectSize:0,delta:i};const t=i/r,l=Math.sqrt(Math.max(0,(o+f)/2)),u=l===0?i:i/l;return{pValue:ge(2*(1-de(Math.abs(t)))),effectSize:u,delta:i}}function qe(s,a){const o=G(s),e=G(a);return ye(s.length,o,ae(s,o),a.length,e,ae(a,e))}const Se=250;function we(s,a,o){const e=r=>r.filter(i=>q(o?.find(t=>t.testNumber===i)));if(a)return{testNumbers:e(a.slice().sort((r,i)=>r-i))};const n=new Set;let f=!1;e:for(const r of s)for(const i of Object.keys(r.testValues??{})){const t=Number(i);if(!n.has(t)&&(n.add(t),n.size>Se)){f=!0;break e}}if(f){const r=`Test-value analysis was skipped: more than ${Se} tests were found in the die data, so no test findings were computed. Pass testNumbers: [...] to analyse specific tests.`;return console.warn(`[wafermap] analyzeWaferMap: ${r}`),{testNumbers:[],warning:{code:"test-count-capped",message:r,severity:"warning"}}}return{testNumbers:e([...n].sort((r,i)=>r-i))}}function _(s,a,o,e){const n=we(s,e.testNumbers,o);if(n.warning)return{findings:[],warning:n.warning};const f=n.testNumbers,r=new Map;for(let h=0;h<a.length;h++)for(const m of a[h].dieKeys)r.set(m,h);const i=a.length,t=[],l=[];for(const h of s){const m=r.get(E(h));m!==void 0&&(t.push(h),l.push(m))}const u=[],b=new Float64Array(i),g=new Float64Array(i),d=new Float64Array(i);for(const h of f){b.fill(0),g.fill(0),d.fill(0);let m=0,S=0,D=0,$;for(let p=0;p<t.length;p++){const k=t[p].testValues?.[h];if(k===void 0)continue;$===void 0&&($=k);const c=k-$,y=l[p];b[y]+=1,g[y]+=c,d[y]+=c*c,m+=1,S+=c,D+=c*c}if($!==void 0)for(let p=0;p<i;p++){const w=b[p],k=m-w;if(w<e.minimumSampleSize||k<e.minimumSampleSize)continue;const c=g[p]/w,z=(S-g[p])/k,v=c+$,M=z+$,F=w>1?Math.max(0,d[p]-w*c*c)/(w-1):0,R=k>1?Math.max(0,D-d[p]-k*z*z)/(k-1):0,{pValue:C,effectSize:V,delta:K}=ye(w,v,F,k,M,R),P=a[p],{label:W,unit:U}=Te(h,o),T=M!==0?K/Math.abs(M):void 0;u.push({id:`test:${h}:${P.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:h,label:W,unit:U},comparison:{family:P.family,left:P.label,right:O(P.family)},effect:{direction:K===0?"different":K>0?"higher":"lower",absoluteDelta:K,relativeDelta:T,effectSize:V},stats:{method:"welch-z-approx",pValue:C,sampleSizeLeft:w,sampleSizeRight:k},summary:be(P.label,W,K,T,P.family,U),highlight:{kind:"region",regionFamily:P.family,regionKeys:[P.key],dieKeys:[...P.dieKeys]}})}}return ee(u),{activeTestNumbers:f,findings:u.filter(h=>{const m=h.stats.adjustedPValue??h.stats.pValue??1,S=Math.abs(h.effect.effectSize??0);return m<=e.significanceLevel&&S>=e.minimumEffectSize}).map(h=>({...h,severity:ne(h.stats.adjustedPValue??h.stats.pValue??1,h.effect.effectSize??0)}))}}function je(s,a,o,e){if(!o?.length)return[];const n=o.filter(i=>q(i)&&(i.limitLow!==void 0||i.limitHigh!==void 0));if(!n.length)return[];const f=[],r=new Map(s.map(i=>[E(i),i]));for(const i of n){const t=i.testNumber;for(const l of a){const u=new Map;for(const g of l){const d=[];for(const h of g.dieKeys){const m=r.get(h);m&&d.push(m)}u.set(g.key,d)}const b=[];for(const g of l){const d=u.get(g.key),h=[];for(const[v,M]of u)if(v!==g.key)for(const F of M)h.push(F);const m=v=>v.testValues?.[t]!==void 0,S=v=>{const M=v.testValues?.[t];return M===void 0?!1:i.limitLow!==void 0&&M<i.limitLow||i.limitHigh!==void 0&&M>i.limitHigh},D=d.filter(m),$=h.filter(m);if(D.length<e.minimumSampleSize||$.length<e.minimumSampleSize)continue;const p=D.filter(S).length,w=$.filter(S).length,k=p/D.length,c=w/$.length,y=k-c,z=H(p,D.length,w,$.length);b.push({id:`specLimit:${t}:${g.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:t,label:i.name,unit:i.unit},comparison:{family:g.family,left:g.label,right:O(g.family)},effect:{direction:y===0?"different":y>0?"higher":"lower",absoluteDelta:y,relativeDelta:c===0?void 0:y/c,effectSize:y},stats:{method:"two-proportion-z",pValue:z,sampleSizeLeft:D.length,sampleSizeRight:$.length},summary:`${g.label} spec-fail rate for ${i.name} is ${(Math.abs(y)*100).toFixed(1)} pp ${y>0?"higher":"lower"} than the rest of the wafer`,highlight:{kind:"region",regionFamily:g.family,regionKeys:[g.key],dieKeys:[...g.dieKeys]}})}ee(b),f.push(...b.filter(g=>{const d=g.stats.adjustedPValue??g.stats.pValue??1,h=Math.abs(g.effect.absoluteDelta??0),m=Math.abs(g.effect.relativeDelta??0);return d<=e.significanceLevel&&(h>=e.minimumEffectSize||m>=e.minimumRelativeEffect)}).map(g=>({...g,severity:ne(g.stats.adjustedPValue??g.stats.pValue??1,g.effect.effectSize??0)})))}}return f}function x(s){return s.highlight.regionKeys??[]}function He(s){const a=s.variable.bin??s.variable.index??"";return`${s.comparison.family}\0${s.variable.kind}\0${a}\0${s.effect.direction}`}function I(s,a){const o=x(s)[0]??"",e=j(o);return e.family==="ring"?e.ring??0:e.family==="sector"?ue(a).indexOf(e.sector??""):0}function Oe(s,a,o){if(a==="quadrant"){const r=new Map;for(const u of s){const b=j(x(u)[0]??"").quadrant;b&&r.set(b,u)}const i=[...r.keys()],t=new Set,l=[];for(const u of i){if(t.has(u))continue;const b=[],g=[u];for(t.add(u);g.length;){const d=g.pop();b.push(d);for(const h of i)!t.has(h)&&Re(d,h)&&(t.add(h),g.push(h))}l.push(b.map(d=>r.get(d)))}return l}const e=[...s].sort((r,i)=>I(r,o)-I(i,o)),n=[];let f=[];for(const r of e){if(f.length===0){f.push(r);continue}const i=I(f[f.length-1],o);I(r,o)-i===1?f.push(r):(n.push(f),f=[r])}if(f.length&&n.push(f),a==="sector"&&n.length>1){const i=ue(o).length,t=I(n[0][0],o),l=I(n[n.length-1][n[n.length-1].length-1],o);if(t===0&&l===i-1){const u=n.pop();n[0]=[...u,...n[0]]}}return n}function Ie(s,a){const o=s.map(r=>j(x(r)[0]??"").ring).filter(r=>r!==void 0).sort((r,i)=>r-i),e=o[0],n=o[o.length-1];let f="";return e===n&&e===1&&a>1?f=" (core)":e===n&&e===a&&a>1&&(f=" (edge)"),e===n?`Ring ${e}${f}`:`Rings ${e}\u2013${n}`}function Ye(s,a){const o=s.map(e=>j(x(e)[0]??"").sector??"");return o.length===1?`Sector ${o[0]}`:`Sectors ${o[0]}\u2013${o[o.length-1]}`}function Qe(s){const a=s.map(o=>j(x(o)[0]??"").quadrant??"");return a.length===1?a[0]:a.length===2?`Quadrants ${a[0]} & ${a[1]}`:`Quadrants ${a.slice(0,-1).join(", ")} & ${a[a.length-1]}`}function re(s){return[...new Set(s)]}function _e(s,a){const o=s[0],e=o.comparison.family,n=o.variable.kind,f=re(s.flatMap(p=>p.highlight.dieKeys??[])),r=re(s.flatMap(x)),i=new Set(f),t=e==="ring"?Ie(s,a.ringCount):e==="sector"?Ye(s,a.sectorCount):Qe(s),l=n==="softBin"?a.softEligibleDies:n==="test"?a.testDies:a.eligibleDies,u=l.filter(p=>i.has(E(p))),b=l.filter(p=>!i.has(E(p)));let g,d,h,m,S;if(n==="test"){const p=o.variable.index,w=R=>R.testValues?.[p],k=u.map(w).filter(R=>R!==void 0),c=b.map(w).filter(R=>R!==void 0),{pValue:y,effectSize:z,delta:v}=qe(k,c),M=G(c),F=M!==0?v/Math.abs(M):void 0;g={direction:v===0?"different":v>0?"higher":"lower",absoluteDelta:v,relativeDelta:F,effectSize:z},d={method:"welch-z-approx",pValue:y,sampleSizeLeft:k.length,sampleSizeRight:c.length},h=ne(y,z),m=be(t,o.variable.label,v,F,e,o.variable.unit),S=`test:${p}`}else if(n==="yield"){const p=new Set(a.passBins),w=u.filter(V=>V.hbin!==void 0),k=b.filter(V=>V.hbin!==void 0),c=w.length,y=k.length,z=w.filter(V=>p.has(V.hbin)).length,v=k.filter(V=>p.has(V.hbin)).length,M=c>0?z/c:0,F=y>0?v/y:0,R=M-F,C=H(z,c,v,y);g={direction:R===0?"different":R>0?"higher":"lower",absoluteDelta:R,relativeDelta:F===0?void 0:R/F,effectSize:R},d={method:"two-proportion-z",pValue:C,sampleSizeLeft:c,sampleSizeRight:y},h=te(C,R,g.relativeDelta),m=he(t,R,e),S="yield"}else{const p=o.variable.bin,w=R=>n==="softBin"?R.sbin:R.hbin,k=u.filter(R=>w(R)===p).length,c=b.filter(R=>w(R)===p).length,y=k/u.length,z=c/b.length,v=y-z,M=H(k,u.length,c,b.length);g={direction:v===0?"different":v>0?"higher":"lower",absoluteDelta:v,relativeDelta:z===0?void 0:v/z,effectSize:v},d={method:"two-proportion-z",pValue:M,sampleSizeLeft:u.length,sampleSizeRight:b.length},h=te(M,v,g.relativeDelta);const F=n==="softBin"?a.sbinDefs:a.hbinDefs;m=me(t,pe(p,F,n==="softBin"?"SBin":"HBin"),v,e),S=`${n}:${p}`}const D=re(r.map(p=>p.replace(`${e}:`,""))).join("-"),$=n==="hardBin"||n==="softBin"?{kind:"bin",bin:o.variable.bin,regionKeys:r,dieKeys:f}:{kind:"region",regionFamily:e,regionKeys:r,dieKeys:f};return{...o,id:`${S}:${e}:${D}`,severity:h,comparison:{family:e,left:t,right:O(e)},effect:g,stats:d,summary:m,highlight:$,relatedIds:s.map(p=>p.id)}}function We(s,a){const o=new Set(["ring","quadrant","sector"]),e=[],n=new Map;for(const r of s){if(!o.has(r.comparison.family)){e.push(r);continue}const i=He(r),t=n.get(i)??[];t.push(r),n.set(i,t)}const f=[];for(const r of n.values()){if(r.length===1){f.push(r[0]);continue}const i=r[0].comparison.family;for(const t of Oe(r,i,a.sectorCount))f.push(t.length===1?t[0]:_e(t,a))}return[...e,...f]}function Ue(s){const a=new Map,o=new Map;for(const n of s){const f=E(n);n.hbin!==void 0&&n.hbin!==null&&(a.get(n.hbin)??a.set(n.hbin,new Set).get(n.hbin)).add(f),n.sbin!==void 0&&n.sbin!==null&&(o.get(n.sbin)??o.set(n.sbin,new Set).get(n.sbin)).add(f)}const e=new Set;for(const[n,f]of a)for(const[r,i]of o){if(f.size!==i.size)continue;let t=!0;for(const l of f)if(!i.has(l)){t=!1;break}t&&e.add(`${n}:${r}`)}return e}function Ge(s){return`${s.comparison.family}\0${s.comparison.left}`}function Je(s,a,o){const e=(i,t)=>{i.absorbedIds=[...i.absorbedIds??[],t.id]},n=new Map;for(const i of s){const t=Ge(i);(n.get(t)??n.set(t,[]).get(t)).push(i)}const f=Ue(a),r=new Set;for(const i of n.values()){for(const l of i)if(!(l.variable.kind!=="hardBin"||l.variable.bin===void 0))for(const u of i){if(u.variable.kind!=="softBin"||u.variable.bin===void 0||r.has(u.id)||!f.has(`${l.variable.bin}:${u.variable.bin}`)||u.effect.direction!==l.effect.direction)continue;e(l,u),r.add(u.id);const b=`${l.variable.label} and ${u.variable.label} (same dies)`;l.summary=l.summary.replace(l.variable.label,b),l.variable.label=b;break}if(o.length!==1)continue;const t=i.find(l=>l.variable.kind==="yield");if(t){for(const l of i)if(!(l===t||r.has(l.id)||!((l.variable.kind==="hardBin"||l.variable.kind==="softBin")&&l.variable.bin===o[0]))){e(t,l),r.add(l.id);for(const b of l.absorbedIds??[])t.absorbedIds=[...t.absorbedIds??[],b]}}}}export function analyzeWaferMap(s,a={}){const o={...Pe,...a},e=Le(s),n=e.isLotStack,f=e.aggrMethod,r=!n||f==="mode"||f==="countBin"||f==="percent",i=e.dies.filter(c=>Ne(c,o)),t=Ve(o,i.length),l=e.dies.filter(c=>X(c,t)),u=De(l,e.wafer,t.ringCount),b=$e(l,e.wafer,t.ringCount),g=t.enableReticlePositionAnalysis?ke(l,e.reticleConfig):[],d=t.enableTestSiteAnalysis===!1?[]:Me(l,t.enableTestSiteAnalysis===!0),h=t.enableAngularAnalysis?ze(l,e.wafer,t.sectorCount):[],m=[];if(t.enableYieldAnalysis&&r&&m.push(...Y(i,u,t.passBins,t),...Y(i,b,t.passBins,t),...Y(i,g,t.passBins,t),...Y(i,d,t.passBins,t),...Y(i,h,t.passBins,t)),t.enableHardBinAnalysis&&r&&m.push(...L(i,u,"hard",e.hbinDefs,"hardBin",t),...L(i,b,"hard",e.hbinDefs,"hardBin",t),...L(i,g,"hard",e.hbinDefs,"hardBin",t),...L(i,d,"hard",e.hbinDefs,"hardBin",t),...L(i,h,"hard",e.hbinDefs,"hardBin",t)),t.enableSoftBinAnalysis){const c=i.filter(y=>y.sbin!==void 0);m.push(...L(c,u,"soft",e.sbinDefs,"softBin",t),...L(c,b,"soft",e.sbinDefs,"softBin",t),...L(c,g,"soft",e.sbinDefs,"softBin",t),...L(c,d,"soft",e.sbinDefs,"softBin",t),...L(c,h,"soft",e.sbinDefs,"softBin",t))}const S=[];let D;if(t.enableTestValueAnalysis){const c=_(i,u,e.testDefs,t),y=_(i,b,e.testDefs,t),z=_(i,g,e.testDefs,t),v=_(i,d,e.testDefs,t),M=_(i,h,e.testDefs,t);m.push(...c.findings,...y.findings,...z.findings,...v.findings,...M.findings),c.warning&&S.push(c.warning),D=c.activeTestNumbers,m.push(...je(i,[u,b,g,d,h],e.testDefs,t)),m.push(...Q(i,u,e.testDefs,t),...Q(i,b,e.testDefs,t),...Q(i,g,e.testDefs,t),...Q(i,d,e.testDefs,t),...Q(i,h,e.testDefs,t))}if(t.enableClusterAnalysis){const c=Ce(n,r,e.testDefs);(!n||r||c!==void 0)&&m.push(...Be(i,e.wafer,{...t,isFailingDie:c}))}const $=We(m,{eligibleDies:i,softEligibleDies:i.filter(c=>c.sbin!==void 0),testDies:i,passBins:t.passBins,ringCount:t.ringCount,sectorCount:t.sectorCount,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs});if(m.length=0,m.push(...$),t.enablePatternClassification&&r){const c=Fe(i,e.wafer,{passBins:t.passBins,ringCount:t.ringCount});if(c!==null&&c.pattern!=="random"&&c.pattern!=="none"){let U=function(B){const N=x(B).map(A=>j(A).ring).filter(A=>A!==void 0);if(N.length===0)return!0;const le=N.some(A=>A===W),fe=N.some(A=>A===1);return P==="edge-ring"||P==="edge-local"?le:P==="center"?fe:P==="donut"?!le&&!fe:!0};const z={center:"Center cluster",donut:"Donut","edge-ring":"Edge-ring","edge-local":"Edge-local",scratch:"Scratch","near-full":"Near-full"}[c.pattern]??c.pattern,v=c.confidence==="high"?"unusual":c.confidence==="medium"?"notable":"info",M=c.features,F=B=>`${(B*100).toFixed(0)}%`,R=c.pattern==="edge-ring"||c.pattern==="edge-local"?`${F(M.edgeRdd)} of edge dies failing`:c.pattern==="center"||c.pattern==="donut"?`centroid at ${F(M.centroidDistNorm)} of wafer radius from centre`:c.pattern==="scratch"?`linear score ${M.linearScore.toFixed(2)}, eccentricity ${M.eccentricity.toFixed(2)}`:`${F(M.globalRdd)} of dies failing`,C=i.filter(B=>{const N=B.hbin??B.sbin;return N!==void 0&&!new Set(t.passBins).has(N)}),V={"edge-ring":["ring","edge-arc"],"edge-local":["edge-arc","sector","quadrant"],center:["ring","cluster"],donut:["ring"],scratch:["cluster","sector","quadrant"],"near-full":["ring"]},K=new Set(V[c.pattern]??[]),P=c.pattern,W=t.ringCount,T=[];for(const B of m){if(!K.has(B.comparison.family)||B.comparison.family==="ring"&&!U(B))continue;T.push(B.id);const N=B.comparison.family==="ring"||B.comparison.family==="quadrant"||B.comparison.family==="sector";v!=="info"&&N&&(B.severity="info")}m.push({id:`spatial-pattern:${c.pattern}`,level:"wafer",severity:v,variable:{kind:"spatialPattern",label:z},comparison:{family:"spatial-pattern",left:z,right:"Wafer"},effect:{direction:"different",effectSize:M.globalRdd},stats:{method:"geometry",sampleSizeLeft:C.length,sampleSizeRight:i.length},summary:`Spatial pattern: ${z.toLowerCase()} (${c.confidence} confidence) \u2014 ${R}${c.note?` [${c.note}]`:""}`,highlight:{kind:"dies",dieKeys:C.map(B=>E(B))},relatedIds:T.length>0?T:void 0})}}m.sort((c,y)=>{const z=c.stats.adjustedPValue??c.stats.pValue??1,v=y.stats.adjustedPValue??y.stats.pValue??1;return z!==v?z-v:Math.abs(y.effect.absoluteDelta??0)-Math.abs(c.effect.absoluteDelta??0)});const p=Ke(e.dies,i.length,e.yield.yieldPercent);n&&(p.isLotStack=!0,f&&(p.aggregationMethod=f));const w=Ae(e.dies,e.testDefs);w&&(p.testSpecYield=w);const k=computeFunctionalYield(e.dies,e.testDefs);if(k&&(p.functionalYield=k),D===void 0&&t.computePerTestStats){const c=we(i,t.testNumbers,e.testDefs);c.warning&&S.push(c.warning),D=c.testNumbers}if(D?.length){const c=Ee(e.dies,D,e.testDefs,t.minimumSampleSize);c&&(p.perTestStats=c)}return S.length>0&&(p.warnings=S),Je(m,i,t.passBins),{level:"wafer",hasNotableFindings:m.some(c=>c.severity==="notable"||c.severity==="unusual"),findings:m,wafer:e.wafer.metadata??void 0,stats:p}}
1
+ import{isYieldEligibleDie as Z,getDieKey as x,isPositionedDie as ue}from"../core/dies.js";import{buildWaferMap as ke,getTestPassStatus as de,isParametricTest as q}from"../renderer/buildWaferMap.js";import{buildQuadrantRegions as ze,buildReticlePositionRegions as Me,buildRingRegions as Re,buildSectorRegions as Be,buildTestSiteRegions as Fe,sectorCompassNames as ge,areQuadrantsAdjacent as Pe,parseRegionKey as j}from"./regions.js";import{buildClusterFindings as Ve}from"./clusterDetection.js";import{classifyPattern as Le}from"./patternClassification.js";import{normalCdf as he}from"./math.js";import{quantile as ee}from"./math.js";import{mean as G,clamp01 as me}from"../core/utils.js";const Ne={ringCount:4,passBins:[1],significanceLevel:.05,minimumEffectSize:.2,minimumRelativeEffect:1,minimumSampleSize:5,includePartial:!1,includeEdgeExcluded:!1,enableYieldAnalysis:!0,enableHardBinAnalysis:!0,enableSoftBinAnalysis:!0,enableTestValueAnalysis:!1,computePerTestStats:!1,enableReticlePositionAnalysis:!0,enableClusterAnalysis:!0,enableAngularAnalysis:!0,sectorCount:8,enablePatternClassification:!0,minimumClusterSize:5};function Ce(s,a){const o={...s};return o.minimumClusterSize=Math.max(5,Math.round(a*.003)),o}function Ke(s){return"wafer"in s&&"dies"in s&&"view"in s?s:ke(s)}function Ee(s,a){return Z(s,a)?s.hbin!==void 0||s.sbin!==void 0||s.testValues!==void 0&&Object.keys(s.testValues).length>0:!1}function Ae(s,a,o){if(!s||a)return;const e=(o??[]).filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(e.length!==0)return n=>{for(const f of e){const r=f.testNumber,i=n.testValues?.[r];if(i!==void 0&&(f.limitLow!==void 0&&i<f.limitLow||f.limitHigh!==void 0&&i>f.limitHigh))return!0}return!1}}function xe(s,a,o){const e=new Set,n=new Set,f=new Set,r=new Map,i=new Map;for(const l of s){if(l.testValues)for(const t of Object.keys(l.testValues))e.add(Number(t));if(l.testPass)for(const t of Object.keys(l.testPass))e.add(Number(t));l.hbin!==void 0&&n.add(l.hbin),l.sbin!==void 0&&f.add(l.sbin),Z(l)&&(l.hbin!==void 0&&r.set(l.hbin,(r.get(l.hbin)??0)+1),l.sbin!==void 0&&i.set(l.sbin,(i.get(l.sbin)??0)+1))}return{totalDies:s.length,analyzedDies:a,excludedDies:s.length-a,yieldPercent:o,testsConsidered:[...e].sort((l,t)=>l-t),hardBinsConsidered:[...n].sort((l,t)=>l-t),softBinsConsidered:[...f].sort((l,t)=>l-t),...r.size?{hardBinCounts:Object.fromEntries(r)}:{},...i.size?{softBinCounts:Object.fromEntries(i)}:{}}}function Te(s,a){if(!a?.length)return;const o=a.filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(!o.length)return;const e=[];for(const n of o){const f=n.testNumber;let r=0,i=0,l=0,t=0;for(const c of s){if(c.partial||c.edgeExcluded)continue;const m=c.testValues?.[f];m!==void 0&&(t++,n.limitLow!==void 0&&m<n.limitLow?i++:n.limitHigh!==void 0&&m>n.limitHigh?l++:r++)}e.push({testNumber:f,label:n.name,passDies:r,failLowDies:i,failHighDies:l,totalDies:t,yieldPercent:t>0?r/t*100:null})}return e.length?e:void 0}export function computeFunctionalYield(s,a){if(!a?.length)return;const o=a.filter(n=>!q(n));if(!o.length)return;const e=[];for(const n of o){const f=n.testNumber;let r=0,i=0;for(const t of s){if(t.partial||t.edgeExcluded)continue;const c=de(t,f,n);c!==void 0&&(c?r++:i++)}const l=r+i;e.push({testNumber:f,label:n.name,passDies:r,failDies:i,totalDies:l,passRatePercent:l>0?r/l*100:null})}return e.length?e:void 0}function qe(s,a,o,e){const n=[];for(const f of a){const r=[];for(const m of s){if(m.partial||m.edgeExcluded)continue;const g=m.testValues?.[f];g!==void 0&&r.push(g)}if(r.length<e)continue;const i=G(r),l=Math.sqrt(re(r,i)),t=r.slice().sort((m,g)=>m-g),c=o?.find(m=>m.testNumber===f)?.name??String(f);n.push({testNumber:f,label:c,count:r.length,min:t[0],max:t[t.length-1],mean:i,stddev:l,median:ee(t,.5),q1:ee(t,.25),q3:ee(t,.75)})}return n.length?n:void 0}function H(s,a,o,e){const n=(s+o)/(a+e),f=n*(1-n)*(1/a+1/e);if(!Number.isFinite(f)||f<=0)return 1;const r=(s/a-o/e)/Math.sqrt(f);return me(2*(1-he(Math.abs(r))))}function te(s){const a=new Map;for(const o of s){const e=`${o.variable.kind}:${o.comparison.family}`,n=a.get(e)??[];n.push(o),a.set(e,n)}for(const o of a.values()){const e=[...o].filter(r=>r.stats.pValue!==void 0).sort((r,i)=>(r.stats.pValue??1)-(i.stats.pValue??1)),n=e.length;let f=1;for(let r=n-1;r>=0;r--){const i=e[r],l=i.stats.pValue??1,t=Math.min(1,Math.min(f,l*n/(r+1)));i.stats.adjustedPValue=t,f=t}}return s}function ne(s,a,o){const e=Math.abs(a),n=o!==void 0?Math.abs(o):0;return s<=.01&&(e>=.3||n>=2.5)?"unusual":s<=.05&&(e>=.2||n>=1.5)?"notable":"info"}function ie(s,a){return s<=.01&&Math.abs(a)>=.5?"unusual":s<=.05&&Math.abs(a)>=.15?"notable":"info"}function J(s){return s==="reticle-position"?"other reticle positions":s==="test-site"?"other test sites":"the rest of the map"}function O(s){return s==="reticle-position"?"Other reticle positions":s==="test-site"?"Other test sites":"Rest of map"}function be(s,a,o){const e=(Math.abs(a)*100).toFixed(1);return`${s} yield is ${e} percentage points ${a>0?"higher":"lower"} than ${J(o)}`}function se(s,a){return a==="quadrant"&&!s.startsWith("Quadrant")?`quadrant ${s}`:s}function pe(s,a,o,e){const n=se(s,e),f=(Math.abs(o)*100).toFixed(1);return`${n} has ${a} occurrence ${f} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function je(s,a,o,e){const n=se(s,e),f=(Math.abs(o)*100).toFixed(1);return`${n} has ${a} pass rate ${f} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function ye(s,a,o,e,n,f){const r=se(s,n),i=J(n),l=o>0?"higher":"lower";if(e!==void 0&&Number.isFinite(e)){const c=(Math.abs(e)*100).toFixed(1);return`${r} mean ${a} is ${c}% ${l} than ${i}`}const t=f?` ${f}`:"";return`${r} mean ${a} is ${Math.abs(o).toPrecision(3)}${t} ${l} than ${i}`}function Se(s,a,o){const e=a?.find(n=>n.bin===s);return e?.name?`${o} ${s} (${e.name})`:`${o} ${s}`}function He(s,a){const o=a?.find(e=>e.testNumber===s);return{label:o?.name??`Test ${s}`,unit:o?.unit}}function oe(s,a){const o=new Map(s.map(n=>[x(n),n])),e=new Map;for(const n of a){const f=[];for(const r of n.dieKeys){const i=o.get(r);i&&f.push(i)}e.set(n.key,f)}return e}function ae(s,a){return te(s),s.filter(o=>{const e=o.stats.adjustedPValue??o.stats.pValue??1,n=Math.abs(o.effect.absoluteDelta??0),f=Math.abs(o.effect.relativeDelta??0);return e<=a.significanceLevel&&(n>=a.minimumEffectSize||f>=a.minimumRelativeEffect)}).map(o=>({...o,severity:ne(o.stats.adjustedPValue??o.stats.pValue??1,o.effect.absoluteDelta??0,o.effect.relativeDelta)}))}function Y(s,a,o,e){const n=new Set(o),f=oe(s,a),r=new Map,i=new Map;for(const[t,c]of f){let m=0,g=0;for(const d of c)d.hbin!==void 0&&(g++,n.has(d.hbin)&&m++);r.set(t,m),i.set(t,g)}const l=[];for(const t of a){const c=i.get(t.key)??0,m=r.get(t.key)??0;let g=0,d=0;for(const[z,b]of i)z!==t.key&&(g+=b,d+=r.get(z)??0);if(c<e.minimumSampleSize||g<e.minimumSampleSize)continue;const h=m/c,p=d/g,y=h-p,S=H(m,c,d,g);l.push({id:`yield:${t.key}`,level:"wafer",severity:"info",variable:{kind:"yield",label:"Yield"},comparison:{family:t.family,left:t.label,right:O(t.family)},effect:{direction:y===0?"different":y>0?"higher":"lower",absoluteDelta:y,relativeDelta:p===0?void 0:y/p,effectSize:y},stats:{method:"two-proportion-z",pValue:S,sampleSizeLeft:c,sampleSizeRight:g},summary:be(t.label,y,t.family),highlight:{kind:"region",regionFamily:t.family,regionKeys:[t.key],dieKeys:[...t.dieKeys]}})}return ae(l,e)}function L(s,a,o,e,n,f){const r=d=>o==="soft"?d.sbin:d.hbin,i=[...new Set(s.map(r).filter(d=>d!==void 0))].sort((d,h)=>d-h),l=oe(s,a),t=new Map,c=new Map;for(const[d,h]of l){const p=new Map;for(const y of h){const S=r(y);S!==void 0&&p.set(S,(p.get(S)??0)+1)}t.set(d,p),c.set(d,h.length)}const m=[],g=n==="hardBin"?"HBin":"SBin";for(const d of a){const h=c.get(d.key)??0,p=t.get(d.key);let y=0;const S=new Map;for(const[z,b]of t)if(z!==d.key){y+=c.get(z)??0;for(const[w,D]of b)S.set(w,(S.get(w)??0)+D)}if(!(h<f.minimumSampleSize||y<f.minimumSampleSize))for(const z of i){const b=p.get(z)??0,w=S.get(z)??0,D=b/h,v=w/y,k=D-v,u=H(b,h,w,y),$=Se(z,e,g);m.push({id:`${n}:${z}:${d.key}`,level:"wafer",severity:"info",variable:{kind:n,bin:z,label:$},comparison:{family:d.family,left:d.label,right:O(d.family)},effect:{direction:k===0?"different":k>0?"higher":"lower",absoluteDelta:k,relativeDelta:v===0?void 0:k/v,effectSize:k},stats:{method:"two-proportion-z",pValue:u,sampleSizeLeft:h,sampleSizeRight:y},summary:pe(d.label,$,k,d.family),highlight:{kind:"bin",bin:z,regionKeys:[d.key],dieKeys:[...d.dieKeys]}})}}return ae(m,f)}function Q(s,a,o,e){const n=(o??[]).filter(c=>!q(c));if(!n.length)return[];const f=n.map(c=>c.testNumber),r=oe(s,a),i=new Map,l=new Map;for(const[c,m]of r){const g=new Uint32Array(n.length),d=new Uint32Array(n.length);for(const h of m)for(let p=0;p<n.length;p++){const y=de(h,f[p],n[p]);y!==void 0&&(d[p]++,y&&g[p]++)}i.set(c,g),l.set(c,d)}const t=[];for(const c of a){const m=i.get(c.key),g=l.get(c.key);for(let d=0;d<n.length;d++){let h=0,p=0;for(const[k,u]of l)k!==c.key&&(p+=u[d],h+=i.get(k)[d]);const y=g[d],S=m[d];if(y<e.minimumSampleSize||p<e.minimumSampleSize)continue;const z=S/y,b=h/p,w=z-b,D=H(S,y,h,p),v=f[d];t.push({id:`functional:${v}:${c.key}`,level:"wafer",severity:"info",variable:{kind:"functionalTest",index:v,label:`${n[d].name} pass rate`},comparison:{family:c.family,left:c.label,right:O(c.family)},effect:{direction:w===0?"different":w>0?"higher":"lower",absoluteDelta:w,relativeDelta:b===0?void 0:w/b,effectSize:w},stats:{method:"two-proportion-z",pValue:D,sampleSizeLeft:y,sampleSizeRight:p},summary:je(c.label,n[d].name,w,c.family),highlight:{kind:"region",regionFamily:c.family,regionKeys:[c.key],dieKeys:[...c.dieKeys]}})}}return ae(t,e)}function re(s,a){return s.length<2?0:s.reduce((o,e)=>o+(e-a)**2,0)/(s.length-1)}function we(s,a,o,e,n,f){const r=Math.sqrt(o/s+f/e),i=a-n;if(!Number.isFinite(r)||r===0)return{pValue:1,effectSize:0,delta:i};const l=i/r,t=Math.sqrt(Math.max(0,(o+f)/2)),c=t===0?i:i/t;return{pValue:me(2*(1-he(Math.abs(l)))),effectSize:c,delta:i}}function Oe(s,a){const o=G(s),e=G(a);return we(s.length,o,re(s,o),a.length,e,re(a,e))}const De=250;function ve(s,a,o){const e=r=>r.filter(i=>q(o?.find(l=>l.testNumber===i)));if(a)return{testNumbers:e(a.slice().sort((r,i)=>r-i))};const n=new Set;let f=!1;e:for(const r of s)for(const i of Object.keys(r.testValues??{})){const l=Number(i);if(!n.has(l)&&(n.add(l),n.size>De)){f=!0;break e}}if(f){const r=`Test-value analysis was skipped: more than ${De} tests were found in the die data, so no test findings were computed. Pass testNumbers: [...] to analyse specific tests.`;return console.warn(`[wafermap] analyzeWaferMap: ${r}`),{testNumbers:[],warning:{code:"test-count-capped",message:r,severity:"warning"}}}return{testNumbers:e([...n].sort((r,i)=>r-i))}}function _(s,a,o,e){const n=ve(s,e.testNumbers,o);if(n.warning)return{findings:[],warning:n.warning};const f=n.testNumbers,r=new Map;for(let h=0;h<a.length;h++)for(const p of a[h].dieKeys)r.set(p,h);const i=a.length,l=[],t=[];for(const h of s){const p=r.get(x(h));p!==void 0&&(l.push(h),t.push(p))}const c=[],m=new Float64Array(i),g=new Float64Array(i),d=new Float64Array(i);for(const h of f){m.fill(0),g.fill(0),d.fill(0);let p=0,y=0,S=0,z;for(let b=0;b<l.length;b++){const D=l[b].testValues?.[h];if(D===void 0)continue;z===void 0&&(z=D);const v=D-z,k=t[b];m[k]+=1,g[k]+=v,d[k]+=v*v,p+=1,y+=v,S+=v*v}if(z!==void 0)for(let b=0;b<i;b++){const w=m[b],D=p-w;if(w<e.minimumSampleSize||D<e.minimumSampleSize)continue;const v=g[b]/w,u=(y-g[b])/D,$=v+z,R=u+z,B=w>1?Math.max(0,d[b]-w*v*v)/(w-1):0,M=D>1?Math.max(0,S-d[b]-D*u*u)/(D-1):0,{pValue:N,effectSize:P,delta:C}=we(w,$,B,D,R,M),V=a[b],{label:W,unit:E}=He(h,o),U=R!==0?C/Math.abs(R):void 0;c.push({id:`test:${h}:${V.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:h,label:W,unit:E},comparison:{family:V.family,left:V.label,right:O(V.family)},effect:{direction:C===0?"different":C>0?"higher":"lower",absoluteDelta:C,relativeDelta:U,effectSize:P},stats:{method:"welch-z-approx",pValue:N,sampleSizeLeft:w,sampleSizeRight:D},summary:ye(V.label,W,C,U,V.family,E),highlight:{kind:"region",regionFamily:V.family,regionKeys:[V.key],dieKeys:[...V.dieKeys]}})}}return te(c),{activeTestNumbers:f,findings:c.filter(h=>{const p=h.stats.adjustedPValue??h.stats.pValue??1,y=Math.abs(h.effect.effectSize??0);return p<=e.significanceLevel&&y>=e.minimumEffectSize}).map(h=>({...h,severity:ie(h.stats.adjustedPValue??h.stats.pValue??1,h.effect.effectSize??0)}))}}function Ie(s,a,o,e){if(!o?.length)return[];const n=o.filter(i=>q(i)&&(i.limitLow!==void 0||i.limitHigh!==void 0));if(!n.length)return[];const f=[],r=new Map(s.map(i=>[x(i),i]));for(const i of n){const l=i.testNumber;for(const t of a){const c=new Map;for(const g of t){const d=[];for(const h of g.dieKeys){const p=r.get(h);p&&d.push(p)}c.set(g.key,d)}const m=[];for(const g of t){const d=c.get(g.key),h=[];for(const[$,R]of c)if($!==g.key)for(const B of R)h.push(B);const p=$=>$.testValues?.[l]!==void 0,y=$=>{const R=$.testValues?.[l];return R===void 0?!1:i.limitLow!==void 0&&R<i.limitLow||i.limitHigh!==void 0&&R>i.limitHigh},S=d.filter(p),z=h.filter(p);if(S.length<e.minimumSampleSize||z.length<e.minimumSampleSize)continue;const b=S.filter(y).length,w=z.filter(y).length,D=b/S.length,v=w/z.length,k=D-v,u=H(b,S.length,w,z.length);m.push({id:`specLimit:${l}:${g.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:l,label:i.name,unit:i.unit},comparison:{family:g.family,left:g.label,right:O(g.family)},effect:{direction:k===0?"different":k>0?"higher":"lower",absoluteDelta:k,relativeDelta:v===0?void 0:k/v,effectSize:k},stats:{method:"two-proportion-z",pValue:u,sampleSizeLeft:S.length,sampleSizeRight:z.length},summary:`${g.label} spec-fail rate for ${i.name} is ${(Math.abs(k)*100).toFixed(1)} pp ${k>0?"higher":"lower"} than the rest of the wafer`,highlight:{kind:"region",regionFamily:g.family,regionKeys:[g.key],dieKeys:[...g.dieKeys]}})}te(m),f.push(...m.filter(g=>{const d=g.stats.adjustedPValue??g.stats.pValue??1,h=Math.abs(g.effect.absoluteDelta??0),p=Math.abs(g.effect.relativeDelta??0);return d<=e.significanceLevel&&(h>=e.minimumEffectSize||p>=e.minimumRelativeEffect)}).map(g=>({...g,severity:ie(g.stats.adjustedPValue??g.stats.pValue??1,g.effect.effectSize??0)})))}}return f}function T(s){return s.highlight.regionKeys??[]}function Ye(s){const a=s.variable.bin??s.variable.index??"";return`${s.comparison.family}\0${s.variable.kind}\0${a}\0${s.effect.direction}`}function I(s,a){const o=T(s)[0]??"",e=j(o);return e.family==="ring"?e.ring??0:e.family==="sector"?ge(a).indexOf(e.sector??""):0}function Qe(s,a,o){if(a==="quadrant"){const r=new Map;for(const c of s){const m=j(T(c)[0]??"").quadrant;m&&r.set(m,c)}const i=[...r.keys()],l=new Set,t=[];for(const c of i){if(l.has(c))continue;const m=[],g=[c];for(l.add(c);g.length;){const d=g.pop();m.push(d);for(const h of i)!l.has(h)&&Pe(d,h)&&(l.add(h),g.push(h))}t.push(m.map(d=>r.get(d)))}return t}const e=[...s].sort((r,i)=>I(r,o)-I(i,o)),n=[];let f=[];for(const r of e){if(f.length===0){f.push(r);continue}const i=I(f[f.length-1],o);I(r,o)-i===1?f.push(r):(n.push(f),f=[r])}if(f.length&&n.push(f),a==="sector"&&n.length>1){const i=ge(o).length,l=I(n[0][0],o),t=I(n[n.length-1][n[n.length-1].length-1],o);if(l===0&&t===i-1){const c=n.pop();n[0]=[...c,...n[0]]}}return n}function _e(s,a){const o=s.map(r=>j(T(r)[0]??"").ring).filter(r=>r!==void 0).sort((r,i)=>r-i),e=o[0],n=o[o.length-1];let f="";return e===n&&e===1&&a>1?f=" (core)":e===n&&e===a&&a>1&&(f=" (edge)"),e===n?`Ring ${e}${f}`:`Rings ${e}\u2013${n}`}function We(s,a){const o=s.map(e=>j(T(e)[0]??"").sector??"");return o.length===1?`Sector ${o[0]}`:`Sectors ${o[0]}\u2013${o[o.length-1]}`}function Ue(s){const a=s.map(o=>j(T(o)[0]??"").quadrant??"");return a.length===1?a[0]:a.length===2?`Quadrants ${a[0]} & ${a[1]}`:`Quadrants ${a.slice(0,-1).join(", ")} & ${a[a.length-1]}`}function le(s){return[...new Set(s)]}function Ge(s,a){const o=s[0],e=o.comparison.family,n=o.variable.kind,f=le(s.flatMap(b=>b.highlight.dieKeys??[])),r=le(s.flatMap(T)),i=new Set(f),l=e==="ring"?_e(s,a.ringCount):e==="sector"?We(s,a.sectorCount):Ue(s),t=n==="softBin"?a.softEligibleDies:n==="test"?a.testDies:a.eligibleDies,c=t.filter(b=>i.has(x(b))),m=t.filter(b=>!i.has(x(b)));let g,d,h,p,y;if(n==="test"){const b=o.variable.index,w=M=>M.testValues?.[b],D=c.map(w).filter(M=>M!==void 0),v=m.map(w).filter(M=>M!==void 0),{pValue:k,effectSize:u,delta:$}=Oe(D,v),R=G(v),B=R!==0?$/Math.abs(R):void 0;g={direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:B,effectSize:u},d={method:"welch-z-approx",pValue:k,sampleSizeLeft:D.length,sampleSizeRight:v.length},h=ie(k,u),p=ye(l,o.variable.label,$,B,e,o.variable.unit),y=`test:${b}`}else if(n==="yield"){const b=new Set(a.passBins),w=c.filter(P=>P.hbin!==void 0),D=m.filter(P=>P.hbin!==void 0),v=w.length,k=D.length,u=w.filter(P=>b.has(P.hbin)).length,$=D.filter(P=>b.has(P.hbin)).length,R=v>0?u/v:0,B=k>0?$/k:0,M=R-B,N=H(u,v,$,k);g={direction:M===0?"different":M>0?"higher":"lower",absoluteDelta:M,relativeDelta:B===0?void 0:M/B,effectSize:M},d={method:"two-proportion-z",pValue:N,sampleSizeLeft:v,sampleSizeRight:k},h=ne(N,M,g.relativeDelta),p=be(l,M,e),y="yield"}else{const b=o.variable.bin,w=M=>n==="softBin"?M.sbin:M.hbin,D=c.filter(M=>w(M)===b).length,v=m.filter(M=>w(M)===b).length,k=D/c.length,u=v/m.length,$=k-u,R=H(D,c.length,v,m.length);g={direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:u===0?void 0:$/u,effectSize:$},d={method:"two-proportion-z",pValue:R,sampleSizeLeft:c.length,sampleSizeRight:m.length},h=ne(R,$,g.relativeDelta);const B=n==="softBin"?a.sbinDefs:a.hbinDefs;p=pe(l,Se(b,B,n==="softBin"?"SBin":"HBin"),$,e),y=`${n}:${b}`}const S=le(r.map(b=>b.replace(`${e}:`,""))).join("-"),z=n==="hardBin"||n==="softBin"?{kind:"bin",bin:o.variable.bin,regionKeys:r,dieKeys:f}:{kind:"region",regionFamily:e,regionKeys:r,dieKeys:f};return{...o,id:`${y}:${e}:${S}`,severity:h,comparison:{family:e,left:l,right:O(e)},effect:g,stats:d,summary:p,highlight:z,relatedIds:s.map(b=>b.id)}}function Je(s,a){const o=new Set(["ring","quadrant","sector"]),e=[],n=new Map;for(const r of s){if(!o.has(r.comparison.family)){e.push(r);continue}const i=Ye(r),l=n.get(i)??[];l.push(r),n.set(i,l)}const f=[];for(const r of n.values()){if(r.length===1){f.push(r[0]);continue}const i=r[0].comparison.family;for(const l of Qe(r,i,a.sectorCount))f.push(l.length===1?l[0]:Ge(l,a))}return[...e,...f]}function Xe(s){const a=new Map,o=new Map;for(const n of s){const f=x(n);n.hbin!==void 0&&n.hbin!==null&&(a.get(n.hbin)??a.set(n.hbin,new Set).get(n.hbin)).add(f),n.sbin!==void 0&&n.sbin!==null&&(o.get(n.sbin)??o.set(n.sbin,new Set).get(n.sbin)).add(f)}const e=new Set;for(const[n,f]of a)for(const[r,i]of o){if(f.size!==i.size)continue;let l=!0;for(const t of f)if(!i.has(t)){l=!1;break}l&&e.add(`${n}:${r}`)}return e}function Ze(s){return`${s.comparison.family}\0${s.comparison.left}`}function et(s,a,o){const e=(i,l)=>{i.absorbedIds=[...i.absorbedIds??[],l.id]},n=new Map;for(const i of s){const l=Ze(i);(n.get(l)??n.set(l,[]).get(l)).push(i)}const f=Xe(a),r=new Set;for(const i of n.values()){for(const t of i)if(!(t.variable.kind!=="hardBin"||t.variable.bin===void 0))for(const c of i){if(c.variable.kind!=="softBin"||c.variable.bin===void 0||r.has(c.id)||!f.has(`${t.variable.bin}:${c.variable.bin}`)||c.effect.direction!==t.effect.direction)continue;e(t,c),r.add(c.id);const m=`${t.variable.label} and ${c.variable.label} (same dies)`;t.summary=t.summary.replace(t.variable.label,m),t.variable.label=m;break}if(o.length!==1)continue;const l=i.find(t=>t.variable.kind==="yield");if(l){for(const t of i)if(!(t===l||r.has(t.id)||!((t.variable.kind==="hardBin"||t.variable.kind==="softBin")&&t.variable.bin===o[0]))){e(l,t),r.add(t.id);for(const m of t.absorbedIds??[])l.absorbedIds=[...l.absorbedIds??[],m]}}}}export function analyzeWaferMap(s,a={}){const o={...Ne,...a},e=Ke(s),n=e.isLotStack,f=e.aggrMethod,r=!n||f==="mode"||f==="countBin"||f==="percent",i=e.dies.filter(u=>Ee(u,o)),l=i.filter(ue),t=Ce(o,i.length),c=e.dies.filter(u=>Z(u,t)),m=c.filter(ue),g=Re(m,e.wafer,t.ringCount),d=ze(m,e.wafer,t.ringCount),h=t.enableReticlePositionAnalysis?Me(m,e.reticleConfig):[],p=t.enableTestSiteAnalysis===!1?[]:Fe(c,t.enableTestSiteAnalysis===!0),y=t.enableAngularAnalysis?Be(m,e.wafer,t.sectorCount):[],S=[];if(t.enableYieldAnalysis&&r&&S.push(...Y(i,g,t.passBins,t),...Y(i,d,t.passBins,t),...Y(i,h,t.passBins,t),...Y(i,p,t.passBins,t),...Y(i,y,t.passBins,t)),t.enableHardBinAnalysis&&r&&S.push(...L(i,g,"hard",e.hbinDefs,"hardBin",t),...L(i,d,"hard",e.hbinDefs,"hardBin",t),...L(i,h,"hard",e.hbinDefs,"hardBin",t),...L(i,p,"hard",e.hbinDefs,"hardBin",t),...L(i,y,"hard",e.hbinDefs,"hardBin",t)),t.enableSoftBinAnalysis){const u=i.filter($=>$.sbin!==void 0);S.push(...L(u,g,"soft",e.sbinDefs,"softBin",t),...L(u,d,"soft",e.sbinDefs,"softBin",t),...L(u,h,"soft",e.sbinDefs,"softBin",t),...L(u,p,"soft",e.sbinDefs,"softBin",t),...L(u,y,"soft",e.sbinDefs,"softBin",t))}const z=[];let b;if(t.enableTestValueAnalysis){const u=_(i,g,e.testDefs,t),$=_(i,d,e.testDefs,t),R=_(i,h,e.testDefs,t),B=_(i,p,e.testDefs,t),M=_(i,y,e.testDefs,t);S.push(...u.findings,...$.findings,...R.findings,...B.findings,...M.findings),u.warning&&z.push(u.warning),b=u.activeTestNumbers,S.push(...Ie(i,[g,d,h,p,y],e.testDefs,t)),S.push(...Q(i,g,e.testDefs,t),...Q(i,d,e.testDefs,t),...Q(i,h,e.testDefs,t),...Q(i,p,e.testDefs,t),...Q(i,y,e.testDefs,t))}if(t.enableClusterAnalysis){const u=Ae(n,r,e.testDefs);(!n||r||u!==void 0)&&S.push(...Ve(l,e.wafer,{...t,isFailingDie:u}))}const w=Je(S,{eligibleDies:i,softEligibleDies:i.filter(u=>u.sbin!==void 0),testDies:i,passBins:t.passBins,ringCount:t.ringCount,sectorCount:t.sectorCount,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs});if(S.length=0,S.push(...w),t.enablePatternClassification&&r){const u=Le(l,e.wafer,{passBins:t.passBins,ringCount:t.ringCount});if(u!==null&&u.pattern!=="random"&&u.pattern!=="none"){let $e=function(F){const K=T(F).map(A=>j(A).ring).filter(A=>A!==void 0);if(K.length===0)return!0;const fe=K.some(A=>A===U),ce=K.some(A=>A===1);return E==="edge-ring"||E==="edge-local"?fe:E==="center"?ce:E==="donut"?!fe&&!ce:!0};const R={center:"Center cluster",donut:"Donut","edge-ring":"Edge-ring","edge-local":"Edge-local",scratch:"Scratch","near-full":"Near-full"}[u.pattern]??u.pattern,B=u.confidence==="high"?"unusual":u.confidence==="medium"?"notable":"info",M=u.features,N=F=>`${(F*100).toFixed(0)}%`,P=u.pattern==="edge-ring"||u.pattern==="edge-local"?`${N(M.edgeRdd)} of edge dies failing`:u.pattern==="center"||u.pattern==="donut"?`centroid at ${N(M.centroidDistNorm)} of wafer radius from centre`:u.pattern==="scratch"?`linear score ${M.linearScore.toFixed(2)}, eccentricity ${M.eccentricity.toFixed(2)}`:`${N(M.globalRdd)} of dies failing`,C=i.filter(F=>{const K=F.hbin??F.sbin;return K!==void 0&&!new Set(t.passBins).has(K)}),V={"edge-ring":["ring","edge-arc"],"edge-local":["edge-arc","sector","quadrant"],center:["ring","cluster"],donut:["ring"],scratch:["cluster","sector","quadrant"],"near-full":["ring"]},W=new Set(V[u.pattern]??[]),E=u.pattern,U=t.ringCount,X=[];for(const F of S){if(!W.has(F.comparison.family)||F.comparison.family==="ring"&&!$e(F))continue;X.push(F.id);const K=F.comparison.family==="ring"||F.comparison.family==="quadrant"||F.comparison.family==="sector";B!=="info"&&K&&(F.severity="info")}S.push({id:`spatial-pattern:${u.pattern}`,level:"wafer",severity:B,variable:{kind:"spatialPattern",label:R},comparison:{family:"spatial-pattern",left:R,right:"Wafer"},effect:{direction:"different",effectSize:M.globalRdd},stats:{method:"geometry",sampleSizeLeft:C.length,sampleSizeRight:i.length},summary:`Spatial pattern: ${R.toLowerCase()} (${u.confidence} confidence) \u2014 ${P}${u.note?` [${u.note}]`:""}`,highlight:{kind:"dies",dieKeys:C.map(F=>x(F))},relatedIds:X.length>0?X:void 0})}}S.sort((u,$)=>{const R=u.stats.adjustedPValue??u.stats.pValue??1,B=$.stats.adjustedPValue??$.stats.pValue??1;return R!==B?R-B:Math.abs($.effect.absoluteDelta??0)-Math.abs(u.effect.absoluteDelta??0)});const D=xe(e.dies,i.length,e.yield.yieldPercent);n&&(D.isLotStack=!0,f&&(D.aggregationMethod=f));const v=Te(e.dies,e.testDefs);v&&(D.testSpecYield=v);const k=computeFunctionalYield(e.dies,e.testDefs);if(k&&(D.functionalYield=k),b===void 0&&t.computePerTestStats){const u=ve(i,t.testNumbers,e.testDefs);u.warning&&z.push(u.warning),b=u.testNumbers}if(b?.length){const u=qe(e.dies,b,e.testDefs,t.minimumSampleSize);u&&(D.perTestStats=u)}return z.length>0&&(D.warnings=z),et(S,i,t.passBins),{level:"wafer",hasNotableFindings:S.some(u=>u.severity==="notable"||u.severity==="unusual"),findings:S,wafer:e.wafer.metadata??void 0,stats:D}}
@@ -1,4 +1,4 @@
1
- import type { Die, Wafer } from '../core/index.js';
1
+ import type { Die, PositionedDie, Wafer } from '../core/index.js';
2
2
  import type { StatsFinding } from './types.js';
3
3
  interface ClusterOptions {
4
4
  passBins: number[];
@@ -13,6 +13,6 @@ interface ClusterOptions {
13
13
  /** Optional override for the failure predicate. Default: hard bin, then soft bin, not in passBins. */
14
14
  isFailingDie?: (die: Die) => boolean;
15
15
  }
16
- export declare function buildClusterFindings(dies: Die[], wafer: Wafer, options: ClusterOptions): StatsFinding[];
16
+ export declare function buildClusterFindings(dies: PositionedDie[], wafer: Wafer, options: ClusterOptions): StatsFinding[];
17
17
  export {};
18
18
  //# sourceMappingURL=clusterDetection.d.ts.map
@@ -1,4 +1,4 @@
1
- import type { Die, Wafer } from '../core/index.js';
1
+ import type { PositionedDie, Wafer } from '../core/index.js';
2
2
  export type PatternLabel = 'center' | 'donut' | 'edge-ring' | 'edge-local' | 'scratch' | 'near-full' | 'random' | 'none';
3
3
  export interface PatternFeatures {
4
4
  /** Ratio of failing dies to total eligible dies (whole-wafer). */
@@ -100,7 +100,7 @@ export declare const DEFAULT_PATTERN_THRESHOLDS: PatternThresholds;
100
100
  * Returns `null` when the failing-die count is below the minimum threshold
101
101
  * (`thresholds.minimumFailingDies`, default 5) — too few failures to classify.
102
102
  */
103
- export declare function classifyPattern(dies: Die[], wafer: Wafer, options: {
103
+ export declare function classifyPattern(dies: PositionedDie[], wafer: Wafer, options: {
104
104
  passBins: number[];
105
105
  ringCount?: number;
106
106
  }): PatternClassification | null;