@wafertools/wafermap 0.26.1 → 0.28.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 (109) hide show
  1. package/AGENTS.md +10 -2
  2. package/CHANGELOG.md +670 -0
  3. package/README.md +46 -3
  4. package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
  5. package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/binCluster.d.ts +0 -1
  9. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +21 -2
  11. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -11
  13. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
  15. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -10
  17. package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
  18. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
  19. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
  20. package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -2
  21. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  22. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +0 -1
  23. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  24. package/dist/packages/canvas-adapter/charts/scatter.d.ts +6 -1
  25. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  26. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
  27. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
  28. package/dist/packages/canvas-adapter/charts/trend.d.ts +37 -0
  29. package/dist/packages/canvas-adapter/charts/trend.js +1 -0
  30. package/dist/packages/canvas-adapter/dieList.js +7 -7
  31. package/dist/packages/canvas-adapter/hitGrid.d.ts +22 -0
  32. package/dist/packages/canvas-adapter/hitGrid.js +1 -0
  33. package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
  34. package/dist/packages/canvas-adapter/identityHeader.js +1 -0
  35. package/dist/packages/canvas-adapter/index.d.ts +2 -1
  36. package/dist/packages/canvas-adapter/index.js +1 -1
  37. package/dist/packages/canvas-adapter/insightsTab.d.ts +51 -5
  38. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  39. package/dist/packages/canvas-adapter/maplessSummary.d.ts +5 -1
  40. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  41. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
  42. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
  43. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +110 -18
  44. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  45. package/dist/packages/canvas-adapter/summaryPanel.d.ts +233 -37
  46. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  47. package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
  48. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  49. package/dist/packages/canvas-adapter/toolbar.d.ts +352 -15
  50. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  51. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  52. package/dist/packages/canvas-adapter/userGuideHtml.js +115 -41
  53. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  54. package/dist/packages/canvas-adapter/version.js +1 -1
  55. package/dist/packages/canvas-adapter/warnings.d.ts +21 -0
  56. package/dist/packages/canvas-adapter/warnings.js +1 -1
  57. package/dist/packages/core/dies.d.ts +11 -0
  58. package/dist/packages/core/dies.js +1 -1
  59. package/dist/packages/core/utils.d.ts +11 -0
  60. package/dist/packages/core/utils.js +1 -1
  61. package/dist/packages/renderer/binColors.d.ts +80 -0
  62. package/dist/packages/renderer/binColors.js +1 -0
  63. package/dist/packages/renderer/buildView.d.ts +100 -6
  64. package/dist/packages/renderer/buildView.js +1 -1
  65. package/dist/packages/renderer/buildWaferMap.d.ts +112 -7
  66. package/dist/packages/renderer/buildWaferMap.js +1 -1
  67. package/dist/packages/renderer/colorMap.d.ts +16 -15
  68. package/dist/packages/renderer/colorMap.js +1 -1
  69. package/dist/packages/renderer/colorSchemes.d.ts +45 -26
  70. package/dist/packages/renderer/colorSchemes.js +1 -1
  71. package/dist/packages/renderer/index.d.ts +3 -1
  72. package/dist/packages/renderer/index.js +1 -1
  73. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  74. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  75. package/dist/packages/stats/binPareto.d.ts +15 -0
  76. package/dist/packages/stats/binPareto.js +1 -1
  77. package/dist/packages/stats/clusterDetection.js +1 -1
  78. package/dist/packages/stats/connectedComponents.d.ts +14 -0
  79. package/dist/packages/stats/connectedComponents.js +1 -0
  80. package/dist/packages/stats/correlation.d.ts +26 -0
  81. package/dist/packages/stats/correlation.js +1 -1
  82. package/dist/packages/stats/facets.js +1 -1
  83. package/dist/packages/stats/filterFindings.d.ts +18 -0
  84. package/dist/packages/stats/filterFindings.js +1 -1
  85. package/dist/packages/stats/findingsNarrative.js +1 -1
  86. package/dist/packages/stats/histogram.d.ts +47 -2
  87. package/dist/packages/stats/histogram.js +1 -1
  88. package/dist/packages/stats/index.d.ts +6 -0
  89. package/dist/packages/stats/index.js +1 -1
  90. package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
  91. package/dist/packages/stats/mergeTestDefs.js +1 -0
  92. package/dist/packages/stats/metadataColumns.d.ts +20 -4
  93. package/dist/packages/stats/metadataColumns.js +1 -1
  94. package/dist/packages/stats/patternClassification.js +1 -1
  95. package/dist/packages/stats/population.d.ts +23 -0
  96. package/dist/packages/stats/population.js +1 -0
  97. package/dist/packages/stats/renderFindingsReport.js +15 -15
  98. package/dist/packages/stats/renderSummaryReport.js +35 -32
  99. package/dist/packages/stats/reportHtml.js +38 -13
  100. package/dist/packages/stats/testPassRate.d.ts +85 -0
  101. package/dist/packages/stats/testPassRate.js +1 -0
  102. package/dist/packages/stats/trend.d.ts +40 -0
  103. package/dist/packages/stats/trend.js +1 -0
  104. package/dist/packages/stats/types.d.ts +117 -6
  105. package/dist/packages/stats/yield.js +1 -1
  106. package/llms.txt +6 -0
  107. package/package.json +9 -5
  108. package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
  109. package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
@@ -1,39 +1,58 @@
1
- export interface ColorScheme {
2
- /** Human-readable display name */
1
+ export interface ValueColorScheme {
2
+ /** Human-readable display name. */
3
3
  label: string;
4
4
  /**
5
- * Return a CSS colour string for a categorical bin number.
6
- * Index 0 conventionally means "no data / unknown".
7
- */
8
- forBin: (bin: number) => string;
9
- /**
10
- * Return a CSS colour string for a continuous value t ∈ [0, 1].
11
- * Values are pre-normalized by buildView before this is called.
5
+ * Return a CSS colour string for a continuous value t ∈ [0, 1]. Values are
6
+ * pre-normalized by buildView before this is called.
12
7
  */
13
8
  forValue: (t: number) => string;
14
9
  }
15
10
  /**
16
- * Register a named colour scheme, making it available to buildView via the
17
- * colorScheme option. Call this once at app startup before rendering.
11
+ * Register a named value gradient, making it selectable via the
12
+ * `valueColorScheme` option and listed in the toolbar for value and stacked
13
+ * modes. Call once at startup, before rendering.
18
14
  *
19
- * `forBin` and `forValue` must return valid CSS color strings — invalid values
20
- * produce silent rendering artifacts (blank or black rectangles).
21
- *
22
- * @example
23
- * registerColorScheme('my-brand', {
24
- * label: 'My Brand',
25
- * forBin: (bin) => MY_BRAND_BINS[bin] ?? '#ccc',
26
- * forValue: (t) => `hsl(${200 + t * 60}, 70%, ${30 + t * 40}%)`,
27
- * });
15
+ * `forValue` must return a valid CSS colour — an invalid string renders as a
16
+ * silent blank or black die.
28
17
  */
29
- export declare function registerColorScheme(name: string, scheme: ColorScheme): void;
18
+ export declare function registerValueColorScheme(name: string, scheme: ValueColorScheme): void;
19
+ /** Retrieve a value gradient by name. Falls back to `'default'`, never undefined. */
20
+ export declare function getValueColorScheme(name?: string): ValueColorScheme;
21
+ /** All registered value gradients as `{ name, label }`, in registration order. */
22
+ export declare function listValueColorSchemes(): Array<{
23
+ name: string;
24
+ label: string;
25
+ }>;
26
+ export interface BinColorScheme {
27
+ /** Human-readable display name. */
28
+ label: string;
29
+ /**
30
+ * Colours for passing bins, handed out most-populous bin first. Convention is
31
+ * greens: an engineer reads green as pass before reading the legend.
32
+ */
33
+ pass: readonly string[];
34
+ /**
35
+ * Colours for failing bins, handed out most-populous bin first, so put the
36
+ * most distinct colours at the front. Must not resemble the pass colours —
37
+ * a fail bin that reads as a pass is the most expensive misreading a wafer
38
+ * map can produce.
39
+ */
40
+ fail: readonly string[];
41
+ }
30
42
  /**
31
- * Retrieve a registered scheme by name. Falls back to 'default' if the name
32
- * is not found, so callers never receive undefined.
43
+ * Register a named bin palette, making it selectable via the `binColorScheme`
44
+ * option and listed in the toolbar for hard/soft-bin modes. Call once at
45
+ * startup, before rendering.
46
+ *
47
+ * Throws on an empty `pass` or `fail` list: there would be nothing to colour
48
+ * that category with, and the failure is better at registration than as
49
+ * uncoloured dies later.
33
50
  */
34
- export declare function getColorScheme(name?: string): ColorScheme;
35
- /** Return all registered schemes as { name, label } pairs, in insertion order. */
36
- export declare function listColorSchemes(): Array<{
51
+ export declare function registerBinColorScheme(name: string, scheme: BinColorScheme): void;
52
+ /** Retrieve a bin palette by name. Falls back to `'default'`, never undefined. */
53
+ export declare function getBinColorScheme(name?: string): BinColorScheme;
54
+ /** All registered bin palettes as `{ name, label }`, in registration order. */
55
+ export declare function listBinColorSchemes(): Array<{
37
56
  name: string;
38
57
  label: string;
39
58
  }>;
@@ -1 +1 @@
1
- import{HARD_BIN_GREY as t,VIRIDIS as l,lerpKp as a,valueToGreyscale as i,hardBinColor as s,wangHash as u}from"./colorMap.js";const b=new Map;export function registerColorScheme(e,f){b.set(e,f)}export function getColorScheme(e){return b.get(e??"default")??b.get("default")}export function listColorSchemes(){return[...b.entries()].map(([e,f])=>({name:e,label:f.label}))}function c(e,f){const r=e.length-1;return n=>e[u(n^f)%r+1]}const d=[[0,0,255],[0,255,255],[255,255,0],[255,0,0]];registerColorScheme("default",{label:"Default",forBin:s,forValue:e=>a(d,e)}),registerColorScheme("viridis",{label:"Viridis",forBin:e=>a(l,Math.min(e,16)/16),forValue:e=>a(l,1-e)}),registerColorScheme("greyscale",{label:"Greyscale",forBin:c(t,2654435769),forValue:e=>i(1-e)});const B=["#aaaaaa","#51b3ec","#5199ec","#517fec","#5165ec","#5b51ec","#7a51ec","#1891d8","#1871d8","#1851d8","#1832d8","#2518d8","#4b18d8","#116697","#115097","#113997","#112397","#1a1197","#351197","#f39349","#f3a749","#f3bb49","#f3d149","#f3eb49","#e16a0e","#e1820e","#e19b0e","#e1b70e","#e1d70e","#9e4a0a","#9e5c0a","#9e6d0a","#9e810a","#9e970a","#54e8b7","#54e8ca","#54e8de","#54dee8","#54cae8","#1dd396","#1dd3af","#1dd3c7","#1dc7d3","#1dafd3","#149469","#14947b","#14948c","#148c94","#147b94","#bb5ce0","#d35ce0","#e05cd5","#e05cbf","#e05cab","#9b26c9","#b926c9","#c926bc","#c926a1","#c92688","#6d1b8d","#821b8d","#8d1b84","#8d1b71","#8d1b60"],I=[[0,32,77],[54,68,130],[107,107,145],[180,154,108],[253,228,32]];registerColorScheme("accessible",{label:"Accessible (CVD-safe / Cividis)",forBin:c(B,2654435769),forValue:e=>a(I,1-e)});const A=["#888888","#0d0887","#5302a3","#8b0aa5","#b83289","#db5c68","#f48849","#febc2a","#f0f921","#2c0594","#6a00a8","#a62098","#d0456d","#ec7958","#fad44c"],S=[[13,8,135],[126,3,168],[204,71,120],[248,149,64],[240,249,33]];registerColorScheme("plasma",{label:"Plasma",forBin:c(A,2654435769),forValue:e=>a(S,1-e)});const T=["#aaaaaa","#000004","#1b0c41","#4a0c6b","#781c6d","#a52c60","#cf4446","#ed6925","#fb9b06","#f7d13d","#fcffa4","#2e0a47","#8e1e6e","#c8424b","#f5a623"],V=[[0,0,4],[87,16,110],[188,55,84],[249,142,9],[252,255,164]];registerColorScheme("inferno",{label:"Inferno",forBin:c(T,2654435769),forValue:e=>a(V,1-e)});const _=["#aaaaaa","#27ae60","#e74c3c","#f39c12","#2ecc71","#c0392b","#f1c40f","#1e8449","#e67e22","#a9cce3","#922b21","#82e0aa","#d4e6f1","#fdebd0","#d5f5e3"],m=[[46,204,113],[241,196,15],[231,76,60]];registerColorScheme("traffic",{label:"Traffic (Green\u2013Yellow\u2013Red)",forBin:c(_,2654435769),forValue:e=>a(m,e)});const C=["#aaaaaa","#2980b9","#e74c3c","#1abc9c","#f39c12","#8e44ad","#e67e22","#3498db","#c0392b","#16a085","#f1c40f","#154360","#922b21","#d6eaf8","#fdedec"];registerColorScheme("thermal",{label:"Thermal (Blue\u2013Cyan\u2013Yellow\u2013Red)",forBin:c(C,2654435769),forValue:e=>a(d,e)});const o=[[0,0,128],[0,0,255],[0,255,255],[255,255,0],[255,0,0],[128,0,0]];registerColorScheme("jet",{label:"Jet (MATLAB rainbow)",forBin:e=>a(o,Math.min(e,16)/16),forValue:e=>a(o,e)});
1
+ import{VIRIDIS as o,lerpKp as a,valueToGreyscale as i}from"./colorMap.js";const r=new Map;export function registerValueColorScheme(e,l){r.set(e,l)}export function getValueColorScheme(e){return r.get(e??"default")??r.get("default")}export function listValueColorSchemes(){return[...r.entries()].map(([e,l])=>({name:e,label:l.label}))}const t=new Map;export function registerBinColorScheme(e,l){if(!l.pass.length||!l.fail.length)throw new Error(`registerBinColorScheme('${e}'): 'pass' and 'fail' must each list at least one colour`);t.set(e,l)}export function getBinColorScheme(e){return t.get(e??"default")??t.get("default")}export function listBinColorSchemes(){return[...t.entries()].map(([e,l])=>({name:e,label:l.label}))}const n=[[0,0,255],[0,255,255],[255,255,0],[255,0,0]];registerValueColorScheme("default",{label:"Default (Blue\u2013Cyan\u2013Yellow\u2013Red)",forValue:e=>a(n,e)}),registerValueColorScheme("viridis",{label:"Viridis",forValue:e=>a(o,1-e)});const f=[[0,32,77],[54,68,130],[107,107,145],[180,154,108],[253,228,32]];registerValueColorScheme("cividis",{label:"Cividis (colour-blind safe)",forValue:e=>a(f,1-e)}),registerValueColorScheme("greyscale",{label:"Greyscale",forValue:e=>i(1-e)});const s=[[13,8,135],[126,3,168],[204,71,120],[248,149,64],[240,249,33]];registerValueColorScheme("plasma",{label:"Plasma",forValue:e=>a(s,1-e)});const c=[[0,0,4],[87,16,110],[188,55,84],[249,142,9],[252,255,164]];registerValueColorScheme("inferno",{label:"Inferno",forValue:e=>a(c,1-e)});const u=[[46,204,113],[241,196,15],[231,76,60]];registerValueColorScheme("traffic",{label:"Traffic (Green\u2013Yellow\u2013Red)",forValue:e=>a(u,e)});const b=[[0,0,128],[0,0,255],[0,255,255],[255,255,0],[255,0,0],[128,0,0]];registerValueColorScheme("jet",{label:"Jet (MATLAB rainbow)",forValue:e=>a(b,e)}),registerBinColorScheme("default",{label:"Default",pass:["#2ca02c","#98df8a","#1b5e20"],fail:["#d62728","#332288","#e69f00","#000000","#0072b2","#654522","#ff9d9a","#aa3377","#499894","#e377c2","#f0e442","#225555","#79706e","#33bbee","#ee7733","#999933","#9467bd","#663333","#cc6677"]}),registerBinColorScheme("accessible",{label:"Colour-blind safe",pass:["#009e73","#44bb99"],fail:["#d55e00","#000000","#604e97","#882d17","#f3c300","#222255","#225555","#a1caf1","#9467bd","#8c564b","#b07aa1","#eedd88","#f38400","#663333"]});
@@ -2,6 +2,8 @@ export { buildView, getDieKey, buildHoverText, buildMapTitle, } from './buildVie
2
2
  export type { View, MapTitleParts, PlotMode, ViewRect, ViewText, ViewHoverPoint, ViewOverlay, ViewOptions, } from './buildView.js';
3
3
  export * from './buildWaferMap.js';
4
4
  export type { WaferMetadata, DieMetadata } from '../core/metadata.js';
5
- export { hardBinColor, hardBinGreyscale, valueToViridis, valueToGreyscale, softBinColor, contrastTextColor, } from './colorMap.js';
5
+ export { valueToViridis, valueToGreyscale, contrastTextColor, } from './colorMap.js';
6
6
  export * from './colorSchemes.js';
7
+ export { resolveBinColors } from './binColors.js';
8
+ export type { BinColors, BinColorOptions } from './binColors.js';
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- export{buildView,getDieKey,buildHoverText,buildMapTitle}from"./buildView.js";export*from"./buildWaferMap.js";export{hardBinColor,hardBinGreyscale,valueToViridis,valueToGreyscale,softBinColor,contrastTextColor}from"./colorMap.js";export*from"./colorSchemes.js";
1
+ export{buildView,getDieKey,buildHoverText,buildMapTitle}from"./buildView.js";export*from"./buildWaferMap.js";export{valueToViridis,valueToGreyscale,contrastTextColor}from"./colorMap.js";export*from"./colorSchemes.js";export{resolveBinColors}from"./binColors.js";
@@ -1 +1 @@
1
- import{analyzeWaferMap as h}from"./analyzeWaferMap.js";import{median as y}from"../core/utils.js";const g=1.3,b=2;function p(f,r){const s={info:0,notable:1,unusual:2};return s[f]>=s[r]?f:r}function w(f){const r=new Map;for(const o of f){const n=new Set,c=new Set(o.summary.findings.flatMap(e=>e.absorbedIds??[]));for(const e of o.summary.findings){if(c.has(e.id))continue;const a=[e.variable.kind,e.variable.index??"",e.variable.bin??"",e.comparison.family,e.comparison.left,e.effect.direction].join("|");if(n.has(a))continue;n.add(a);const l=r.get(a)??{finding:e,waferIndices:[],severity:e.severity};l.waferIndices.push(o.waferIndex),l.severity=p(l.severity,e.severity),r.set(a,l)}}const s=[];for(const[o,n]of r){if(n.waferIndices.length<b)continue;const c=n.waferIndices.length/Math.max(1,f.length),e=c>=.6||n.severity==="unusual"?"unusual":"notable",a=n.finding;s.push({id:`lot-repeat:${o}`,level:"lot",severity:e,variable:{...a.variable},comparison:{...a.comparison},effect:{direction:a.effect.direction,absoluteDelta:a.effect.absoluteDelta,relativeDelta:a.effect.relativeDelta,effectSize:a.effect.effectSize},stats:{method:"wafer-finding-frequency",sampleSizeLeft:n.waferIndices.length,sampleSizeRight:f.length-n.waferIndices.length},summary:`${a.summary} \u2014 seen on ${n.waferIndices.length}/${f.length} wafers (${Math.round(c*100)}%)`,highlight:{kind:"wafer",waferIndices:n.waferIndices}})}return s}function v(f){const r=f.map(e=>({waferIndex:e.waferIndex,yieldPercent:e.summary.stats.yieldPercent})).filter(e=>e.yieldPercent!==null);if(r.length<3)return[];const s=r.map(e=>e.yieldPercent),o=y(s),n=y(s.map(e=>Math.abs(e-o)));if(!Number.isFinite(n)||n===0)return[];const c=[];for(const e of r){const a=.6745*(e.yieldPercent-o)/n;if(Math.abs(a)<g)continue;const l=e.yieldPercent-o;c.push({id:`inter-wafer:yield:${e.waferIndex}`,level:"inter-wafer",severity:Math.abs(a)>=2?"unusual":"notable",variable:{kind:"yield",label:"Yield"},comparison:{family:"wafer",left:`Wafer ${e.waferIndex+1}`,right:"Lot median"},effect:{direction:l>0?"higher":"lower",absoluteDelta:l,relativeDelta:o===0?void 0:l/o,effectSize:a},stats:{method:"mad-z-score",sampleSizeLeft:1,sampleSizeRight:r.length-1},summary:`Wafer ${e.waferIndex+1} yield is ${Math.abs(l).toFixed(1)} percentage points ${l>0?"higher":"lower"} than the lot median`,highlight:{kind:"wafer",waferIndices:[e.waferIndex]}})}return c}export function analyzeWaferLot(f,r={}){const s=f.map((t,i)=>({waferIndex:i,summary:r.perWaferSummaries?.[i]??h(t,r)})),o=[...w(s),...v(s)].sort((t,i)=>{const d=t.severity==="unusual"?2:t.severity==="notable"?1:0,m=i.severity==="unusual"?2:i.severity==="notable"?1:0;return d!==m?m-d:t.summary.localeCompare(i.summary,"en")}),n=s.map(t=>t.summary.wafer).filter(t=>!!t),c={},e=[];if(n.length>0){const t=new Set;for(const i of n)for(const d of Object.keys(i))t.add(d);t.delete("wafer"),t.delete("waferId");for(const i of t){const d=n.filter(u=>i in u).map(u=>u[i]);d.every(u=>u===d[0])?c[i]=d[0]:e.push(i)}}const a=s.map(({waferIndex:t,summary:i})=>({waferIndex:t,yieldPercent:i.stats.yieldPercent})),l=s.map(({waferIndex:t,summary:i})=>i.stats.perTestStats?.length?{waferIndex:t,tests:i.stats.perTestStats}:null).filter(t=>t!==null);return{level:"lot",hasNotableFindings:o.some(t=>t.severity!=="info")||s.some(t=>t.summary.hasNotableFindings),findings:o,lot:Object.keys(c).length>0?c:void 0,...e.length>0?{mixedIdentityFields:e}:{},stats:{waferCount:f.length},lotYieldSeries:a,perWafer:s,...l.length>0?{perWaferTestStats:l}:{}}}
1
+ import{analyzeWaferMap as h}from"./analyzeWaferMap.js";import{median as y}from"../core/utils.js";import{describeWaferPopulation as p,populationStat as g}from"./population.js";const b=1.3,w=2;function v(f,c){const i={info:0,notable:1,unusual:2};return i[f]>=i[c]?f:c}function I(f){const c=new Map;for(const l of f){const s=new Set,o=new Set(l.summary.findings.flatMap(a=>a.absorbedIds??[]));for(const a of l.summary.findings){if(o.has(a.id))continue;const r=[a.variable.kind,a.variable.index??"",a.variable.bin??"",a.comparison.family,a.comparison.left,a.effect.direction].join("|");if(s.has(r))continue;s.add(r);const n=c.get(r)??{finding:a,waferIndices:[],severity:a.severity};n.waferIndices.push(l.waferIndex),n.severity=v(n.severity,a.severity),c.set(r,n)}}const i=[];for(const[l,s]of c){if(s.waferIndices.length<w)continue;const o=s.waferIndices.length/Math.max(1,f.length),a=o>=.6||s.severity==="unusual"?"unusual":"notable",r=s.finding;i.push({id:`lot-repeat:${l}`,level:"lot",severity:a,variable:{...r.variable},comparison:{...r.comparison},effect:{direction:r.effect.direction,absoluteDelta:r.effect.absoluteDelta,relativeDelta:r.effect.relativeDelta,effectSize:r.effect.effectSize},stats:{method:"wafer-finding-frequency",sampleSizeLeft:s.waferIndices.length,sampleSizeRight:f.length-s.waferIndices.length},summary:`${r.summary} \u2014 seen on ${s.waferIndices.length}/${f.length} wafers (${Math.round(o*100)}%)`,highlight:{kind:"wafer",waferIndices:s.waferIndices}})}return i}function S(f,c){const i=g(c,"median"),l=f.map(n=>({waferIndex:n.waferIndex,yieldPercent:n.summary.stats.yieldPercent})).filter(n=>n.yieldPercent!==null);if(l.length<3)return[];const s=l.map(n=>n.yieldPercent),o=y(s),a=y(s.map(n=>Math.abs(n-o)));if(!Number.isFinite(a)||a===0)return[];const r=[];for(const n of l){const e=.6745*(n.yieldPercent-o)/a;if(Math.abs(e)<b)continue;const t=n.yieldPercent-o;r.push({id:`inter-wafer:yield:${n.waferIndex}`,level:"inter-wafer",severity:Math.abs(e)>=2?"unusual":"notable",variable:{kind:"yield",label:"Yield"},comparison:{family:"wafer",left:`Wafer ${n.waferIndex+1}`,right:i.charAt(0).toUpperCase()+i.slice(1)},effect:{direction:t>0?"higher":"lower",absoluteDelta:t,relativeDelta:o===0?void 0:t/o,effectSize:e},stats:{method:"mad-z-score",sampleSizeLeft:1,sampleSizeRight:l.length-1},summary:`Wafer ${n.waferIndex+1} yield is ${Math.abs(t).toFixed(1)} percentage points ${t>0?"higher":"lower"} than the ${i}`,highlight:{kind:"wafer",waferIndices:[n.waferIndex]}})}return r}export function analyzeWaferLot(f,c={}){const i=f.map((e,t)=>({waferIndex:t,summary:c.perWaferSummaries?.[t]??h(e,c)})),l=[...I(i),...S(i,p(i.map(e=>e.summary.wafer)))].sort((e,t)=>{const d=e.severity==="unusual"?2:e.severity==="notable"?1:0,m=t.severity==="unusual"?2:t.severity==="notable"?1:0;return d!==m?m-d:e.summary.localeCompare(t.summary,"en")}),s=i.map(e=>e.summary.wafer).filter(e=>!!e),o={},a=[];if(s.length>0){const e=new Set;for(const t of s)for(const d of Object.keys(t))e.add(d);e.delete("wafer"),e.delete("waferId");for(const t of e){const d=s.filter(u=>t in u).map(u=>u[t]);d.every(u=>u===d[0])?o[t]=d[0]:a.push(t)}}const r=i.map(({waferIndex:e,summary:t})=>({waferIndex:e,yieldPercent:t.stats.yieldPercent})),n=i.map(({waferIndex:e,summary:t})=>t.stats.perTestStats?.length?{waferIndex:e,tests:t.stats.perTestStats}:null).filter(e=>e!==null);return{level:"lot",hasNotableFindings:l.some(e=>e.severity!=="info")||i.some(e=>e.summary.hasNotableFindings),findings:l,lot:Object.keys(o).length>0?o:void 0,...a.length>0?{mixedIdentityFields:a}:{},stats:{waferCount:f.length},lotYieldSeries:r,perWafer:i,...n.length>0?{perWaferTestStats:n}:{}}}
@@ -1 +1 @@
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
+ import{isYieldEligibleDie as ee,getDieKey as x,isPositionedDie as de}from"../core/dies.js";import{buildWaferMap as Re,getTestPassStatus as me,isParametricTest as j}from"../renderer/buildWaferMap.js";import{buildQuadrantRegions as Be,buildReticlePositionRegions as Ce,buildRingRegions as Fe,buildSectorRegions as Ne,buildTestSiteRegions as Pe,sectorCompassNames as ge,areQuadrantsAdjacent as Ve,parseRegionKey as O}from"./regions.js";import{buildClusterFindings as Le}from"./clusterDetection.js";import{classifyPattern as Ee}from"./patternClassification.js";import{normalCdf as he}from"./math.js";import{quantile as te}from"./math.js";import{mean as X,clamp01 as be}from"../core/utils.js";const Ae={significanceLevel:{min:1e-6,max:1},minimumEffectSize:{min:0,max:1},minimumRelativeEffect:{min:0,max:Number.MAX_SAFE_INTEGER},ringCount:{min:1,max:Number.MAX_SAFE_INTEGER}},pe=[4,8,16,32];function Ke(i){const a=Object.fromEntries(Object.entries(i).filter(([,f])=>f!==void 0)),s={..._,...a},o=[],e=[];for(const f of["significanceLevel","minimumEffectSize","minimumRelativeEffect","ringCount"]){const{min:l,max:r}=Ae[f],n=s[f];if(typeof n!="number"||!Number.isFinite(n)){e.push(`${f}=${String(n)} is not a finite number (using ${_[f]})`),s[f]=_[f];continue}const c=Math.min(r,Math.max(l,n));c!==n&&(e.push(`${f}=${n} is outside ${l}\u2013${r} (using ${c})`),s[f]=c)}if(!Number.isInteger(s.ringCount)){const f=Math.max(1,Math.round(s.ringCount));e.push(`ringCount=${s.ringCount} is not a whole number (using ${f})`),s.ringCount=f}if(pe.includes(s.sectorCount)||(e.push(`sectorCount=${s.sectorCount} is not one of ${pe.join(", ")} (using ${_.sectorCount})`),s.sectorCount=_.sectorCount),e.length){const f=`analyzeWaferMap: ${e.join("; ")}. The analysis ran with the corrected values.`;console.warn(`[wafermap] ${f}`),o.push({code:"analysis-option-corrected",message:f,severity:"warning"})}return{resolved:s,warnings:o}}const _={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 xe(i,a){const s={...i};return s.minimumClusterSize=Math.max(5,Math.round(a*.003)),s}function Te(i){return"wafer"in i&&"dies"in i&&"view"in i?i:Re(i)}function je(i,a){return ee(i,a)?i.hbin!==void 0||i.sbin!==void 0||i.testValues!==void 0&&Object.keys(i.testValues).length>0:!1}function Oe(i,a,s){if(!i||a)return;const o=(s??[]).filter(e=>j(e)&&(e.limitLow!==void 0||e.limitHigh!==void 0));if(o.length!==0)return e=>{for(const f of o){const l=f.testNumber,r=e.testValues?.[l];if(r!==void 0&&(f.limitLow!==void 0&&r<f.limitLow||f.limitHigh!==void 0&&r>f.limitHigh))return!0}return!1}}function He(i,a,s){const o=new Set,e=new Set,f=new Set,l=new Map,r=new Map;for(const n of i){if(n.testValues)for(const c of Object.keys(n.testValues))o.add(Number(c));if(n.testPass)for(const c of Object.keys(n.testPass))o.add(Number(c));n.hbin!==void 0&&e.add(n.hbin),n.sbin!==void 0&&f.add(n.sbin),ee(n)&&(n.hbin!==void 0&&l.set(n.hbin,(l.get(n.hbin)??0)+1),n.sbin!==void 0&&r.set(n.sbin,(r.get(n.sbin)??0)+1))}return{totalDies:i.length,analyzedDies:a,excludedDies:i.length-a,yieldPercent:s,testsConsidered:[...o].sort((n,c)=>n-c),hardBinsConsidered:[...e].sort((n,c)=>n-c),softBinsConsidered:[...f].sort((n,c)=>n-c),...l.size?{hardBinCounts:Object.fromEntries(l)}:{},...r.size?{softBinCounts:Object.fromEntries(r)}:{}}}function qe(i,a){if(!a?.length)return;const s=a.filter(e=>j(e)&&(e.limitLow!==void 0||e.limitHigh!==void 0));if(!s.length)return;const o=[];for(const e of s){const f=e.testNumber;let l=0,r=0,n=0,c=0;for(const t of i){if(t.partial||t.edgeExcluded)continue;const h=t.testValues?.[f];h!==void 0&&(c++,e.limitLow!==void 0&&h<e.limitLow?r++:e.limitHigh!==void 0&&h>e.limitHigh?n++:l++)}o.push({testNumber:f,label:e.name,passDies:l,failLowDies:r,failHighDies:n,totalDies:c,yieldPercent:c>0?l/c*100:null})}return o.length?o:void 0}export function computeFunctionalYield(i,a){if(!a?.length)return;const s=a.filter(e=>!j(e));if(!s.length)return;const o=[];for(const e of s){const f=e.testNumber;let l=0,r=0;for(const c of i){if(c.partial||c.edgeExcluded)continue;const t=me(c,f,e);t!==void 0&&(t?l++:r++)}const n=l+r;o.push({testNumber:f,label:e.name,passDies:l,failDies:r,totalDies:n,passRatePercent:n>0?l/n*100:null})}return o.length?o:void 0}function Ie(i,a,s,o){const e=[];for(const f of a){const l=[];for(const h of i){if(h.partial||h.edgeExcluded)continue;const m=h.testValues?.[f];m!==void 0&&l.push(m)}if(l.length<o)continue;const r=X(l),n=Math.sqrt(le(l,r)),c=l.slice().sort((h,m)=>h-m),t=s?.find(h=>h.testNumber===f)?.name??String(f);e.push({testNumber:f,label:t,count:l.length,min:c[0],max:c[c.length-1],mean:r,stddev:n,median:te(c,.5),q1:te(c,.25),q3:te(c,.75)})}return e.length?e:void 0}function H(i,a,s,o){const e=(i+s)/(a+o),f=e*(1-e)*(1/a+1/o);if(!Number.isFinite(f)||f<=0)return 1;const l=(i/a-s/o)/Math.sqrt(f);return be(2*(1-he(Math.abs(l))))}function ne(i){const a=new Map;for(const s of i){const o=`${s.variable.kind}:${s.comparison.family}`,e=a.get(o)??[];e.push(s),a.set(o,e)}for(const s of a.values()){const o=[...s].filter(l=>l.stats.pValue!==void 0).sort((l,r)=>(l.stats.pValue??1)-(r.stats.pValue??1)),e=o.length;let f=1;for(let l=e-1;l>=0;l--){const r=o[l],n=r.stats.pValue??1,c=Math.min(1,Math.min(f,n*e/(l+1)));r.stats.adjustedPValue=c,f=c}}return i}function ie(i,a,s){const o=Math.abs(a),e=s!==void 0?Math.abs(s):0;return i<=.01&&(o>=.3||e>=2.5)?"unusual":i<=.05&&(o>=.2||e>=1.5)?"notable":"info"}function se(i,a){return i<=.01&&Math.abs(a)>=.5?"unusual":i<=.05&&Math.abs(a)>=.15?"notable":"info"}function J(i){return i==="reticle-position"?"other reticle positions":i==="test-site"?"other test sites":"the rest of the map"}function q(i){return i==="reticle-position"?"Other reticle positions":i==="test-site"?"Other test sites":"Rest of map"}function ye(i,a,s){const o=(Math.abs(a)*100).toFixed(1);return`${i} yield is ${o} percentage points ${a>0?"higher":"lower"} than ${J(s)}`}function oe(i,a){return a==="quadrant"&&!i.startsWith("Quadrant")?`quadrant ${i}`:i}function Se(i,a,s,o){const e=oe(i,o),f=(Math.abs(s)*100).toFixed(1);return`${e} has ${a} occurrence ${f} percentage points ${s>0?"higher":"lower"} than ${J(o)}`}function _e(i,a,s,o){const e=oe(i,o),f=(Math.abs(s)*100).toFixed(1);return`${e} has ${a} pass rate ${f} percentage points ${s>0?"higher":"lower"} than ${J(o)}`}function we(i,a,s,o,e,f){const l=oe(i,e),r=J(e),n=s>0?"higher":"lower";if(o!==void 0&&Number.isFinite(o)){const t=(Math.abs(o)*100).toFixed(1);return`${l} mean ${a} is ${t}% ${n} than ${r}`}const c=f?` ${f}`:"";return`${l} mean ${a} is ${Math.abs(s).toPrecision(3)}${c} ${n} than ${r}`}function $e(i,a,s){const o=a?.find(e=>e.bin===i);return o?.name?`${s} ${i} (${o.name})`:`${s} ${i}`}function Ye(i,a){const s=a?.find(o=>o.testNumber===i);return{label:s?.name??`Test ${i}`,unit:s?.unit}}function ae(i,a){const s=new Map(i.map(e=>[x(e),e])),o=new Map;for(const e of a){const f=[];for(const l of e.dieKeys){const r=s.get(l);r&&f.push(r)}o.set(e.key,f)}return o}function re(i,a){return ne(i),i.filter(s=>{const o=s.stats.adjustedPValue??s.stats.pValue??1,e=Math.abs(s.effect.absoluteDelta??0),f=Math.abs(s.effect.relativeDelta??0);return o<=a.significanceLevel&&(e>=a.minimumEffectSize||f>=a.minimumRelativeEffect)}).map(s=>({...s,severity:ie(s.stats.adjustedPValue??s.stats.pValue??1,s.effect.absoluteDelta??0,s.effect.relativeDelta)}))}function Y(i,a,s,o){const e=new Set(s),f=ae(i,a),l=new Map,r=new Map;for(const[c,t]of f){let h=0,m=0;for(const d of t)d.hbin!==void 0&&(m++,e.has(d.hbin)&&h++);l.set(c,h),r.set(c,m)}const n=[];for(const c of a){const t=r.get(c.key)??0,h=l.get(c.key)??0;let m=0,d=0;for(const[y,b]of r)y!==c.key&&(m+=b,d+=l.get(y)??0);if(t<o.minimumSampleSize||m<o.minimumSampleSize)continue;const g=h/t,p=d/m,S=g-p,D=H(h,t,d,m);n.push({id:`yield:${c.key}`,level:"wafer",severity:"info",variable:{kind:"yield",label:"Yield"},comparison:{family:c.family,left:c.label,right:q(c.family)},effect:{direction:S===0?"different":S>0?"higher":"lower",absoluteDelta:S,relativeDelta:p===0?void 0:S/p,effectSize:S},stats:{method:"two-proportion-z",pValue:D,sampleSizeLeft:t,sampleSizeRight:m},summary:ye(c.label,S,c.family),highlight:{kind:"region",regionFamily:c.family,regionKeys:[c.key],dieKeys:[...c.dieKeys]}})}return re(n,o)}function V(i,a,s,o,e,f){const l=d=>s==="soft"?d.sbin:d.hbin,r=[...new Set(i.map(l).filter(d=>d!==void 0))].sort((d,g)=>d-g),n=ae(i,a),c=new Map,t=new Map;for(const[d,g]of n){const p=new Map;for(const S of g){const D=l(S);D!==void 0&&p.set(D,(p.get(D)??0)+1)}c.set(d,p),t.set(d,g.length)}const h=[],m=e==="hardBin"?"HBin":"SBin";for(const d of a){const g=t.get(d.key)??0,p=c.get(d.key);let S=0;const D=new Map;for(const[y,b]of c)if(y!==d.key){S+=t.get(y)??0;for(const[$,k]of b)D.set($,(D.get($)??0)+k)}if(!(g<f.minimumSampleSize||S<f.minimumSampleSize))for(const y of r){const b=p.get(y)??0,$=D.get(y)??0,k=b/g,w=$/S,v=k-w,R=H(b,g,$,S),u=$e(y,o,m);h.push({id:`${e}:${y}:${d.key}`,level:"wafer",severity:"info",variable:{kind:e,bin:y,label:u},comparison:{family:d.family,left:d.label,right:q(d.family)},effect:{direction:v===0?"different":v>0?"higher":"lower",absoluteDelta:v,relativeDelta:w===0?void 0:v/w,effectSize:v},stats:{method:"two-proportion-z",pValue:R,sampleSizeLeft:g,sampleSizeRight:S},summary:Se(d.label,u,v,d.family),highlight:{kind:"bin",bin:y,regionKeys:[d.key],dieKeys:[...d.dieKeys]}})}}return re(h,f)}function W(i,a,s,o){const e=(s??[]).filter(t=>!j(t));if(!e.length)return[];const f=e.map(t=>t.testNumber),l=ae(i,a),r=new Map,n=new Map;for(const[t,h]of l){const m=new Uint32Array(e.length),d=new Uint32Array(e.length);for(const g of h)for(let p=0;p<e.length;p++){const S=me(g,f[p],e[p]);S!==void 0&&(d[p]++,S&&m[p]++)}r.set(t,m),n.set(t,d)}const c=[];for(const t of a){const h=r.get(t.key),m=n.get(t.key);for(let d=0;d<e.length;d++){let g=0,p=0;for(const[v,R]of n)v!==t.key&&(p+=R[d],g+=r.get(v)[d]);const S=m[d],D=h[d];if(S<o.minimumSampleSize||p<o.minimumSampleSize)continue;const y=D/S,b=g/p,$=y-b,k=H(D,S,g,p),w=f[d];c.push({id:`functional:${w}:${t.key}`,level:"wafer",severity:"info",variable:{kind:"functionalTest",index:w,label:`${e[d].name} pass rate`},comparison:{family:t.family,left:t.label,right:q(t.family)},effect:{direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:b===0?void 0:$/b,effectSize:$},stats:{method:"two-proportion-z",pValue:k,sampleSizeLeft:S,sampleSizeRight:p},summary:_e(t.label,e[d].name,$,t.family),highlight:{kind:"region",regionFamily:t.family,regionKeys:[t.key],dieKeys:[...t.dieKeys]}})}}return re(c,o)}function le(i,a){return i.length<2?0:i.reduce((s,o)=>s+(o-a)**2,0)/(i.length-1)}function ve(i,a,s,o,e,f){const l=Math.sqrt(s/i+f/o),r=a-e;if(!Number.isFinite(l)||l===0)return{pValue:1,effectSize:0,delta:r};const n=r/l,c=Math.sqrt(Math.max(0,(s+f)/2)),t=c===0?r:r/c;return{pValue:be(2*(1-he(Math.abs(n)))),effectSize:t,delta:r}}function We(i,a){const s=X(i),o=X(a);return ve(i.length,s,le(i,s),a.length,o,le(a,o))}const De=250;function ke(i,a,s){const o=l=>l.filter(r=>j(s?.find(n=>n.testNumber===r)));if(a)return{testNumbers:o(a.slice().sort((l,r)=>l-r))};const e=new Set;let f=!1;e:for(const l of i)for(const r of Object.keys(l.testValues??{})){const n=Number(r);if(!e.has(n)&&(e.add(n),e.size>De)){f=!0;break e}}if(f){const l=`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: ${l}`),{testNumbers:[],warning:{code:"test-count-capped",message:l,severity:"warning"}}}return{testNumbers:o([...e].sort((l,r)=>l-r))}}function Q(i,a,s,o){const e=ke(i,o.testNumbers,s);if(e.warning)return{findings:[],warning:e.warning};const f=e.testNumbers,l=new Map;for(let g=0;g<a.length;g++)for(const p of a[g].dieKeys)l.set(p,g);const r=a.length,n=[],c=[];for(const g of i){const p=l.get(x(g));p!==void 0&&(n.push(g),c.push(p))}const t=[],h=new Float64Array(r),m=new Float64Array(r),d=new Float64Array(r);for(const g of f){h.fill(0),m.fill(0),d.fill(0);let p=0,S=0,D=0,y;for(let b=0;b<n.length;b++){const k=n[b].testValues?.[g];if(k===void 0)continue;y===void 0&&(y=k);const w=k-y,v=c[b];h[v]+=1,m[v]+=w,d[v]+=w*w,p+=1,S+=w,D+=w*w}if(y!==void 0)for(let b=0;b<r;b++){const $=h[b],k=p-$;if($<o.minimumSampleSize||k<o.minimumSampleSize)continue;const w=m[b]/$,R=(S-m[b])/k,u=w+y,M=R+y,B=$>1?Math.max(0,d[b]-$*w*w)/($-1):0,z=k>1?Math.max(0,D-d[b]-k*R*R)/(k-1):0,{pValue:F,effectSize:N,delta:E}=ve($,u,B,k,M,z),P=a[b],{label:U,unit:G}=Ye(g,s),A=M!==0?E/Math.abs(M):void 0;t.push({id:`test:${g}:${P.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:g,label:U,unit:G},comparison:{family:P.family,left:P.label,right:q(P.family)},effect:{direction:E===0?"different":E>0?"higher":"lower",absoluteDelta:E,relativeDelta:A,effectSize:N},stats:{method:"welch-z-approx",pValue:F,sampleSizeLeft:$,sampleSizeRight:k},summary:we(P.label,U,E,A,P.family,G),highlight:{kind:"region",regionFamily:P.family,regionKeys:[P.key],dieKeys:[...P.dieKeys]}})}}return ne(t),{activeTestNumbers:f,findings:t.filter(g=>{const p=g.stats.adjustedPValue??g.stats.pValue??1,S=Math.abs(g.effect.effectSize??0);return p<=o.significanceLevel&&S>=o.minimumEffectSize}).map(g=>({...g,severity:se(g.stats.adjustedPValue??g.stats.pValue??1,g.effect.effectSize??0)}))}}function Qe(i,a,s,o){if(!s?.length)return[];const e=s.filter(r=>j(r)&&(r.limitLow!==void 0||r.limitHigh!==void 0));if(!e.length)return[];const f=[],l=new Map(i.map(r=>[x(r),r]));for(const r of e){const n=r.testNumber;for(const c of a){const t=new Map;for(const m of c){const d=[];for(const g of m.dieKeys){const p=l.get(g);p&&d.push(p)}t.set(m.key,d)}const h=[];for(const m of c){const d=t.get(m.key),g=[];for(const[u,M]of t)if(u!==m.key)for(const B of M)g.push(B);const p=u=>u.testValues?.[n]!==void 0,S=u=>{const M=u.testValues?.[n];return M===void 0?!1:r.limitLow!==void 0&&M<r.limitLow||r.limitHigh!==void 0&&M>r.limitHigh},D=d.filter(p),y=g.filter(p);if(D.length<o.minimumSampleSize||y.length<o.minimumSampleSize)continue;const b=D.filter(S).length,$=y.filter(S).length,k=b/D.length,w=$/y.length,v=k-w,R=H(b,D.length,$,y.length);h.push({id:`specLimit:${n}:${m.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:n,label:r.name,unit:r.unit},comparison:{family:m.family,left:m.label,right:q(m.family)},effect:{direction:v===0?"different":v>0?"higher":"lower",absoluteDelta:v,relativeDelta:w===0?void 0:v/w,effectSize:v},stats:{method:"two-proportion-z",pValue:R,sampleSizeLeft:D.length,sampleSizeRight:y.length},summary:`${m.label} spec-fail rate for ${r.name} is ${(Math.abs(v)*100).toFixed(1)} pp ${v>0?"higher":"lower"} than the rest of the wafer`,highlight:{kind:"region",regionFamily:m.family,regionKeys:[m.key],dieKeys:[...m.dieKeys]}})}ne(h),f.push(...h.filter(m=>{const d=m.stats.adjustedPValue??m.stats.pValue??1,g=Math.abs(m.effect.absoluteDelta??0),p=Math.abs(m.effect.relativeDelta??0);return d<=o.significanceLevel&&(g>=o.minimumEffectSize||p>=o.minimumRelativeEffect)}).map(m=>({...m,severity:se(m.stats.adjustedPValue??m.stats.pValue??1,m.effect.effectSize??0)})))}}return f}function T(i){return i.highlight.regionKeys??[]}function Ue(i){const a=i.variable.bin??i.variable.index??"";return`${i.comparison.family}\0${i.variable.kind}\0${a}\0${i.effect.direction}`}function I(i,a){const s=T(i)[0]??"",o=O(s);return o.family==="ring"?o.ring??0:o.family==="sector"?ge(a).indexOf(o.sector??""):0}function Ge(i,a,s){if(a==="quadrant"){const l=new Map;for(const t of i){const h=O(T(t)[0]??"").quadrant;h&&l.set(h,t)}const r=[...l.keys()],n=new Set,c=[];for(const t of r){if(n.has(t))continue;const h=[],m=[t];for(n.add(t);m.length;){const d=m.pop();h.push(d);for(const g of r)!n.has(g)&&Ve(d,g)&&(n.add(g),m.push(g))}c.push(h.map(d=>l.get(d)))}return c}const o=[...i].sort((l,r)=>I(l,s)-I(r,s)),e=[];let f=[];for(const l of o){if(f.length===0){f.push(l);continue}const r=I(f[f.length-1],s);I(l,s)-r===1?f.push(l):(e.push(f),f=[l])}if(f.length&&e.push(f),a==="sector"&&e.length>1){const r=ge(s).length,n=I(e[0][0],s),c=I(e[e.length-1][e[e.length-1].length-1],s);if(n===0&&c===r-1){const t=e.pop();e[0]=[...t,...e[0]]}}return e}function Xe(i,a){const s=i.map(l=>O(T(l)[0]??"").ring).filter(l=>l!==void 0).sort((l,r)=>l-r),o=s[0],e=s[s.length-1];let f="";return o===e&&o===1&&a>1?f=" (core)":o===e&&o===a&&a>1&&(f=" (edge)"),o===e?`Ring ${o}${f}`:`Rings ${o}\u2013${e}`}function Je(i,a){const s=i.map(o=>O(T(o)[0]??"").sector??"");return s.length===1?`Sector ${s[0]}`:`Sectors ${s[0]}\u2013${s[s.length-1]}`}function Ze(i){const a=i.map(s=>O(T(s)[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 fe(i){return[...new Set(i)]}function et(i,a){const s=i[0],o=s.comparison.family,e=s.variable.kind,f=fe(i.flatMap(b=>b.highlight.dieKeys??[])),l=fe(i.flatMap(T)),r=new Set(f),n=o==="ring"?Xe(i,a.ringCount):o==="sector"?Je(i,a.sectorCount):Ze(i),c=e==="softBin"?a.softEligibleDies:e==="test"?a.testDies:a.eligibleDies,t=c.filter(b=>r.has(x(b))),h=c.filter(b=>!r.has(x(b)));let m,d,g,p,S;if(e==="test"){const b=s.variable.index,$=z=>z.testValues?.[b],k=t.map($).filter(z=>z!==void 0),w=h.map($).filter(z=>z!==void 0),{pValue:v,effectSize:R,delta:u}=We(k,w),M=X(w),B=M!==0?u/Math.abs(M):void 0;m={direction:u===0?"different":u>0?"higher":"lower",absoluteDelta:u,relativeDelta:B,effectSize:R},d={method:"welch-z-approx",pValue:v,sampleSizeLeft:k.length,sampleSizeRight:w.length},g=se(v,R),p=we(n,s.variable.label,u,B,o,s.variable.unit),S=`test:${b}`}else if(e==="yield"){const b=new Set(a.passBins),$=t.filter(N=>N.hbin!==void 0),k=h.filter(N=>N.hbin!==void 0),w=$.length,v=k.length,R=$.filter(N=>b.has(N.hbin)).length,u=k.filter(N=>b.has(N.hbin)).length,M=w>0?R/w:0,B=v>0?u/v:0,z=M-B,F=H(R,w,u,v);m={direction:z===0?"different":z>0?"higher":"lower",absoluteDelta:z,relativeDelta:B===0?void 0:z/B,effectSize:z},d={method:"two-proportion-z",pValue:F,sampleSizeLeft:w,sampleSizeRight:v},g=ie(F,z,m.relativeDelta),p=ye(n,z,o),S="yield"}else{const b=s.variable.bin,$=z=>e==="softBin"?z.sbin:z.hbin,k=t.filter(z=>$(z)===b).length,w=h.filter(z=>$(z)===b).length,v=k/t.length,R=w/h.length,u=v-R,M=H(k,t.length,w,h.length);m={direction:u===0?"different":u>0?"higher":"lower",absoluteDelta:u,relativeDelta:R===0?void 0:u/R,effectSize:u},d={method:"two-proportion-z",pValue:M,sampleSizeLeft:t.length,sampleSizeRight:h.length},g=ie(M,u,m.relativeDelta);const B=e==="softBin"?a.sbinDefs:a.hbinDefs;p=Se(n,$e(b,B,e==="softBin"?"SBin":"HBin"),u,o),S=`${e}:${b}`}const D=fe(l.map(b=>b.replace(`${o}:`,""))).join("-"),y=e==="hardBin"||e==="softBin"?{kind:"bin",bin:s.variable.bin,regionKeys:l,dieKeys:f}:{kind:"region",regionFamily:o,regionKeys:l,dieKeys:f};return{...s,id:`${S}:${o}:${D}`,severity:g,comparison:{family:o,left:n,right:q(o)},effect:m,stats:d,summary:p,highlight:y,relatedIds:i.map(b=>b.id)}}function tt(i,a){const s=new Set(["ring","quadrant","sector"]),o=[],e=new Map;for(const l of i){if(!s.has(l.comparison.family)){o.push(l);continue}const r=Ue(l),n=e.get(r)??[];n.push(l),e.set(r,n)}const f=[];for(const l of e.values()){if(l.length===1){f.push(l[0]);continue}const r=l[0].comparison.family;for(const n of Ge(l,r,a.sectorCount))f.push(n.length===1?n[0]:et(n,a))}return[...o,...f]}function nt(i){const a=new Map,s=new Map;for(const e of i){const f=x(e);e.hbin!==void 0&&e.hbin!==null&&(a.get(e.hbin)??a.set(e.hbin,new Set).get(e.hbin)).add(f),e.sbin!==void 0&&e.sbin!==null&&(s.get(e.sbin)??s.set(e.sbin,new Set).get(e.sbin)).add(f)}const o=new Set;for(const[e,f]of a)for(const[l,r]of s){if(f.size!==r.size)continue;let n=!0;for(const c of f)if(!r.has(c)){n=!1;break}n&&o.add(`${e}:${l}`)}return o}function it(i){return`${i.comparison.family}\0${i.comparison.left}`}function st(i,a,s){const o=(r,n)=>{r.absorbedIds=[...r.absorbedIds??[],n.id]},e=new Map;for(const r of i){const n=it(r);(e.get(n)??e.set(n,[]).get(n)).push(r)}const f=nt(a),l=new Set;for(const r of e.values()){for(const c of r)if(!(c.variable.kind!=="hardBin"||c.variable.bin===void 0))for(const t of r){if(t.variable.kind!=="softBin"||t.variable.bin===void 0||l.has(t.id)||!f.has(`${c.variable.bin}:${t.variable.bin}`)||t.effect.direction!==c.effect.direction)continue;o(c,t),l.add(t.id);const h=c.variable.label.replace(/^HBin\s+/,""),m=t.variable.label.replace(/^SBin\s+/,""),d=h===m?`HBin and SBin ${h} (same dies)`:`${c.variable.label} and ${t.variable.label} (same dies)`;c.summary=c.summary.replace(c.variable.label,d),c.variable.label=d;break}if(s.length!==1)continue;const n=r.find(c=>c.variable.kind==="yield");if(n){for(const c of r)if(!(c===n||l.has(c.id)||!((c.variable.kind==="hardBin"||c.variable.kind==="softBin")&&c.variable.bin===s[0]))){o(n,c),l.add(c.id);for(const h of c.absorbedIds??[])n.absorbedIds=[...n.absorbedIds??[],h]}}}}export function analyzeWaferMap(i,a={}){const{resolved:s,warnings:o}=Ke(a),e=Te(i),f=e.isLotStack,l=e.aggrMethod,r=!f||l==="mode"||l==="countBin"||l==="percent",n=e.dies.filter(u=>je(u,s)),c=n.filter(de),t=xe(s,n.length),h=e.dies.filter(u=>ee(u,t)),m=h.filter(de),d=Fe(m,e.wafer,t.ringCount),g=Be(m,e.wafer,t.ringCount),p=t.enableReticlePositionAnalysis?Ce(m,e.reticleConfig):[],S=t.enableTestSiteAnalysis===!1?[]:Pe(h,t.enableTestSiteAnalysis===!0),D=t.enableAngularAnalysis?Ne(m,e.wafer,t.sectorCount):[],y=[];if(t.enableYieldAnalysis&&r&&y.push(...Y(n,d,t.passBins,t),...Y(n,g,t.passBins,t),...Y(n,p,t.passBins,t),...Y(n,S,t.passBins,t),...Y(n,D,t.passBins,t)),t.enableHardBinAnalysis&&r&&y.push(...V(n,d,"hard",e.hbinDefs,"hardBin",t),...V(n,g,"hard",e.hbinDefs,"hardBin",t),...V(n,p,"hard",e.hbinDefs,"hardBin",t),...V(n,S,"hard",e.hbinDefs,"hardBin",t),...V(n,D,"hard",e.hbinDefs,"hardBin",t)),t.enableSoftBinAnalysis){const u=n.filter(M=>M.sbin!==void 0);y.push(...V(u,d,"soft",e.sbinDefs,"softBin",t),...V(u,g,"soft",e.sbinDefs,"softBin",t),...V(u,p,"soft",e.sbinDefs,"softBin",t),...V(u,S,"soft",e.sbinDefs,"softBin",t),...V(u,D,"soft",e.sbinDefs,"softBin",t))}const b=[...o];let $;if(t.enableTestValueAnalysis){const u=Q(n,d,e.testDefs,t),M=Q(n,g,e.testDefs,t),B=Q(n,p,e.testDefs,t),z=Q(n,S,e.testDefs,t),F=Q(n,D,e.testDefs,t);y.push(...u.findings,...M.findings,...B.findings,...z.findings,...F.findings),u.warning&&b.push(u.warning),$=u.activeTestNumbers,y.push(...Qe(n,[d,g,p,S,D],e.testDefs,t)),y.push(...W(n,d,e.testDefs,t),...W(n,g,e.testDefs,t),...W(n,p,e.testDefs,t),...W(n,S,e.testDefs,t),...W(n,D,e.testDefs,t))}if(t.enableClusterAnalysis){const u=Oe(f,r,e.testDefs);(!f||r||u!==void 0)&&y.push(...Le(c,e.wafer,{...t,isFailingDie:u}))}const k=tt(y,{eligibleDies:n,softEligibleDies:n.filter(u=>u.sbin!==void 0),testDies:n,passBins:t.passBins,ringCount:t.ringCount,sectorCount:t.sectorCount,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs});if(y.length=0,y.push(...k),t.enablePatternClassification&&r){const u=Ee(c,e.wafer,{passBins:t.passBins,ringCount:t.ringCount});if(u!==null&&u.pattern!=="random"&&u.pattern!=="none"){let Me=function(C){const L=T(C).map(K=>O(K).ring).filter(K=>K!==void 0);if(L.length===0)return!0;const ce=L.some(K=>K===ze),ue=L.some(K=>K===1);return A==="edge-ring"||A==="edge-local"?ce:A==="center"?ue:A==="donut"?!ce&&!ue:!0};const B={center:"Center cluster",donut:"Donut","edge-ring":"Edge-ring","edge-local":"Edge-local",scratch:"Scratch","near-full":"Near-full"}[u.pattern]??u.pattern,z=u.confidence==="high"?"unusual":u.confidence==="medium"?"notable":"info",F=u.features,N=C=>`${(C*100).toFixed(0)}%`,E=u.pattern==="edge-ring"||u.pattern==="edge-local"?`${N(F.edgeRdd)} of edge dies failing`:u.pattern==="center"||u.pattern==="donut"?`centroid at ${N(F.centroidDistNorm)} of wafer radius from centre`:u.pattern==="scratch"?`linear score ${F.linearScore.toFixed(2)}, eccentricity ${F.eccentricity.toFixed(2)}`:`${N(F.globalRdd)} of dies failing`,P=n.filter(C=>{const L=C.hbin??C.sbin;return L!==void 0&&!new Set(t.passBins).has(L)}),U={"edge-ring":["ring","edge-arc"],"edge-local":["edge-arc","sector","quadrant"],center:["ring","cluster"],donut:["ring"],scratch:["cluster","sector","quadrant"],"near-full":["ring"]},G=new Set(U[u.pattern]??[]),A=u.pattern,ze=t.ringCount,Z=[];for(const C of y){if(!G.has(C.comparison.family)||C.comparison.family==="ring"&&!Me(C))continue;Z.push(C.id);const L=C.comparison.family==="ring"||C.comparison.family==="quadrant"||C.comparison.family==="sector";z!=="info"&&L&&(C.severity="info")}y.push({id:`spatial-pattern:${u.pattern}`,level:"wafer",severity:z,variable:{kind:"spatialPattern",label:B},comparison:{family:"spatial-pattern",left:B,right:"Wafer"},effect:{direction:"different",effectSize:F.globalRdd},stats:{method:"geometry",sampleSizeLeft:P.length,sampleSizeRight:n.length},summary:`Spatial pattern: ${B.toLowerCase()} (${u.confidence} confidence) \u2014 ${E}${u.note?` [${u.note}]`:""}`,highlight:{kind:"dies",dieKeys:P.map(C=>x(C))},relatedIds:Z.length>0?Z:void 0})}}y.sort((u,M)=>{const B=u.stats.adjustedPValue??u.stats.pValue??1,z=M.stats.adjustedPValue??M.stats.pValue??1;return B!==z?B-z:Math.abs(M.effect.absoluteDelta??0)-Math.abs(u.effect.absoluteDelta??0)});const w=He(e.dies,n.length,e.yield.yieldPercent);f&&(w.isLotStack=!0,l&&(w.aggregationMethod=l));const v=qe(e.dies,e.testDefs);v&&(w.testSpecYield=v);const R=computeFunctionalYield(e.dies,e.testDefs);if(R&&(w.functionalYield=R),$===void 0&&t.computePerTestStats){const u=ke(n,t.testNumbers,e.testDefs);u.warning&&b.push(u.warning),$=u.testNumbers}if($?.length){const u=Ie(e.dies,$,e.testDefs,t.minimumSampleSize);u&&(w.perTestStats=u)}return b.length>0&&(w.warnings=b),st(y,n,t.passBins),{level:"wafer",hasNotableFindings:y.some(u=>u.severity==="notable"||u.severity==="unusual"),findings:y,wafer:e.wafer.metadata??void 0,stats:w}}
@@ -46,4 +46,19 @@ export declare function buildBinClusterData(groups: {
46
46
  key: string;
47
47
  items: BinItem[];
48
48
  }[], binType: BinType): BinClusterData;
49
+ /**
50
+ * Display order for a bin breakdown: pass bins first (in bin order), then failing
51
+ * bins by descending count, ties broken by bin number.
52
+ *
53
+ * The single source of this ordering. It previously existed as three separate
54
+ * ascending-bin-number sorts — the Summary panel's bin section and the summary
55
+ * report's two bin tables (per-wafer and lot) — none of which agreed with this
56
+ * module's own `buildBinParetoData`, which has always been count-descending. The
57
+ * effect was that the same wafer's bins came out in one order in the Insights bin
58
+ * chart and a different one in the panel and the report.
59
+ *
60
+ * Pass bins are pinned to the top rather than competing on count: they are the
61
+ * reference the failures are read against, not themselves a failure mode.
62
+ */
63
+ export declare function sortBinsForDisplay(entries: Iterable<[bin: number, count: number]>, passBins?: number[]): Array<[bin: number, count: number]>;
49
64
  //# sourceMappingURL=binPareto.d.ts.map
@@ -1 +1 @@
1
- import{isYieldEligibleDie as m}from"../core/dies.js";export function buildBinParetoData(l,a){const s=new Map;let i=0;for(const o of l){const r=a==="hbin"?o.hardBinCounts:o.softBinCounts;if(r){for(const[c,t]of Object.entries(r)){const n=Number(c);i+=t,s.set(n,(s.get(n)??0)+t)}continue}for(const c of o.dies??[]){if(!m(c))continue;const t=a==="hbin"?c.hbin:c.sbin;t!==void 0&&(i++,s.set(t,(s.get(t)??0)+1))}}const f=Array.from(s.entries()).map(([o,r])=>({label:`Bin ${o}`,value:r,percent:i>0?r/i*100:0,itemCount:1,binCode:o}));return f.sort((o,r)=>r.value-o.value),f}export function buildBinClusterData(l,a){const s=l.map(t=>t.key),i=new Map,f=[];function o(t,n,u){let e=i.get(t);e||(e={total:0,counts:[]},i.set(t,e),f.push(t)),e.total+=u,e.counts[n]=(e.counts[n]??0)+u}l.forEach((t,n)=>{for(const u of t.items){const e=a==="hbin"?u.hardBinCounts:u.softBinCounts;if(e){for(const[d,b]of Object.entries(e))o(Number(d),n,b);continue}for(const d of u.dies??[]){if(!m(d))continue;const b=a==="hbin"?d.hbin:d.sbin;b!==void 0&&o(b,n,1)}}});const c=f.slice().sort((t,n)=>i.get(n).total-i.get(t).total).map(t=>{const n=i.get(t);return{binCode:t,label:`Bin ${t}`,total:n.total,counts:s.map((u,e)=>n.counts[e]??0)}});return{groups:s,bins:c}}
1
+ import{isYieldEligibleDie as p}from"../core/dies.js";export function buildBinParetoData(l,u){const s=new Map;let n=0;for(const o of l){const i=u==="hbin"?o.hardBinCounts:o.softBinCounts;if(i){for(const[a,t]of Object.entries(i)){const e=Number(a);n+=t,s.set(e,(s.get(e)??0)+t)}continue}for(const a of o.dies??[]){if(!p(a))continue;const t=u==="hbin"?a.hbin:a.sbin;t!==void 0&&(n++,s.set(t,(s.get(t)??0)+1))}}const c=Array.from(s.entries()).map(([o,i])=>({label:`Bin ${o}`,value:i,percent:n>0?i/n*100:0,itemCount:1,binCode:o}));return c.sort((o,i)=>i.value-o.value),c}export function buildBinClusterData(l,u){const s=l.map(t=>t.key),n=new Map,c=[];function o(t,e,f){let r=n.get(t);r||(r={total:0,counts:[]},n.set(t,r),c.push(t)),r.total+=f,r.counts[e]=(r.counts[e]??0)+f}l.forEach((t,e)=>{for(const f of t.items){const r=u==="hbin"?f.hardBinCounts:f.softBinCounts;if(r){for(const[d,b]of Object.entries(r))o(Number(d),e,b);continue}for(const d of f.dies??[]){if(!p(d))continue;const b=u==="hbin"?d.hbin:d.sbin;b!==void 0&&o(b,e,1)}}});const a=c.slice().sort((t,e)=>n.get(e).total-n.get(t).total).map(t=>{const e=n.get(t);return{binCode:t,label:`Bin ${t}`,total:e.total,counts:s.map((f,r)=>e.counts[r]??0)}});return{groups:s,bins:a}}export function sortBinsForDisplay(l,u=[1]){const s=new Set(u);return[...l].sort((n,c)=>{const o=s.has(n[0]),i=s.has(c[0]);return o!==i?o?-1:1:o&&i?n[0]-c[0]:c[1]-n[1]||n[0]-c[0]})}
@@ -1 +1 @@
1
- import{normalCdf as rt}from"./math.js";import{getDieKey as $}from"../core/dies.js";const ft=["E","ENE","NE","NNE","N","NNW","NW","WNW","W","WSW","SW","SSW","S","SSE","SE","ESE"];function ut(n,o){const i=(Math.atan2(o,n)+2*Math.PI)%(2*Math.PI);return ft[Math.round(i/(2*Math.PI)*16)%16]}function lt(n,o,i,d){const g=d!==void 0?Math.abs(d):0;return n<=.01&&(o>=.25||g>=2||i>=.1)?"unusual":n<=.05&&(o>=.15||g>=1||i>=.03)?"notable":"info"}function ht(n,o,i){if(o===0||i<=0)return n>0?0:1;if(i>=1)return n<=o?0:1;const d=o*i,g=Math.sqrt(o*i*(1-i));if(g===0)return n>d?0:1;const x=(n-.5-d)/g;return 1-rt(x)}export function buildClusterFindings(n,o,i){const{passBins:d,significanceLevel:g,minimumEffectSize:x,minimumRelativeEffect:G,minimumSampleSize:I,minimumClusterSize:H,ringCount:J}=i,Q=new Set(d);if(n.length<I)return[];const U=t=>{const c=t.hbin??t.sbin;return c===void 0||!Q.has(c)},Z=i.isFailingDie??U,E=n.filter(Z),y=E.length/n.length,M=n[0],B=M?M.width:10,K=M?M.height:10,F=Math.max(B,K)*1.5,R=new Map;for(const t of E)R.set($(t),t);const P=new Map;for(const t of n)P.set($(t),t);const A=Math.ceil(F/B),k=Math.ceil(F/K),b=new Set,V=[];for(const t of E){const c=$(t);if(b.has(c))continue;const r=[],u=[t];for(b.add(c);u.length>0;){const s=u.pop();r.push(s);for(let l=-1;l<=1;l++)for(let h=-1;h<=1;h++){if(h===0&&l===0)continue;const f=`${s.x+h},${s.y+l}`;if(b.has(f))continue;const p=R.get(f);p&&(b.add(f),u.push(p))}}V.push(r)}const L=[],v=o.center.x,Y=o.center.y,tt=o.radius;for(const t of V){if(t.length<H)continue;const c=new Set(t.map(e=>$(e))),r=new Set(c);for(const e of t)for(let a=-k;a<=k;a++)for(let N=-A;N<=A;N++){const W=`${e.x+N},${e.y+a}`;if(r.has(W))continue;const X=P.get(W);X&&Math.hypot(X.physX-e.physX,X.physY-e.physY)<=F&&r.add(W)}const u=[...r].map(e=>P.get(e)).filter(Boolean),s=t.length,l=u.length;if(l<I)continue;const h=s/l,f=h-y,p=ht(s,l,y),S=y>0?f/y:void 0,et=f>=x||S!==void 0&&S>=G;if(p>g||!et)continue;const q=t.reduce((e,a)=>e+a.physX,0)/s,j=t.reduce((e,a)=>e+a.physY,0)/s;let m=t[0],O=1/0;for(const e of t){const a=Math.hypot(e.physX-q,e.physY-j);a<O&&(O=a,m=e)}const w=t.map(e=>(Math.atan2(e.physY-Y,e.physX-v)+2*Math.PI)%(2*Math.PI));let z=w[0],C=w[0];for(const e of w)z=Math.min(z,e),C=Math.max(C,e);const nt=(C-z)*180/Math.PI,st=1-1/J,D=t.filter(e=>Math.hypot(e.physX-v,e.physY-Y)/tt>st).length>s/2&&nt<120,T=D?"edge-arc":"cluster",_=ut(q-v,j-Y),ot=D?`Edge arc ~${_}`:`Cluster at (${m.x}, ${m.y})`,it=s/n.length,ct=lt(p,f,it,S),at=[...c];L.push({id:`${T}:${m.x},${m.y}`,level:"wafer",severity:ct,variable:{kind:"yield",label:"Yield"},comparison:{family:T,left:ot,right:"Rest of wafer"},effect:{direction:"lower",absoluteDelta:-f,relativeDelta:S!==void 0?-S:void 0,effectSize:f},stats:{method:"binomial",pValue:p,adjustedPValue:p,sampleSizeLeft:s,sampleSizeRight:n.length-s},summary:D?`Edge arc near ${_}: ${s} contiguous failing dies (${(h*100).toFixed(0)}% vs ${(y*100).toFixed(0)}% background)`:`Failure cluster at (${m.x}, ${m.y}): ${s} contiguous failing dies (${(h*100).toFixed(0)}% vs ${(y*100).toFixed(0)}% background)`,highlight:{kind:"dies",dieKeys:at}})}return L.sort((t,c)=>{const r=t.stats.pValue??1,u=c.stats.pValue??1;return r!==u?r-u:Math.abs(c.effect.effectSize??0)-Math.abs(t.effect.effectSize??0)})}
1
+ import{normalCdf as ct}from"./math.js";import{getDieKey as q}from"../core/dies.js";import{findConnectedComponents as at}from"./connectedComponents.js";const rt=["E","ENE","NE","NNE","N","NNW","NW","WNW","W","WSW","SW","SSW","S","SSE","SE","ESE"];function ft(n,s){const i=(Math.atan2(s,n)+2*Math.PI)%(2*Math.PI);return rt[Math.round(i/(2*Math.PI)*16)%16]}function lt(n,s,i,r){const f=r!==void 0?Math.abs(r):0;return n<=.01&&(s>=.25||f>=2||i>=.1)?"unusual":n<=.05&&(s>=.15||f>=1||i>=.03)?"notable":"info"}function ut(n,s,i){if(s===0||i<=0)return n>0?0:1;if(i>=1)return n<=s?0:1;const r=s*i,f=Math.sqrt(s*i*(1-i));if(f===0)return n>r?0:1;const S=(n-.5-r)/f;return 1-ct(S)}export function buildClusterFindings(n,s,i){const{passBins:r,significanceLevel:f,minimumEffectSize:S,minimumRelativeEffect:O,minimumSampleSize:W,minimumClusterSize:T,ringCount:_}=i,G=new Set(r);if(n.length<W)return[];const H=e=>{const a=e.hbin??e.sbin;return a===void 0||!G.has(a)},J=i.isFailingDie??H,X=n.filter(J),u=X.length/n.length,m=n[0],I=m?m.width:10,w=m?m.height:10,M=Math.max(I,w)*1.5,b=new Map;for(const e of n)b.set(q(e),e);const B=Math.ceil(M/I),R=Math.ceil(M/w),Q=at(X),A=[],$=s.center.x,x=s.center.y,U=s.radius;for(const e of Q){if(e.length<T)continue;const a=new Set(e.map(t=>q(t))),h=new Set(a);for(const t of e)for(let c=-R;c<=R;c++)for(let z=-B;z<=B;z++){const D=`${t.x+z},${t.y+c}`;if(h.has(D))continue;const N=b.get(D);N&&Math.hypot(N.physX-t.physX,N.physY-t.physY)<=M&&h.add(D)}const p=[...h].map(t=>b.get(t)).filter(Boolean),o=e.length,E=p.length;if(E<W)continue;const C=o/E,d=C-u,y=ut(o,E,u),g=u>0?d/u:void 0,Z=d>=S||g!==void 0&&g>=O;if(y>f||!Z)continue;const K=e.reduce((t,c)=>t+c.physX,0)/o,V=e.reduce((t,c)=>t+c.physY,0)/o;let l=e[0],k=1/0;for(const t of e){const c=Math.hypot(t.physX-K,t.physY-V);c<k&&(k=c,l=t)}const F=e.map(t=>(Math.atan2(t.physY-x,t.physX-$)+2*Math.PI)%(2*Math.PI));let P=F[0],Y=F[0];for(const t of F)P=Math.min(P,t),Y=Math.max(Y,t);const tt=(Y-P)*180/Math.PI,et=1-1/_,v=e.filter(t=>Math.hypot(t.physX-$,t.physY-x)/U>et).length>o/2&&tt<120,L=v?"edge-arc":"cluster",j=ft(K-$,V-x),nt=v?`Edge arc ~${j}`:`Cluster at (${l.x}, ${l.y})`,st=o/n.length,it=lt(y,d,st,g),ot=[...a];A.push({id:`${L}:${l.x},${l.y}`,level:"wafer",severity:it,variable:{kind:"yield",label:"Yield"},comparison:{family:L,left:nt,right:"Rest of wafer"},effect:{direction:"lower",absoluteDelta:-d,relativeDelta:g!==void 0?-g:void 0,effectSize:d},stats:{method:"binomial",pValue:y,adjustedPValue:y,sampleSizeLeft:o,sampleSizeRight:n.length-o},summary:v?`Edge arc near ${j}: ${o} contiguous failing dies (${(C*100).toFixed(0)}% vs ${(u*100).toFixed(0)}% background)`:`Failure cluster at (${l.x}, ${l.y}): ${o} contiguous failing dies (${(C*100).toFixed(0)}% vs ${(u*100).toFixed(0)}% background)`,highlight:{kind:"dies",dieKeys:ot}})}return A.sort((e,a)=>{const h=e.stats.pValue??1,p=a.stats.pValue??1;return h!==p?h-p:Math.abs(a.effect.effectSize??0)-Math.abs(e.effect.effectSize??0)})}
@@ -0,0 +1,14 @@
1
+ import type { PositionedDie } from '../core/dies.js';
2
+ /**
3
+ * Group `failing` into 8-connected components on the integer die grid.
4
+ *
5
+ * Adjacency is `|dx| <= 1 && |dy| <= 1` on `die.x`/`die.y` — the ORIGINAL
6
+ * prober coordinates, per this library's rule that those are the only
7
+ * coordinates any consumer sees. Dies not in `failing` are never traversed, so
8
+ * a component is a contiguous run of failures and nothing else.
9
+ *
10
+ * Iterative, not recursive: a large contiguous failure region is exactly the
11
+ * case this is for, and it is also exactly the case that blows a call stack.
12
+ */
13
+ export declare function findConnectedComponents(failing: readonly PositionedDie[]): PositionedDie[][];
14
+ //# sourceMappingURL=connectedComponents.d.ts.map
@@ -0,0 +1 @@
1
+ import{getDieKey as y}from"../core/dies.js";export function findConnectedComponents(s){if(s.length===0)return[];const r=new Map;for(const e of s)r.set(y(e),e);const n=new Set,u=[];for(const e of s){const f=y(e);if(n.has(f))continue;const p=[],c=[e];for(n.add(f);c.length>0;){const d=c.pop();p.push(d);for(let t=-1;t<=1;t++)for(let o=-1;o<=1;o++){if(o===0&&t===0)continue;const i=`${d.x+o},${d.y+t}`;if(n.has(i))continue;const h=r.get(i);h&&(n.add(i),c.push(h))}}u.push(p)}return u}
@@ -10,7 +10,33 @@ export interface CorrelationCell {
10
10
  xIndex: number;
11
11
  yIndex: number;
12
12
  r: number | null;
13
+ /**
14
+ * Dies contributing to this pair — i.e. those carrying a finite value for BOTH
15
+ * tests, which is not the same as the population size when tests have different
16
+ * coverage. An `r` without its `n` is not interpretable: |r| = 0.8 over 6 dies
17
+ * and over 6,000 are very different claims, and the panel's "strong pair" count
18
+ * thresholds on |r| alone.
19
+ */
20
+ n: number;
13
21
  }
22
+ /**
23
+ * Pearson r from running sums. The single implementation of the formula —
24
+ * `buildCorrelationMatrix`'s per-pair accumulators and `pearsonOfPairs` (used by
25
+ * the scatter panel to report r for the pair it is displaying) both go through
26
+ * here, so the matrix cell and the scatter card can never disagree about the
27
+ * same pair. Returns null below 3 points or with zero variance in either axis.
28
+ */
29
+ export declare function pearsonFromSums(c: number, sumX: number, sumY: number, sumXX: number, sumYY: number, sumXY: number): number | null;
30
+ /** Pearson r and n for an explicit list of XY pairs — the scatter panel's own
31
+ * displayed points, after any legend filtering. Shares `pearsonFromSums` with
32
+ * the matrix. */
33
+ export declare function pearsonOfPairs(pairs: ArrayLike<{
34
+ x: number;
35
+ y: number;
36
+ }>): {
37
+ r: number | null;
38
+ n: number;
39
+ };
14
40
  export interface CorrelationMatrix {
15
41
  tests: CorrelationTestInfo[];
16
42
  cells: CorrelationCell[];
@@ -1 +1 @@
1
- import{isParametricTest as A}from"../renderer/buildWaferMap.js";function F(i){return i.filter(c=>c.testNumber!==void 0&&A(c)).map(c=>({testNumber:c.testNumber,label:c.name,unit:c.unit}))}export function buildCorrelationMatrix(i,c){const u=F(c);if(u.length<2)return{tests:u,cells:[]};const l=u.length,y=u.map(t=>t.testNumber),f=l*(l-1)/2,b=new Float64Array(f),x=new Float64Array(f),r=new Float64Array(f),I=new Float64Array(f),p=new Float64Array(f),g=new Float64Array(f);function w(t,e){return t*l-(t*(t+1)>>1)+(e-t-1)}for(const t of i){if(!t.testValues)continue;const e=new Float64Array(l),o=new Uint8Array(l);for(let n=0;n<l;n++){const s=t.testValues[y[n]];s!==void 0&&Number.isFinite(s)&&(e[n]=s,o[n]=1)}for(let n=0;n<l;n++){if(!o[n])continue;const s=e[n];for(let a=n+1;a<l;a++){if(!o[a])continue;const d=e[a],m=w(n,a);b[m]++,x[m]+=s,r[m]+=d,I[m]+=s*s,p[m]+=d*d,g[m]+=s*d}}}function M(t){const e=b[t];if(e<3)return null;const o=x[t]/e,n=r[t]/e,s=g[t]/e-o*n,a=I[t]/e-o*o,d=p[t]/e-n*n,m=Math.sqrt(a*d);return m===0?null:Math.max(-1,Math.min(1,s/m))}const h=[];for(let t=0;t<l;t++)for(let e=0;e<l;e++){if(e===t){h.push({xIndex:e,yIndex:t,r:1});continue}const o=Math.min(e,t),n=Math.max(e,t),s=M(w(o,n));h.push({xIndex:e,yIndex:t,r:s})}return{tests:u,cells:h}}export function filterCorrelationMatrix(i,{threshold:c=.3,minTests:u=6,maxTests:l=20}={}){const y=[];let f=0,b=0,x=null;for(const t of i.cells){if(t.xIndex>=t.yIndex||t.r===null)continue;const e=Math.abs(t.r);y.push({xi:t.xIndex,yi:t.yIndex,absR:e}),(x===null||e>Math.abs(x.r))&&(x={xLabel:i.tests[t.xIndex].label,yLabel:i.tests[t.yIndex].label,r:t.r})}y.sort((t,e)=>e.absR-t.absR);const r=new Set;for(const{xi:t,yi:e,absR:o}of y){const n=o<c;r.size>=l||n&&r.size>=u||(r.add(t),r.size<l&&r.add(e))}if(r.size<Math.min(u,i.tests.length))for(let t=0;t<i.tests.length&&r.size<Math.min(l,u,i.tests.length);t++)r.add(t);const I=(()=>{const t=Array.from(r),e=new Map,o=new Map;for(const{xi:s,yi:a,absR:d}of y)!r.has(s)||!r.has(a)||(e.set(s,(e.get(s)??0)+d),e.set(a,(e.get(a)??0)+d),o.set(s,(o.get(s)??0)+1),o.set(a,(o.get(a)??0)+1));const n=s=>(o.get(s)??0)>0?e.get(s)/o.get(s):0;return t.sort((s,a)=>n(a)-n(s))})(),p=I.map(t=>i.tests[t]),g=new Map(I.map((t,e)=>[t,e])),w=new Set(p.map(t=>t.testNumber)),M=i.cells.filter(t=>w.has(i.tests[t.xIndex].testNumber)&&w.has(i.tests[t.yIndex].testNumber)).map(t=>({xIndex:g.get(t.xIndex),yIndex:g.get(t.yIndex),r:t.r}));let h=0;for(const{xi:t,yi:e,absR:o}of y)r.has(t)&&r.has(e)?o>=.7?f++:o>=.4&&b++:o<c&&h++;return{matrix:{tests:p,cells:M},strongPairs:f,moderatePairs:b,hiddenWeakPairs:h,strongestPair:x}}
1
+ import{isParametricTest as A}from"../renderer/buildWaferMap.js";export function pearsonFromSums(o,l,u,i,y,f){if(o<3)return null;const d=l/o,c=u/o,r=f/o-d*c,m=i/o-d*d,p=y/o-c*c,h=Math.sqrt(m*p);return h===0?null:Math.max(-1,Math.min(1,r/h))}export function pearsonOfPairs(o){let l=0,u=0,i=0,y=0,f=0,d=0;for(let c=0;c<o.length;c++){const{x:r,y:m}=o[c];!Number.isFinite(r)||!Number.isFinite(m)||(l++,u+=r,i+=m,y+=r*r,f+=m*m,d+=r*m)}return{r:pearsonFromSums(l,u,i,y,f,d),n:l}}function N(o){return o.filter(l=>l.testNumber!==void 0&&A(l)).map(l=>({testNumber:l.testNumber,label:l.name,unit:l.unit}))}export function buildCorrelationMatrix(o,l){const u=N(l);if(u.length<2)return{tests:u,cells:[]};const i=u.length,y=u.map(t=>t.testNumber),f=i*(i-1)/2,d=new Float64Array(f),c=new Float64Array(i),r=new Float64Array(f),m=new Float64Array(f),p=new Float64Array(f),h=new Float64Array(f),g=new Float64Array(f);function w(t,n){return t*i-(t*(t+1)>>1)+(n-t-1)}for(const t of o){if(!t.testValues)continue;const n=new Float64Array(i),x=new Uint8Array(i);for(let s=0;s<i;s++){const a=t.testValues[y[s]];a!==void 0&&Number.isFinite(a)&&(n[s]=a,x[s]=1)}for(let s=0;s<i;s++){if(!x[s])continue;c[s]++;const a=n[s];for(let b=s+1;b<i;b++){if(!x[b])continue;const M=n[b],I=w(s,b);d[I]++,r[I]+=a,m[I]+=M,p[I]+=a*a,h[I]+=M*M,g[I]+=a*M}}}const F=t=>pearsonFromSums(d[t],r[t],m[t],p[t],h[t],g[t]),e=[];for(let t=0;t<i;t++)for(let n=0;n<i;n++){if(n===t){e.push({xIndex:n,yIndex:t,r:1,n:c[n]});continue}const x=Math.min(n,t),s=Math.max(n,t),a=w(x,s);e.push({xIndex:n,yIndex:t,r:F(a),n:d[a]})}return{tests:u,cells:e}}export function filterCorrelationMatrix(o,{threshold:l=.3,minTests:u=6,maxTests:i=20}={}){const y=[];let f=0,d=0,c=null;for(const e of o.cells){if(e.xIndex>=e.yIndex||e.r===null)continue;const t=Math.abs(e.r);y.push({xi:e.xIndex,yi:e.yIndex,absR:t}),(c===null||t>Math.abs(c.r))&&(c={xLabel:o.tests[e.xIndex].label,yLabel:o.tests[e.yIndex].label,r:e.r})}y.sort((e,t)=>t.absR-e.absR);const r=new Set;for(const{xi:e,yi:t,absR:n}of y){const x=n<l;r.size>=i||x&&r.size>=u||(r.add(e),r.size<i&&r.add(t))}if(r.size<Math.min(u,o.tests.length))for(let e=0;e<o.tests.length&&r.size<Math.min(i,u,o.tests.length);e++)r.add(e);const m=(()=>{const e=Array.from(r),t=new Map,n=new Map;for(const{xi:s,yi:a,absR:b}of y)!r.has(s)||!r.has(a)||(t.set(s,(t.get(s)??0)+b),t.set(a,(t.get(a)??0)+b),n.set(s,(n.get(s)??0)+1),n.set(a,(n.get(a)??0)+1));const x=s=>(n.get(s)??0)>0?t.get(s)/n.get(s):0;return e.sort((s,a)=>x(a)-x(s))})(),p=m.map(e=>o.tests[e]),h=new Map(m.map((e,t)=>[e,t])),g=new Set(p.map(e=>e.testNumber)),w=o.cells.filter(e=>g.has(o.tests[e.xIndex].testNumber)&&g.has(o.tests[e.yIndex].testNumber)).map(e=>({xIndex:h.get(e.xIndex),yIndex:h.get(e.yIndex),r:e.r,n:e.n}));let F=0;for(const{xi:e,yi:t,absR:n}of y)r.has(e)&&r.has(t)?n>=.7?f++:n>=.4&&d++:n<l&&F++;return{matrix:{tests:p,cells:w},strongPairs:f,moderatePairs:d,hiddenWeakPairs:F,strongestPair:c}}
@@ -1 +1 @@
1
- import{metadataDisplayValue as b}from"../core/metadata.js";import{compareNatural as C}from"../core/utils.js";export const FACET_NONE_VALUE="(none)";export{prettyKey}from"../core/utils.js";export const DEFAULT_FACET_CURATION={split:{label:"Split"},lot:{label:"Lot"},product:{label:"Product"},testProgram:{label:"Program"},temperature:{label:"Temperature"},testDate:{label:"Test date",date:!0},operator:{label:"Operator"},waferId:{label:"Wafer",facet:!1}};function y(e){const t=e.indexOf("T");return t>0?e.slice(0,t):e}export function facetValueOf(e,t,o=DEFAULT_FACET_CURATION){const l=b(e?.[t]);if(l!==void 0)return o[t]?.date?y(l):l}export function commonMetadata(e){if(!e.length)return{};const t=e[0].metadata??{},o={};for(const[l,r]of Object.entries(t))r==null||r===""||e.every(d=>(d.metadata??{})[l]===r)&&(o[l]=r);return o}export function buildFacetTable(e,t={}){const o={...DEFAULT_FACET_CURATION,...t.curation},l=t.facetableOnly??!0,r=new Set;for(const n of e)for(const c of Object.keys(n.metadata??{}))r.add(c);const m=[...Object.keys(o).filter(n=>r.has(n)),...[...r].filter(n=>!(n in o))],p=[];for(const n of m){const c=o[n];if(l&&c&&c.facet===!1)continue;const u=new Map;for(const f of e){const a=facetValueOf(f.metadata,n,o)??FACET_NONE_VALUE,i=u.get(a)??{waferCount:0,dieCount:0};i.waferCount+=1,i.dieCount+=f.dieCount??0,u.set(a,i)}if(u.size===0)continue;const s=Array.from(u,([f,a])=>({value:f,waferCount:a.waferCount,dieCount:a.dieCount}));s.sort((f,a)=>(f.value===FACET_NONE_VALUE?1:0)-(a.value===FACET_NONE_VALUE?1:0)||a.waferCount-f.waferCount||C(f.value,a.value)),p.push({key:n,label:c?.label??n,values:s,splittable:s.length>1})}return p}
1
+ import{metadataDisplayValue as b}from"../core/metadata.js";import{compareNatural as y,prettyKey as C}from"../core/utils.js";export const FACET_NONE_VALUE="(none)";export{prettyKey}from"../core/utils.js";export const DEFAULT_FACET_CURATION={split:{label:"Split"},lot:{label:"Lot"},product:{label:"Product"},testProgram:{label:"Program"},temperature:{label:"Temperature"},testDate:{label:"Test date",date:!0},operator:{label:"Operator"},waferId:{label:"Wafer",facet:!1}};function O(e){const t=e.indexOf("T");return t>0?e.slice(0,t):e}export function facetValueOf(e,t,o=DEFAULT_FACET_CURATION){const l=b(e?.[t]);if(l!==void 0)return o[t]?.date?O(l):l}export function commonMetadata(e){if(!e.length)return{};const t=e[0].metadata??{},o={};for(const[l,r]of Object.entries(t))r==null||r===""||e.every(d=>(d.metadata??{})[l]===r)&&(o[l]=r);return o}export function buildFacetTable(e,t={}){const o={...DEFAULT_FACET_CURATION,...t.curation},l=t.facetableOnly??!0,r=new Set;for(const n of e)for(const c of Object.keys(n.metadata??{}))r.add(c);const m=[...Object.keys(o).filter(n=>r.has(n)),...[...r].filter(n=>!(n in o))],p=[];for(const n of m){const c=o[n];if(l&&c&&c.facet===!1)continue;const u=new Map;for(const f of e){const a=facetValueOf(f.metadata,n,o)??FACET_NONE_VALUE,i=u.get(a)??{waferCount:0,dieCount:0};i.waferCount+=1,i.dieCount+=f.dieCount??0,u.set(a,i)}if(u.size===0)continue;const s=Array.from(u,([f,a])=>({value:f,waferCount:a.waferCount,dieCount:a.dieCount}));s.sort((f,a)=>(f.value===FACET_NONE_VALUE?1:0)-(a.value===FACET_NONE_VALUE?1:0)||a.waferCount-f.waferCount||y(f.value,a.value)),p.push({key:n,label:c?.label??C(n),values:s,splittable:s.length>1})}return p}
@@ -6,4 +6,22 @@ export interface FindingsFilter {
6
6
  level?: StatsLevel | StatsLevel[];
7
7
  }
8
8
  export declare function filterFindings(source: StatsSummary | LotStatsSummary, filter: FindingsFilter): StatsFinding[];
9
+ /**
10
+ * Drop findings another finding has claimed as an exact restatement of itself —
11
+ * a soft-bin twin covering the same dies, or the single pass bin's row against
12
+ * the yield row that says the same thing.
13
+ *
14
+ * The claimer's own label names what it absorbed ("hard bin and soft bin 3 (same
15
+ * dies)"), so listing both prints one fact twice: once merged, once not. The full
16
+ * uncollapsed list stays on `summary.findings` for any host that wants it.
17
+ *
18
+ * Extracted because this rule existed in three places and one of them was wrong:
19
+ * the Summary panel and the findings report both applied it, while the summary
20
+ * report printed every finding — so a wafer with 8 merged twins listed 16 rows,
21
+ * each duplicate contradicting the merge the label had just described.
22
+ */
23
+ export declare function visibleFindings<T extends {
24
+ id: string;
25
+ absorbedIds?: string[];
26
+ }>(findings: T[]): T[];
9
27
  //# sourceMappingURL=filterFindings.d.ts.map
@@ -1 +1 @@
1
- function t(s){return Array.isArray(s)?s:[s]}export function filterFindings(s,n){let e=s.findings;if(n.severity!==void 0){const i=new Set(t(n.severity));e=e.filter(d=>i.has(d.severity))}if(n.kind!==void 0){const i=new Set(t(n.kind));e=e.filter(d=>i.has(d.variable.kind))}if(n.family!==void 0){const i=new Set(t(n.family));e=e.filter(d=>i.has(d.comparison.family))}if(n.level!==void 0){const i=new Set(t(n.level));e=e.filter(d=>i.has(d.level))}return e}
1
+ function s(i){return Array.isArray(i)?i:[i]}export function filterFindings(i,n){let e=i.findings;if(n.severity!==void 0){const t=new Set(s(n.severity));e=e.filter(d=>t.has(d.severity))}if(n.kind!==void 0){const t=new Set(s(n.kind));e=e.filter(d=>t.has(d.variable.kind))}if(n.family!==void 0){const t=new Set(s(n.family));e=e.filter(d=>t.has(d.comparison.family))}if(n.level!==void 0){const t=new Set(s(n.level));e=e.filter(d=>t.has(d.level))}return e}export function visibleFindings(i){const n=new Set(i.flatMap(e=>e.absorbedIds??[]));return i.filter(e=>!n.has(e.id))}
@@ -1 +1 @@
1
- const _=["spatial-pattern","ring","sector","quadrant","cluster","edge-arc","reticle-position"],y={unusual:0,notable:1,info:2},S={E:0,ENE:22.5,NE:45,NNE:67.5,N:90,NNW:112.5,NW:135,WNW:157.5,W:180,WSW:202.5,SW:225,SSW:247.5,S:270,SSE:292.5,SE:315,ESE:337.5};function m(e){return e.stats.adjustedPValue??e.stats.pValue??1}function b(e){return[...e].sort((n,t)=>{const o=y[n.severity]-y[t.severity];return o!==0?o:m(n)-m(t)})[0]}function w(e){return e.some(t=>t.variable.kind==="yield")?e.filter(t=>t.variable.kind!=="hardBin"&&t.variable.kind!=="softBin"):e}function $(e){return e.replace(/^(Rings?|Quadrants?|Sectors?)\s+/i,"").split(/[,&]|–|\band\b/).map(n=>n.trim()).filter(Boolean)}function W(e,n){const t=new Set($(e));return $(n).some(o=>t.has(o))}function v(e){return e.variable.kind==="yield"?"yield":e.variable.kind==="hardBin"?`HBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.kind==="softBin"?`SBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.label}function h(e){let n=0,t=0;for(const o of e)o.effect.direction==="higher"?n++:o.effect.direction==="lower"&&t++;return n>t?"higher":t>n?"lower":"mixed"}function d(e){return e==="higher"?"elevated":e==="lower"?"reduced":"shifted"}function p(e){const n=new Set,t=[];for(const o of e){const r=v(o);n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:t.length===3?`${t[0]}, ${t[1]}, and ${t[2]}`:`${t[0]}, ${t[1]}, and ${t.length-2} more`}function E(e){const n=new Set,t=[];for(const o of e){const r=o.comparison.left;n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`}function j(e){return e.length!==1?!1:!/^(Rings|Quadrants|Sectors)\b/.test(e[0].comparison.left)}function A(e){const n=h(e),t=e.every(c=>c.comparison.left.includes("(edge)")),o=e.every(c=>c.comparison.left.includes("(core)"));let r,a;return o?(r="The core ring",a=!0):t&&e.length>1?(r="The edge rings",a=!1):(r=E(e),a=j(e)),`${r} show${a?"s":""} ${d(n)} ${p(e)}.`}function B(e){const n=h(e),t=s=>s.highlight.kind==="region"&&s.highlight.regionKeys.length>1||s.comparison.left.startsWith("Quadrants"),o=e.filter(s=>t(s)),r=e.filter(s=>!t(s)),a=[];r.length===1?a.push(`the ${r[0].comparison.left} quadrant`):r.length>1&&a.push(`the ${E(r)} quadrants`);for(const s of o)a.push(s.comparison.left.replace(/^Quadrants/,"quadrants"));const c=a.join(" and "),u=r.length===1&&o.length===0;return`${c.charAt(0).toUpperCase()+c.slice(1)} show${u?"s":""} ${d(n)} ${p(e)}.`}function P(e){const n=h(e),t=[],o=new Set;for(const u of e){const i=u.comparison.left.replace(/^Sectors? /,"");o.has(i)||(o.add(i),t.push(i))}let r,a;return t.length===1?(r=`the ${t[0]} ${e[0].comparison.left.startsWith("Sectors")?"sectors":"region"}`,a=!e[0].comparison.left.startsWith("Sectors")):(r=`the ${t.length===2?t.join(" and "):`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`} sectors`,a=!1),`${r.charAt(0).toUpperCase()+r.slice(1)} show${a?"s":""} ${d(n)} ${p(e)}.`}function q(e){if(e.length===1)return`A failure cluster at ${e[0].comparison.left.replace(/^Cluster at /,"")}.`;const t=e.reduce((o,r)=>(o.stats.sampleSizeLeft??0)>=(r.stats.sampleSizeLeft??0)?o:r).comparison.left.replace(/^Cluster at /,"");return`${e.length} failure clusters identified; the largest at ${t}.`}function x(e){const n=e[0].comparison.left.replace(/^Edge arc ~/,"");return e.length===1?`An edge arc near ${n} shows localised failures.`:`${e.length} edge arcs detected, including near ${n}.`}function C(e){const n=h(e);return e.length===1?`Reticle-position variation: ${e[0].comparison.left} shows ${d(n)} ${p(e)}.`:`Reticle-position variation across ${e.length} cells (${d(n)} ${p(e)}).`}function Y(e,n){const t=e.stats.method==="geometry"?` (${e.severity==="unusual"?"high":e.severity==="notable"?"medium":"low"} confidence)`:"",o=new Set(e.relatedIds??[]),r=n.find(c=>o.has(c.id)&&(c.comparison.family==="edge-arc"||c.comparison.family==="cluster"));let a="";return r&&(r.comparison.family==="edge-arc"?a=`; failures concentrated at the wafer edge near ${r.comparison.left.replace(/^Edge arc ~/,"")}`:a=`; failures concentrated near ${r.comparison.left.replace(/^Cluster at /,"")}`),`${e.comparison.left} failure pattern detected${t}${a}.`}function F(e){const n=e.replace(/\.$/,"");return n.charAt(0).toLowerCase()+n.slice(1)}function M(e){const n=e.filter(t=>t.variable.kind==="yield");return n.length>0?h(n):h(e)}function k(e,n){const t=new Map;for(const i of n){const s=t.get(i.comparison.left)??[];s.push(i),t.set(i.comparison.left,s)}const o=[],r=[];for(const i of t.values()){const s=M(i);s==="higher"?o.push(...i.filter(l=>l.effect.direction==="higher")):s==="lower"?r.push(...i.filter(l=>l.effect.direction==="lower")):o.push(...i)}const a=o.map(i=>i.comparison.left),c=r.filter(i=>!a.some(s=>W(s,i.comparison.left)));if(o.length>0&&c.length>0){const i=n.some(f=>f.variable.kind==="yield"),[s,l]=i?[c,o]:[o,c];return`${e(s).replace(/\.$/,"")} while ${F(e(l))}.`}if(o.length>0&&r.length>0&&c.length===0)return e(o);const u=M(n);if(u!=="mixed"&&n.some(i=>i.variable.kind==="yield")){const i=n.filter(s=>s.effect.direction===u);if(i.length>0)return e(i)}return e(n)}function N(e){let n=0,t=0,o=0;for(const r of e)for(const a of $(r.comparison.left)){const c=S[a];if(c===void 0)continue;const u=c*Math.PI/180;n+=Math.cos(u),t+=Math.sin(u),o++}return o===0||n===0&&t===0?null:(Math.atan2(t,n)*180/Math.PI+360)%360}function I(e){let n="E",t=360;for(const[o,r]of Object.entries(S)){const a=Math.abs((e-r+540)%360-180);a<t&&(t=a,n=o)}return n}function O(e){const n=e.filter(u=>u.effect.direction==="higher"),t=e.filter(u=>u.effect.direction==="lower");if(n.length===0||t.length===0)return null;const o=new Set(e.map(v));if(o.size!==1)return null;const r=N(n),a=N(t);return r===null||a===null||Math.abs((r-a+540)%360-180)<120?null:`${[...o][0]} increases from ${I(a)} toward ${I(r)} across the wafer.`}function T(e){const n=w(e);if(n.length===0)return"";const t=["sector","quadrant","ring"],o=new Map;for(const i of n){const s=o.get(i.comparison.family)??[];s.push(i),o.set(i.comparison.family,s)}const r=[...o.keys()].sort((i,s)=>{const l=Math.min(...o.get(i).map(m)),f=Math.min(...o.get(s).map(m));return l!==f?l-f:t.indexOf(i)-t.indexOf(s)})[0],a=o.get(r),c=O(a);return c||k(r==="ring"?A:r==="quadrant"?B:P,a)}function L(e,n){switch(e){case"cluster":return q(n);case"edge-arc":return x(n);case"reticle-position":return k(C,n);default:return""}}const R=new Set(["ring","quadrant","sector"]);export function buildFindingsNarrative(e){if(e.length===0)return"";const n=e.filter(s=>s.severity!=="info");if(n.length===0)return"Minor spatial variation detected; no strongly significant patterns.";const t=[],o=n.filter(s=>s.comparison.family==="spatial-pattern"),r=o.length?b(o):null,a=new Set(r?.relatedIds??[]);r&&t.push(Y(r,n));const c=n.filter(s=>s!==r&&s.comparison.family!=="spatial-pattern"&&!a.has(s.id));let u=c.filter(s=>R.has(s.comparison.family));if(r&&u.length){const s=w(u);s.length>0&&s.every(f=>f.variable.kind==="yield"&&f.effect.direction==="higher")&&(u=[])}const i=new Set;if(u.length){const s=T(u);if(s){t.push(s);for(const l of R)i.add(l)}}else if(!r){const s=b(c);if(s){const l=s.comparison.family,f=L(l,c.filter(g=>g.comparison.family===l));f&&(t.push(f),i.add(l))}}if(t.length<3){const s=c.filter(l=>(l.comparison.family==="cluster"||l.comparison.family==="edge-arc")&&!i.has(l.comparison.family));if(s.length){const l=s[0].comparison.family,f=L(l,s.filter(g=>g.comparison.family===l));f&&t.push(f)}}return t.slice(0,3).join(" ")}
1
+ const y={unusual:0,notable:1,info:2},S={E:0,ENE:22.5,NE:45,NNE:67.5,N:90,NNW:112.5,NW:135,WNW:157.5,W:180,WSW:202.5,SW:225,SSW:247.5,S:270,SSE:292.5,SE:315,ESE:337.5};function m(e){return e.stats.adjustedPValue??e.stats.pValue??1}function b(e){return[...e].sort((n,t)=>{const o=y[n.severity]-y[t.severity];return o!==0?o:m(n)-m(t)})[0]}function w(e){return e.some(t=>t.variable.kind==="yield")?e.filter(t=>t.variable.kind!=="hardBin"&&t.variable.kind!=="softBin"):e}function $(e){return e.replace(/^(Rings?|Quadrants?|Sectors?)\s+/i,"").split(/[,&]|–|\band\b/).map(n=>n.trim()).filter(Boolean)}function A(e,n){const t=new Set($(e));return $(n).some(o=>t.has(o))}function v(e){return e.variable.kind==="yield"?"yield":e.variable.kind==="hardBin"?`HBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.kind==="softBin"?`SBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.label}function h(e){let n=0,t=0;for(const o of e)o.effect.direction==="higher"?n++:o.effect.direction==="lower"&&t++;return n>t?"higher":t>n?"lower":"mixed"}function d(e){return e==="higher"?"elevated":e==="lower"?"reduced":"shifted"}function p(e){const n=new Set,t=[];for(const o of e){const r=v(o);n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:t.length===3?`${t[0]}, ${t[1]}, and ${t[2]}`:`${t[0]}, ${t[1]}, and ${t.length-2} more`}function E(e){const n=new Set,t=[];for(const o of e){const r=o.comparison.left;n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`}function R(e){return e.length!==1?!1:!/^(Rings|Quadrants|Sectors)\b/.test(e[0].comparison.left)}function B(e){const n=h(e),t=e.every(c=>c.comparison.left.includes("(edge)")),o=e.every(c=>c.comparison.left.includes("(core)"));let r,a;return o?(r="The core ring",a=!0):t&&e.length>1?(r="The edge rings",a=!1):(r=E(e),a=R(e)),`${r} show${a?"s":""} ${d(n)} ${p(e)}.`}function I(e){const n=h(e),t=s=>s.highlight.kind==="region"&&s.highlight.regionKeys.length>1||s.comparison.left.startsWith("Quadrants"),o=e.filter(s=>t(s)),r=e.filter(s=>!t(s)),a=[];r.length===1?a.push(`the ${r[0].comparison.left} quadrant`):r.length>1&&a.push(`the ${E(r)} quadrants`);for(const s of o)a.push(s.comparison.left.replace(/^Quadrants/,"quadrants"));const c=a.join(" and "),u=r.length===1&&o.length===0;return`${c.charAt(0).toUpperCase()+c.slice(1)} show${u?"s":""} ${d(n)} ${p(e)}.`}function P(e){const n=h(e),t=[],o=new Set;for(const u of e){const i=u.comparison.left.replace(/^Sectors? /,"");o.has(i)||(o.add(i),t.push(i))}let r,a;return t.length===1?(r=`the ${t[0]} ${e[0].comparison.left.startsWith("Sectors")?"sectors":"region"}`,a=!e[0].comparison.left.startsWith("Sectors")):(r=`the ${t.length===2?t.join(" and "):`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`} sectors`,a=!1),`${r.charAt(0).toUpperCase()+r.slice(1)} show${a?"s":""} ${d(n)} ${p(e)}.`}function x(e){if(e.length===1)return`A failure cluster at ${e[0].comparison.left.replace(/^Cluster at /,"")}.`;const t=e.reduce((o,r)=>(o.stats.sampleSizeLeft??0)>=(r.stats.sampleSizeLeft??0)?o:r).comparison.left.replace(/^Cluster at /,"");return`${e.length} failure clusters identified; the largest at ${t}.`}function C(e){const n=e[0].comparison.left.replace(/^Edge arc ~/,"");return e.length===1?`An edge arc near ${n} shows localised failures.`:`${e.length} edge arcs detected, including near ${n}.`}function q(e){const n=h(e);return e.length===1?`Reticle-position variation: ${e[0].comparison.left} shows ${d(n)} ${p(e)}.`:`Reticle-position variation across ${e.length} cells (${d(n)} ${p(e)}).`}function F(e,n){const t=e.stats.method==="geometry"?` (${e.severity==="unusual"?"high":e.severity==="notable"?"medium":"low"} confidence)`:"",o=new Set(e.relatedIds??[]),r=n.find(c=>o.has(c.id)&&(c.comparison.family==="edge-arc"||c.comparison.family==="cluster"));let a="";return r&&(r.comparison.family==="edge-arc"?a=`; failures concentrated at the wafer edge near ${r.comparison.left.replace(/^Edge arc ~/,"")}`:a=`; failures concentrated near ${r.comparison.left.replace(/^Cluster at /,"")}`),`${e.comparison.left} failure pattern detected${t}${a}.`}function Y(e){const n=e.replace(/\.$/,"");return n.charAt(0).toLowerCase()+n.slice(1)}function M(e){const n=e.filter(t=>t.variable.kind==="yield");return n.length>0?h(n):h(e)}function k(e,n){const t=new Map;for(const i of n){const s=t.get(i.comparison.left)??[];s.push(i),t.set(i.comparison.left,s)}const o=[],r=[];for(const i of t.values()){const s=M(i);s==="higher"?o.push(...i.filter(l=>l.effect.direction==="higher")):s==="lower"?r.push(...i.filter(l=>l.effect.direction==="lower")):o.push(...i)}const a=o.map(i=>i.comparison.left),c=r.filter(i=>!a.some(s=>A(s,i.comparison.left)));if(o.length>0&&c.length>0){const i=n.some(f=>f.variable.kind==="yield"),[s,l]=i?[c,o]:[o,c];return`${e(s).replace(/\.$/,"")} while ${Y(e(l))}.`}if(o.length>0&&r.length>0&&c.length===0)return e(o);const u=M(n);if(u!=="mixed"&&n.some(i=>i.variable.kind==="yield")){const i=n.filter(s=>s.effect.direction===u);if(i.length>0)return e(i)}return e(n)}function N(e){let n=0,t=0,o=0;for(const r of e)for(const a of $(r.comparison.left)){const c=S[a];if(c===void 0)continue;const u=c*Math.PI/180;n+=Math.cos(u),t+=Math.sin(u),o++}return o===0||n===0&&t===0?null:(Math.atan2(t,n)*180/Math.PI+360)%360}function W(e){let n="E",t=360;for(const[o,r]of Object.entries(S)){const a=Math.abs((e-r+540)%360-180);a<t&&(t=a,n=o)}return n}function D(e){const n=e.filter(u=>u.effect.direction==="higher"),t=e.filter(u=>u.effect.direction==="lower");if(n.length===0||t.length===0)return null;const o=new Set(e.map(v));if(o.size!==1)return null;const r=N(n),a=N(t);return r===null||a===null||Math.abs((r-a+540)%360-180)<120?null:`${[...o][0]} increases from ${W(a)} toward ${W(r)} across the wafer.`}function O(e){const n=w(e);if(n.length===0)return"";const t=["sector","quadrant","ring"],o=new Map;for(const i of n){const s=o.get(i.comparison.family)??[];s.push(i),o.set(i.comparison.family,s)}const r=[...o.keys()].sort((i,s)=>{const l=Math.min(...o.get(i).map(m)),f=Math.min(...o.get(s).map(m));return l!==f?l-f:t.indexOf(i)-t.indexOf(s)})[0],a=o.get(r),c=D(a);return c||k(r==="ring"?B:r==="quadrant"?I:P,a)}function j(e,n){switch(e){case"cluster":return x(n);case"edge-arc":return C(n);case"reticle-position":return k(q,n);default:return""}}const L=new Set(["ring","quadrant","sector"]);export function buildFindingsNarrative(e){if(e.length===0)return"";const n=e.filter(s=>s.severity!=="info");if(n.length===0)return"Minor spatial variation detected; no strongly significant patterns.";const t=[],o=n.filter(s=>s.comparison.family==="spatial-pattern"),r=o.length?b(o):null,a=new Set(r?.relatedIds??[]);r&&t.push(F(r,n));const c=n.filter(s=>s!==r&&s.comparison.family!=="spatial-pattern"&&!a.has(s.id));let u=c.filter(s=>L.has(s.comparison.family));if(r&&u.length){const s=w(u);s.length>0&&s.every(f=>f.variable.kind==="yield"&&f.effect.direction==="higher")&&(u=[])}const i=new Set;if(u.length){const s=O(u);if(s){t.push(s);for(const l of L)i.add(l)}}else if(!r){const s=b(c);if(s){const l=s.comparison.family,f=j(l,c.filter(g=>g.comparison.family===l));f&&(t.push(f),i.add(l))}}if(t.length<3){const s=c.filter(l=>(l.comparison.family==="cluster"||l.comparison.family==="edge-arc")&&!i.has(l.comparison.family));if(s.length){const l=s[0].comparison.family,f=j(l,s.filter(g=>g.comparison.family===l));f&&t.push(f)}}return t.slice(0,3).join(" ")}
@@ -27,7 +27,38 @@ export interface HistogramSeriesData {
27
27
  * equal-width buckets. If `limitLow`/`limitHigh` are given, the axis range
28
28
  * is expanded to include them so limit lines always draw.
29
29
  */
30
- export declare function buildTestHistogramData(items: HistogramItem[], testNumber: number, bucketCount?: number, limitLow?: number, limitHigh?: number): HistogramBucket[];
30
+ /** Every finite recorded value for one test across `items`. Exported because a
31
+ * panel that wants to derive a robust fence needs the raw population, and
32
+ * re-walking dies in the chart layer would be a second copy of this loop. */
33
+ export declare function collectTestValues(items: HistogramItem[], testNumber: number): number[];
34
+ /**
35
+ * Min/max of one test's values across `items`, without building an array of
36
+ * them. Both are `NaN` when nothing was measured.
37
+ *
38
+ * A loop rather than `Math.min(...values)`: the spread passes every value as a
39
+ * separate ARGUMENT, and a lot of 25 wafers × ~10k dies overflows the argument
40
+ * limit and throws `RangeError: Maximum call stack size exceeded` — which in the
41
+ * chart layer surfaces as the whole Insights rebuild dying, not as a bad number.
42
+ */
43
+ export declare function testValueExtent(items: HistogramItem[], testNumber: number): {
44
+ min: number;
45
+ max: number;
46
+ };
47
+ export declare function buildTestHistogramData(items: HistogramItem[], testNumber: number, bucketCount?: number, limitLow?: number, limitHigh?: number,
48
+ /**
49
+ * Bound the bucket range and DROP values outside it. Unlike `limitLow`/
50
+ * `limitHigh`, which only ever widen the range, this narrows it — for the
51
+ * panel's "Clip outliers" axis control, where one wild reading otherwise
52
+ * compresses every real bucket into the first column.
53
+ *
54
+ * Affects this chart only. No statistic anywhere is computed from a clipped
55
+ * population: an out-of-spec die is a distribution outlier by construction, so
56
+ * excluding it from yield or capability would delete real failures.
57
+ */
58
+ clip?: {
59
+ lo: number;
60
+ hi: number;
61
+ }): HistogramBucket[];
31
62
  /**
32
63
  * Faceted histogram: one count-series per group over a *shared* set of
33
64
  * buckets, so the series overlay and compare directly. The bucket range
@@ -38,5 +69,19 @@ export declare function buildTestHistogramData(items: HistogramItem[], testNumbe
38
69
  export declare function buildTestHistogramSeries(groups: {
39
70
  key: string;
40
71
  items: HistogramItem[];
41
- }[], testNumber: number, bucketCount?: number, limitLow?: number, limitHigh?: number): HistogramSeriesData;
72
+ }[], testNumber: number, bucketCount?: number, limitLow?: number, limitHigh?: number,
73
+ /**
74
+ * Bound the bucket range and DROP values outside it — the faceted twin of
75
+ * `buildTestHistogramData`'s own `clip`, with the same contract: it narrows
76
+ * where the limits only widen, it is this chart's axis control and nothing
77
+ * else, and no statistic anywhere is computed from a clipped population.
78
+ *
79
+ * It exists because the panel's "Clip outliers" checkbox was rendered in the
80
+ * grouped view too and did nothing there — the series builder had no way to
81
+ * take a clip range, so the control silently applied to one branch only.
82
+ */
83
+ clip?: {
84
+ lo: number;
85
+ hi: number;
86
+ }): HistogramSeriesData;
42
87
  //# sourceMappingURL=histogram.d.ts.map