@wafertools/wafermap 0.30.3 → 0.30.4

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 (47) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +2 -2
  3. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  4. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +1 -1
  6. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/scatter.d.ts +5 -1
  9. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  13. package/dist/packages/canvas-adapter/insightsTab.d.ts +11 -1
  14. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  15. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  16. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  17. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  18. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  19. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  20. package/dist/packages/canvas-adapter/toolbar.js +1 -1
  21. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  22. package/dist/packages/canvas-adapter/userGuideHtml.js +38 -36
  23. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  24. package/dist/packages/canvas-adapter/version.js +1 -1
  25. package/dist/packages/canvas-adapter/warnings.js +1 -1
  26. package/dist/packages/core/dies.d.ts +1 -1
  27. package/dist/packages/renderer/binColors.js +1 -1
  28. package/dist/packages/renderer/buildView.js +1 -1
  29. package/dist/packages/renderer/buildWaferMap.d.ts +40 -12
  30. package/dist/packages/renderer/buildWaferMap.js +1 -1
  31. package/dist/packages/renderer/derivedTests/apply.js +1 -1
  32. package/dist/packages/renderer/derivedTests/parser.js +1 -1
  33. package/dist/packages/renderer/spec.d.ts +17 -0
  34. package/dist/packages/renderer/spec.js +1 -1
  35. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  36. package/dist/packages/stats/capability.d.ts +4 -2
  37. package/dist/packages/stats/capability.js +1 -1
  38. package/dist/packages/stats/correlation.d.ts +1 -1
  39. package/dist/packages/stats/deprecated.d.ts +1 -1
  40. package/dist/packages/stats/deprecated.js +1 -1
  41. package/dist/packages/stats/mergeTestDefs.d.ts +1 -1
  42. package/dist/packages/stats/mergeTestDefs.js +1 -1
  43. package/dist/packages/stats/renderSummaryReport.js +11 -11
  44. package/dist/packages/stats/sweep.d.ts +8 -0
  45. package/dist/packages/stats/sweep.js +1 -1
  46. package/dist/packages/stats/types.d.ts +9 -3
  47. package/package.json +1 -1
@@ -72,13 +72,13 @@ export const USER_GUIDE_HTML=`<div class="wmap-guide">
72
72
  .wmap-guide-toc{display:none!important}
73
73
  }
74
74
  </style>
75
- <h1 id="wafer-map-user-guide">Wafer Map \u2014 User Guide<span class="wmap-guide-version" title="Built 2026-09-23T17:22:32.765Z">v0.30.3</span></h1>
75
+ <h1 id="wafer-map-user-guide">Wafer Map \u2014 User Guide<span class="wmap-guide-version" title="Built 2026-09-25T05:58:15.630Z">v0.30.4</span></h1>
76
76
  <p class="wmap-guide-online-link">This is a quick reference. <a href="https://wafertools.github.io/wafermap/user-guide/" target="_blank" rel="noopener">View the full illustrated guide online \u2197</a></p>
77
77
  <p>This guide describes the display and analysis features of the wafer map viewer.
78
78
  It is written for users who may be semiconductor test engineers, device engineers, and yield engineers
79
79
  or anyone else who may use a wafer map application \u2014 not for developers integrating the wafermap library.</p>
80
80
  <p>Some features depend on what data the application has loaded (bin names, test
81
- definitions, spec limits, reticle geometry). Where this applies it is noted.</p>
81
+ definitions, test limits, reticle geometry). Where this applies it is noted.</p>
82
82
  <p><strong>Getting oriented.</strong> The wafer map is an interactive viewer. A toolbar is always
83
83
  present at the top-right of the map \u2014 use it to change what the colours represent,
84
84
  toggle overlays, rotate or flip the wafer, zoom, select dies, and open the summary
@@ -134,20 +134,20 @@ then failing bins from the most dies to the fewest \u2014 the same order as the
134
134
  panel and reports.</p>
135
135
  <p><strong>Value mode</strong> shows a continuous colorbar: the colour scale runs from the minimum
136
136
  to maximum value, with units when available. The colorbar is informational only \u2014
137
- clicking it has no effect. To switch to a pass/fail view, use the <strong>Spec pass/fail</strong>
138
- or <strong>Test pass/fail</strong> option in the Overlays menu (available when spec limits are
137
+ clicking it has no effect. To switch to a pass/fail view, use the <strong>Limit pass/fail</strong>
138
+ or <strong>Test pass/fail</strong> option in the Overlays menu (available when test limits are
139
139
  defined, or a recorded verdict exists, for the active test).</p>
140
- <p><strong>Spec pass/fail mode</strong> replaces the colorbar with a small <strong>spec legend</strong>: Pass,
140
+ <p><strong>Limit pass/fail mode</strong> replaces the colorbar with a small legend: Pass,
141
141
  Fail high, and Fail low swatches (only the categories that apply to the test&#39;s
142
- limits) with a die count beside each. This judges dies against the test&#39;s spec
143
- limits (<code>limitLow</code> / <code>limitHigh</code>).</p>
142
+ limits) with a die count beside each. This judges dies against the test&#39;s lower
143
+ and upper test limits.</p>
144
144
  <p><strong>Test pass/fail mode</strong> replaces the colorbar with a <strong>Pass / Fail legend</strong> and die
145
145
  counts, coloured by the tester&#39;s own <strong>recorded</strong> verdict for that test \u2014 not a
146
- spec-limit judgement. A test with no measured value (a functional, go/no-go test)
146
+ limit judgement. A test with no measured value (a functional, go/no-go test)
147
147
  always displays this way; selecting it switches the map into Test pass/fail
148
148
  automatically, since there is nothing to plot on a gradient.</p>
149
149
  <p>Every map also shows a short <strong>title</strong> by the colorbar or legend naming what is
150
- displayed \u2014 the test name (and number, in spec mode), the bin type, or the stacked
150
+ displayed \u2014 the test name (and number, in limit pass/fail mode), the bin type, or the stacked
151
151
  wafer count.</p>
152
152
  <p>Clicking a bin swatch in the legend filters the display to that bin
153
153
  (see <a href="#3-toolbar-controls" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'3-toolbar-controls\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Highlight bin</a>).</p>
@@ -220,33 +220,33 @@ application has loaded.</p>
220
220
  <p>Stacked modes are only available on lot-stack maps (multiple wafers combined into
221
221
  one display). The panel identifies how many wafers were stacked and which
222
222
  aggregation method is active.</p>
223
- <h3 id="test-value-mode-colorbar-range-and-spec-limits">Test Value mode \u2014 colorbar range and spec limits</h3>
224
- <p>When spec limits are defined for the active test, two additional display options
223
+ <h3 id="test-value-mode-colorbar-range-and-test-limits">Test Value mode \u2014 colorbar range and test limits</h3>
224
+ <p>When test limits are defined for the active test, two additional display options
225
225
  become available:</p>
226
226
  <ul>
227
- <li><strong>Colorbar range \u2014 Data / Spec</strong>: switches the colorbar scale between the
228
- actual data extent and the spec limit bounds. This affects only the colorbar&#39;s
229
- range, never how out-of-spec dies are shown. In <strong>both</strong> ranges every die is
230
- coloured by the gradient so you can read the value distribution, and out-of-spec
231
- dies are marked with a triangle \u2014 pointing <strong>down (\u25BD) for below LSL</strong>, <strong>up (\u25B3)
232
- for above USL</strong> \u2014 so they stand out without leaving the distribution. The
227
+ <li><strong>Colorbar range \u2014 Data / Limits</strong>: switches the colorbar scale between the
228
+ actual data extent and the test limits. This affects only the colorbar&#39;s
229
+ range, never how failing dies are shown. In <strong>both</strong> ranges every die is
230
+ coloured by the gradient so you can read the value distribution, and dies
231
+ outside the test limits are marked with a triangle \u2014 pointing <strong>down (\u25BD) for below the low limit</strong>, <strong>up (\u25B3)
232
+ for above the high limit</strong> \u2014 so they stand out without leaving the distribution. The
233
233
  triangle is drawn black or white per die for contrast against its own colour, so
234
234
  it stays visible under any colour scheme, and its shape (not colour) carries the
235
235
  below/above-limit meaning.</li>
236
- <li><strong>Spec pass/fail</strong>: when active, dies within spec are shown in a pass colour
237
- and dies outside spec are highlighted \u2014 <strong>blue for below the Lower Spec Limit
238
- (LSL)</strong>, <strong>red for above the Upper Spec Limit (USL)</strong>. Both flags apply
239
- independently; a die can be flagged on either or both limits. A spec legend
236
+ <li><strong>Limit pass/fail</strong>: when active, dies within the test limits are shown in
237
+ a pass colour and dies outside them are highlighted \u2014 <strong>blue for below the
238
+ low limit</strong>, <strong>red for above the high limit</strong>. Both flags apply
239
+ independently; a die can be flagged on either or both limits. A Pass/Fail legend
240
240
  replaces the colorbar, listing each applicable category with its die count.</li>
241
241
  </ul>
242
242
  <p>Some tests have no measured value at all \u2014 a continuity check or any other
243
243
  go/no-go test, where the only result is a recorded pass or fail. Selecting one
244
244
  of these <strong>functional tests</strong> as the active test switches the map into
245
245
  <strong>Test pass/fail</strong> automatically: a Pass/Fail legend by die count, coloured by
246
- the tester&#39;s own recorded verdict rather than a spec-limit judgement (there is
246
+ the tester&#39;s own recorded verdict rather than a limit judgement (there is
247
247
  no gradient to fall back to). <strong>Test pass/fail</strong> is also available as an
248
248
  Overlays option on an ordinary parametric test when it carries recorded
249
- verdicts, as an alternative to spec-limit judgement.</p>
249
+ verdicts, as an alternative to limit judgement.</p>
250
250
  <div data-wmap-demo="value-heatmap" class="wmap-demo"></div><div data-wmap-demo="spec-passfail" class="wmap-demo"></div><hr>
251
251
  <h3 id="derived-tests">Derived tests (\u2020)</h3>
252
252
  <p>A test name marked <strong>\u2020</strong> in front is a <em>derived</em> test: its value was calculated
@@ -284,7 +284,7 @@ shows die tooltips \u2014 a separate thing from the toolbar, which is always pre
284
284
  <tr>
285
285
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yIDEyczMuNi03IDEwLTcgMTAgNyAxMCA3LTMuNiA3LTEwIDctMTAtNy0xMC03eiIvPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjMiLz48L3N2Zz4K" width="20" height="20"></td>
286
286
  <td>Overlays</td>
287
- <td>Check-menu of optional display layers: XY axis indicator, ring boundaries, quadrant lines, die coordinate labels, reticle grid (when geometry is configured), Spec pass/fail (Test Value mode with limits), Test pass/fail (Test Value mode, active test is functional or has recorded verdicts), and <strong>Mark failing dies</strong> (bin modes only). <strong>Clear overlays</strong> at the foot turns off everything in the menu at once, and is greyed when nothing is on \u2014 so the menu also answers &quot;is anything active?&quot; without you auditing every row.</td>
287
+ <td>Check-menu of optional display layers: XY axis indicator, ring boundaries, quadrant lines, die coordinate labels, reticle grid (when geometry is configured), Limit pass/fail (Test Value mode with limits), Test pass/fail (Test Value mode, active test is functional or has recorded verdicts), and <strong>Mark failing dies</strong> (bin modes only). <strong>Clear overlays</strong> at the foot turns off everything in the menu at once, and is greyed when nothing is on \u2014 so the menu also answers &quot;is anything active?&quot; without you auditing every row.</td>
288
288
  </tr>
289
289
  <tr>
290
290
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Im0xNC42MjIgMTcuODk3LTEwLjY4LTIuOTEzIi8+PHBhdGggZD0iTTE4LjM3NiAyLjYyMmExIDEgMCAxIDEgMy4wMDIgMy4wMDJMMTcuMzYgOS42NDNhLjUuNSAwIDAgMCAwIC43MDdsLjk0NC45NDRhMi40MSAyLjQxIDAgMCAxIDAgMy40MDhsLS45NDQuOTQ0YS41LjUgMCAwIDEtLjcwNyAwTDguMzU0IDcuMzQ4YS41LjUgMCAwIDEgMC0uNzA3bC45NDQtLjk0NGEyLjQxIDIuNDEgMCAwIDEgMy40MDggMGwuOTQ0Ljk0NGEuNS41IDAgMCAwIC43MDcgMHoiLz48cGF0aCBkPSJNOSA4Yy0xLjgwNCAyLjcxLTMuOTcgMy40Ni02LjU4MyAzLjk0OGEuNTA3LjUwNyAwIDAgMC0uMzAyLjgxOWw3LjMyIDguODgzYTEgMSAwIDAgMCAxLjE4NS4yMDRDMTIuNzM1IDIwLjQwNSAxNiAxNi43OTIgMTYgMTUiLz48L3N2Zz4K" width="20" height="20"></td>
@@ -299,7 +299,7 @@ shows die tooltips \u2014 a separate thing from the toolbar, which is always pre
299
299
  <tr>
300
300
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik01IDQgTDUgMjAiLz48cGF0aCBkPSJNNSA0IEw4IDQiLz48cGF0aCBkPSJNNSAyMCBMOCAyMCIvPjxwYXRoIGQ9Ik0xOSA0IEwxOSAyMCIvPjxwYXRoIGQ9Ik0xOSA0IEwxNiA0Ii8+PHBhdGggZD0iTTE5IDIwIEwxNiAyMCIvPjxwYXRoIGQ9Ik01IDEyIEwxOSAxMiIgc3Ryb2tlLWRhc2hhcnJheT0iMyAyIi8+PC9zdmc+Cg==" width="20" height="20"></td>
301
301
  <td>Colorbar range</td>
302
- <td>Test Value mode with spec limits only. Toggles the colorbar between the <strong>spec-limit range</strong> (default \u2014 the colours mean the same thing on every wafer, so maps are comparable) and the <strong>data range</strong> (stretches the scale to the values actually present, which shows more contrast but is not comparable between wafers).</td>
302
+ <td>Test Value mode with test limits only. Toggles the colorbar between the <strong>limit range</strong> (default \u2014 the colours mean the same thing on every wafer, so maps are comparable) and the <strong>data range</strong> (stretches the scale to the values actually present, which shows more contrast but is not comparable between wafers).</td>
303
303
  </tr>
304
304
  <tr>
305
305
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjciIHg9IjMiIHk9IjMiIHJ4PSIxIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iNyIgeD0iMyIgeT0iMTQiIHJ4PSIxIi8+PHBhdGggZD0iTTE0IDRoNyIvPjxwYXRoIGQ9Ik0xNCA5aDciLz48cGF0aCBkPSJNMTQgMTVoNyIvPjxwYXRoIGQ9Ik0xNCAyMGg3Ii8+PC9zdmc+Cg==" width="20" height="20"></td>
@@ -404,7 +404,7 @@ highlighting one bin at a time.</p>
404
404
  <li><strong>Quadrant lines</strong> \u2014 divides the wafer into N, S, E, W quadrants</li>
405
405
  <li><strong>Die coordinate labels</strong> \u2014 draws the (x, y) grid position inside each die (useful at high zoom)</li>
406
406
  <li><strong>Reticle grid</strong> \u2014 stepper field grid (only shown when reticle geometry is configured)</li>
407
- <li><strong>Spec pass/fail</strong> \u2014 pass/fail colouring for Test Value mode, judged against spec limits, when the active test defines them</li>
407
+ <li><strong>Limit pass/fail</strong> \u2014 pass/fail colouring for Test Value mode, judged against the test limits, when the active test defines them</li>
408
408
  <li><strong>Test pass/fail</strong> \u2014 pass/fail colouring for Test Value mode, coloured by the tester&#39;s recorded verdict; always on for a functional (no measured value) active test</li>
409
409
  </ul>
410
410
  <div data-wmap-demo="overlays" class="wmap-demo"></div><p><em>Ring boundaries, quadrant lines, and XY indicator all active.</em></p>
@@ -515,7 +515,7 @@ highlighting one bin at a time.</p>
515
515
  <tr>
516
516
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik01IDQgTDUgMjAiLz48cGF0aCBkPSJNNSA0IEw4IDQiLz48cGF0aCBkPSJNNSAyMCBMOCAyMCIvPjxwYXRoIGQ9Ik0xOSA0IEwxOSAyMCIvPjxwYXRoIGQ9Ik0xOSA0IEwxNiA0Ii8+PHBhdGggZD0iTTE5IDIwIEwxNiAyMCIvPjxwYXRoIGQ9Ik01IDEyIEwxOSAxMiIgc3Ryb2tlLWRhc2hhcnJheT0iMyAyIi8+PC9zdmc+Cg==" width="20" height="20"></td>
517
517
  <td>Colorbar range</td>
518
- <td>Test Value mode with spec limits only. Toggles all cards between the spec-limit range and the data range. Leave it on spec limits when comparing wafers \u2014 the data range rescales per view.</td>
518
+ <td>Test Value mode with test limits only. Toggles all cards between the limit range and the data range. Leave it on the test limits when comparing wafers \u2014 the data range rescales per view.</td>
519
519
  </tr>
520
520
  <tr>
521
521
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNOCAydjQiLz48cGF0aCBkPSJNMTIgMnY0Ii8+PHBhdGggZD0iTTE2IDJ2NCIvPjxyZWN0IHdpZHRoPSIxNiIgaGVpZ2h0PSIxOCIgeD0iNCIgeT0iNCIgcng9IjIiLz48cGF0aCBkPSJNOCAxMGg2Ii8+PHBhdGggZD0iTTggMTRoOCIvPjxwYXRoIGQ9Ik04IDE4aDUiLz48L3N2Zz4K" width="20" height="20"></td>
@@ -556,7 +556,7 @@ The rest depends on the active plot mode:</p>
556
556
  <li><strong>Bin modes</strong> \u2014 the die&#39;s bin verdict (number and name, if named), plus a
557
557
  note of how many test values are recorded for the die</li>
558
558
  <li><strong>Test Value mode</strong> \u2014 the active test&#39;s value in bold, flagged when it is
559
- out of spec, with the remaining tests summarised as &quot;+N more tests&quot;</li>
559
+ outside the test limits, with the remaining tests summarised as &quot;+N more tests&quot;</li>
560
560
  <li><strong>Stacked modes</strong> \u2014 the single aggregated value or count at that position</li>
561
561
  </ul>
562
562
  <h3 id="42-zoom-and-pan">4.2 Zoom and pan</h3>
@@ -594,7 +594,8 @@ wafer otherwise.</p>
594
594
  <ul>
595
595
  <li><strong>Value histogram</strong> \u2014 one test&#39;s distribution, opening on the test the map
596
596
  is showing.</li>
597
- <li><strong>Process capability</strong> \u2014 every test normalised to its spec limits, with its
597
+ <li><strong>Process capability</strong> \u2014 every test normalised to its spec limits, or its
598
+ test limits when it has none, with its
598
599
  Ppk. Below 30 dies the chart says each Ppk is a rough estimate: a Ppk from a
599
600
  handful of dies can be far from the process&#39;s real capability.</li>
600
601
  <li><strong>Sweeps</strong> your application has defined (see <a href="#sweep-cards" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'sweep-cards\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Sweep cards</a>).</li>
@@ -712,10 +713,10 @@ with a significance test behind it.</li>
712
713
  marked. Wafers are in slot order by default, since that is what makes a
713
714
  slot-correlated pattern visible; a <strong>Slot / Yield</strong> selector re-sorts. Wafers
714
715
  far below the rest are labelled &quot;low outlier&quot;.</li>
715
- <li><strong>Test values</strong> \u2014 per test: mean, <strong>Ppk</strong>, and spec yield. Ppk (not Cpk)
716
+ <li><strong>Test values</strong> \u2014 per test: mean, <strong>Ppk</strong>, and limit yield. Ppk (not Cpk)
716
717
  because it measures against the <em>overall</em> spread, including wafer-to-wafer
717
718
  variation, which is what the dies actually ship against. The full descriptive
718
- statistics \u2014 min, quartiles, median, max, \u03C3, both spec limits, and all four
719
+ statistics \u2014 min, quartiles, median, max, \u03C3, both test limits, and all four
719
720
  capability indices \u2014 are in the summary report and the CSV export.</li>
720
721
  <li><strong>Functional tests</strong> \u2014 pass/fail counts and pass rate per functional test.</li>
721
722
  </ul>
@@ -846,11 +847,12 @@ actually in use and marked with the median, a hard/soft bin pareto, and
846
847
  ring/quadrant regional yield plus the full per-test statistics table.</p>
847
848
  </li>
848
849
  <li><p><strong>Distributions</strong> \u2014 process capability (Cp/Cpk/Pp/Ppk for tests with both a
849
- lower and upper spec limit; tests missing a spec still appear, normalized
850
+ lower and upper limit \u2014 spec limits where given, otherwise test limits; tests
851
+ without both still appear, normalized
850
852
  onto their own range and sorted by variability), a test-value box plot, a
851
853
  value histogram, and a <strong>wafer-to-wafer trend</strong> \u2014 one point per wafer at its
852
854
  mean, \xB11\u03C3 whiskers, the mean of all wafers dashed across (labelled the &quot;lot
853
- mean&quot; when they all come from one lot), and spec limits where the
855
+ mean&quot; when they all come from one lot), and test limits where the
854
856
  test has them. The trend is always in slot order, deliberately: drift across
855
857
  a cassette only reads in the physical sequence, so there is no sort control
856
858
  to destroy it. Clicking a capability box drives the box plot, histogram and
@@ -858,9 +860,9 @@ trend onto that same test automatically.</p>
858
860
  <p>The box plot, histogram and trend share one row of axis controls, kept in
859
861
  sync across all three so switching between them never re-reads the same
860
862
  data on a different scale: <strong>Axis includes limits</strong> widens the axis to cover
861
- LSL/USL even where the data sits well clear of them (on by default only when
863
+ the low and high limits even where the data sits well clear of them (on by default only when
862
864
  doing so still leaves the data at least a third of the axis \u2014 otherwise a
863
- generous spec window would squash a perfectly capable distribution into a
865
+ generous limit window would squash a perfectly capable distribution into a
864
866
  sliver), and <strong>Clip outliers</strong> narrows the axis to a robust range (the
865
867
  Tukey fence, 1.5\xD7 IQR beyond Q1/Q3) so a handful of extreme values don&#39;t
866
868
  stretch the axis until the rest of the distribution reads as a flat line.
@@ -896,7 +898,7 @@ yield/box-plot rows.</em></p>
896
898
  open that wafer&#39;s map), hard/soft bin pareto, ring and quadrant yield, and
897
899
  pooled per-test statistics. The &quot;\u2039 Gallery&quot; tab returns to the card grid.</em></p>
898
900
  <p><em>Distributions sub-tab: process capability (coloured by Ppk \u2014 green capable,
899
- orange marginal, red poor; tests without spec limits are muted and dashed), a
901
+ orange marginal, red poor; tests without limits are muted and dashed), a
900
902
  per-wafer test-value box plot, and a value histogram. Clicking a capability
901
903
  box drives the box plot and histogram onto that test.</em></p>
902
904
  <p><em>Correlation sub-tab: test-to-test Pearson correlation matrix (blue =
@@ -1,3 +1,3 @@
1
- export declare const WMAP_VERSION = "0.30.3";
2
- export declare const WMAP_BUILD_TIME = "2026-09-23T17:24:44.719Z";
1
+ export declare const WMAP_VERSION = "0.30.4";
2
+ export declare const WMAP_BUILD_TIME = "2026-09-25T05:59:59.978Z";
3
3
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- export const WMAP_VERSION="0.30.3",WMAP_BUILD_TIME="2026-09-23T17:24:44.719Z";
1
+ export const WMAP_VERSION="0.30.4",WMAP_BUILD_TIME="2026-09-25T05:59:59.978Z";
@@ -1 +1 @@
1
- import{SHADOW as S,TRACKING as O,SPACE as s,RADIUS as v,FONT as y,CLR as d,Z_BASE as B}from"./toolbar.js";const E={error:0,warning:1,info:2};export function severityOf(t){return t.severity??"warning"}export function collectWarnings(t){const o=[],i=new Set,n=e=>{if(!e?.message)return;const r=`${e.code}\0${e.message}`;i.has(r)||(i.add(r),o.push(e))};for(const e of t.result?.warnings??[])n(e);for(const e of t.statsSummary?.stats.warnings??[])n(e);for(const e of t.lotStatsSummary?.perWafer??[])for(const r of e.summary.stats.warnings??[])n(r);for(const e of t.extra??[])n(e);return o.sort((e,r)=>E[severityOf(e)]-E[severityOf(r)])}function $(t){return t==="error"?{bg:d.errBg,border:d.errBorder,text:d.errText}:{bg:d.warnBg,border:d.warnBorder,text:d.warnText}}function w(t){return t==="error"?"\u26D4":t==="info"?"\u2139":"\u26A0"}export const SHORT_LABEL={"partial-coverage":"Partial wafer coverage","geometry-conflict":"Geometry conflict","non-standard-diameter":"Non-standard wafer diameter","diameter-exceeds-die-extent":"Diameter exceeds die extent","edge-exclusion-exceeds-radius":"Edge exclusion exceeds radius","test-count-capped":"Test analysis skipped","test-def-collision":"Test definitions disagree","analysis-option-corrected":"Analysis option corrected","bin-colors-shared":"Bins share a colour","pass-bins-mixed":"Pass bins differ by wafer","ring-count-mixed":"Ring counts differ by wafer","input-field-removed":"Removed input name ignored","input-values-not-numbers":"Bins or test values are text","derived-test-invalid":"Derived test not applied"};function C(t){const o=t.code?SHORT_LABEL[t.code]:void 0;if(o)return o;const i=t.message.split(/(?<=\.)\s/)[0]??t.message;return i.length>60?`${i.slice(0,57)}\u2026`:i}export function buildWarningsBanner(t,o=document){const i=o.createElement("div");Object.assign(i.style,{marginBottom:s.lg});for(const n of t){const e=severityOf(n),r=$(e),m=o.createElement("div");Object.assign(m.style,{background:r.bg,border:`1px solid ${r.border}`,borderRadius:v.control,marginBottom:s.sm,fontSize:y.body,color:r.text,lineHeight:"1.5"}),m.setAttribute("role","status");const a=o.createElement("button");a.type="button",Object.assign(a.style,{display:"flex",alignItems:"center",gap:s.sm,width:"100%",background:"none",border:"none",font:"inherit",color:"inherit",textAlign:"left",padding:`${s.sm} ${s.lg}`,cursor:"pointer"});const p=o.createElement("span");Object.assign(p.style,{flex:"1",minWidth:"0"}),p.textContent=`${w(e)} ${C(n)}`;const l=o.createElement("span");Object.assign(l.style,{flexShrink:"0"}),a.append(p,l);const g=o.createElement("div");Object.assign(g.style,{padding:`0 ${s.lg} ${s.sm}`}),g.textContent=n.message;let c=!1;const f=()=>{l.textContent=c?"\u25B4":"\u25BE",g.style.display=c?"block":"none",a.setAttribute("aria-expanded",String(c)),a.setAttribute("aria-label",`${C(n)}. ${c?"Click to collapse.":"Click to expand for detail."}`)};f(),a.addEventListener("click",()=>{c=!c,f()}),a.addEventListener("mouseenter",()=>{l.style.opacity="0.65"}),a.addEventListener("mouseleave",()=>{l.style.opacity="1"}),m.append(a,g),i.appendChild(m)}return i}export function buildWarningsMenuEl(t,o,i=window){const n=i.document,e=n.createElement("div"),r=280,a=t.left+r<=(i.innerWidth??1/0)?t.left:Math.max(4,t.right-r);Object.assign(e.style,{position:"fixed",top:`${t.bottom+4}px`,left:`${a}px`,background:d.menuBg,border:`1px solid ${d.menuBorder}`,borderRadius:v.control,boxShadow:S.menu,zIndex:B,width:`${r}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"}),e.setAttribute("role","dialog"),e.setAttribute("aria-label","Data warnings");const p=n.createElement("div");Object.assign(p.style,{padding:`${s.sm} ${s.xl} ${s.xs}`,fontSize:y.body,fontWeight:"700",letterSpacing:O,textTransform:"uppercase",color:d.label}),p.textContent=o.length===1?"1 warning":`${o.length} warnings`,e.appendChild(p);for(const l of o){const g=severityOf(l),c=$(g),f=n.createElement("div");Object.assign(f.style,{display:"flex",gap:s.md,padding:`${s.md} ${s.xl}`,fontSize:y.body,lineHeight:"1.5",color:d.text,borderTop:`1px solid ${d.menuBorder}`,alignItems:"flex-start"});const x=n.createElement("span");Object.assign(x.style,{color:c.text,flexShrink:"0"}),x.textContent=w(g),f.appendChild(x);const u=n.createElement("div"),b=n.createElement("div");Object.assign(b.style,{fontWeight:"700",color:c.text,marginBottom:s.xxs}),b.textContent=l.code,u.appendChild(b);const h=n.createElement("div");h.textContent=l.message,u.appendChild(h),f.appendChild(u),e.appendChild(f)}return e}
1
+ import{SHADOW as C,TRACKING as O,SPACE as r,RADIUS as v,FONT as y,CLR as d,Z_BASE as B}from"./toolbar.js";const E={error:0,warning:1,info:2};export function severityOf(t){return t.severity??"warning"}export function collectWarnings(t){const s=[],i=new Set,n=e=>{if(!e?.message)return;const o=`${e.code}\0${e.message}`;i.has(o)||(i.add(o),s.push(e))};for(const e of t.result?.warnings??[])n(e);for(const e of t.statsSummary?.stats.warnings??[])n(e);for(const e of t.lotStatsSummary?.perWafer??[])for(const o of e.summary.stats.warnings??[])n(o);for(const e of t.extra??[])n(e);return s.sort((e,o)=>E[severityOf(e)]-E[severityOf(o)])}function $(t){return t==="error"?{bg:d.errBg,border:d.errBorder,text:d.errText}:{bg:d.warnBg,border:d.warnBorder,text:d.warnText}}function S(t){return t==="error"?"\u26D4":t==="info"?"\u2139":"\u26A0"}export const SHORT_LABEL={"partial-coverage":"Partial wafer coverage","geometry-conflict":"Geometry conflict","non-standard-diameter":"Non-standard wafer diameter","diameter-exceeds-die-extent":"Diameter exceeds die extent","edge-exclusion-exceeds-radius":"Edge exclusion exceeds radius","test-count-capped":"Test analysis skipped","test-def-collision":"Test definitions disagree","analysis-option-corrected":"Analysis option corrected","bin-colors-shared":"Bins share a colour","pass-bins-mixed":"Pass bins differ by wafer","ring-count-mixed":"Ring counts differ by wafer","input-field-removed":"Removed input name ignored","input-values-not-numbers":"Bins or test values are text","input-values-outside-stdf":"Values outside STDF ranges","retests-by-part-id":"Retests matched by part ID","derived-test-invalid":"Derived test not applied"};function w(t){const s=t.code?SHORT_LABEL[t.code]:void 0;if(s)return s;const i=t.message.split(/(?<=\.)\s/)[0]??t.message;return i.length>60?`${i.slice(0,57)}\u2026`:i}export function buildWarningsBanner(t,s=document){const i=s.createElement("div");Object.assign(i.style,{marginBottom:r.lg});for(const n of t){const e=severityOf(n),o=$(e),m=s.createElement("div");Object.assign(m.style,{background:o.bg,border:`1px solid ${o.border}`,borderRadius:v.control,marginBottom:r.sm,fontSize:y.body,color:o.text,lineHeight:"1.5"}),m.setAttribute("role","status");const a=s.createElement("button");a.type="button",Object.assign(a.style,{display:"flex",alignItems:"center",gap:r.sm,width:"100%",background:"none",border:"none",font:"inherit",color:"inherit",textAlign:"left",padding:`${r.sm} ${r.lg}`,cursor:"pointer"});const p=s.createElement("span");Object.assign(p.style,{flex:"1",minWidth:"0"}),p.textContent=`${S(e)} ${w(n)}`;const l=s.createElement("span");Object.assign(l.style,{flexShrink:"0"}),a.append(p,l);const g=s.createElement("div");Object.assign(g.style,{padding:`0 ${r.lg} ${r.sm}`}),g.textContent=n.message;let c=!1;const f=()=>{l.textContent=c?"\u25B4":"\u25BE",g.style.display=c?"block":"none",a.setAttribute("aria-expanded",String(c)),a.setAttribute("aria-label",`${w(n)}. ${c?"Click to collapse.":"Click to expand for detail."}`)};f(),a.addEventListener("click",()=>{c=!c,f()}),a.addEventListener("mouseenter",()=>{l.style.opacity="0.65"}),a.addEventListener("mouseleave",()=>{l.style.opacity="1"}),m.append(a,g),i.appendChild(m)}return i}export function buildWarningsMenuEl(t,s,i=window){const n=i.document,e=n.createElement("div"),o=280,a=t.left+o<=(i.innerWidth??1/0)?t.left:Math.max(4,t.right-o);Object.assign(e.style,{position:"fixed",top:`${t.bottom+4}px`,left:`${a}px`,background:d.menuBg,border:`1px solid ${d.menuBorder}`,borderRadius:v.control,boxShadow:C.menu,zIndex:B,width:`${o}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"}),e.setAttribute("role","dialog"),e.setAttribute("aria-label","Data warnings");const p=n.createElement("div");Object.assign(p.style,{padding:`${r.sm} ${r.xl} ${r.xs}`,fontSize:y.body,fontWeight:"700",letterSpacing:O,textTransform:"uppercase",color:d.label}),p.textContent=s.length===1?"1 warning":`${s.length} warnings`,e.appendChild(p);for(const l of s){const g=severityOf(l),c=$(g),f=n.createElement("div");Object.assign(f.style,{display:"flex",gap:r.md,padding:`${r.md} ${r.xl}`,fontSize:y.body,lineHeight:"1.5",color:d.text,borderTop:`1px solid ${d.menuBorder}`,alignItems:"flex-start"});const u=n.createElement("span");Object.assign(u.style,{color:c.text,flexShrink:"0"}),u.textContent=S(g),f.appendChild(u);const x=n.createElement("div"),b=n.createElement("div");Object.assign(b.style,{fontWeight:"700",color:c.text,marginBottom:r.xxs}),b.textContent=l.code,x.appendChild(b);const h=n.createElement("div");h.textContent=l.message,x.appendChild(h),f.appendChild(x),e.appendChild(f)}return e}
@@ -51,7 +51,7 @@ export interface Die {
51
51
  /** STDF site_num — which parallel test site tested this die. Only meaningful when > 1 distinct value appears per wafer. */
52
52
  siteNum?: number;
53
53
  /** STDF pir.part_id — tester-assigned identifier for this tested unit. Encodes probe sequence at most fabs. */
54
- partId?: number;
54
+ partId?: number | string;
55
55
  }
56
56
  export interface DieEligibilityOptions {
57
57
  /** Include dies that straddle the wafer boundary (`die.partial`). Default: excluded. */
@@ -1 +1 @@
1
- import{diePassStatus as w}from"../core/dies.js";import{INPUT_DEFAULT_PASS_BINS as S,itemPassBins as x}from"../core/passBins.js";import{getBinColorScheme as y}from"./colorSchemes.js";import{arrayEqual as u,mapEqual as p,setEqual as b}from"../core/utils.js";export function resolveBinColors(s,n={}){return resolveBinColorsByWafer([{dies:s,passBins:n.passBins}],n).colors}export function binColorsForMaps(s,n={}){return resolveBinColorsForMaps(Array.isArray(s)?s:[s],n).colors}export function resolveBinColorsForMaps(s,n={}){return resolveBinColorsByWafer(s.map(e=>({dies:e.dies,passBins:x(e)})),{...n,hbinDefs:mergeBinDefs(s.map(e=>e.hbinDefs)),sbinDefs:mergeBinDefs(s.map(e=>e.sbinDefs))})}export function mergeBinDefs(s){const n=new Set,e=[];for(const a of s)for(const r of a??[])n.has(r.bin)||(n.add(r.bin),e.push(r));return e}export function resolveBinColorsByWafer(s,n={}){const e=y(n.binColorScheme),a=n.useDefinedBinColors??!0,r=d(s),f=B(r.hardBins,i=>r.pass.hard.has(i),e,a?n.hbinDefs:void 0,!1),t=B(r.softBins,i=>r.pass.soft.has(i),e,a?n.sbinDefs:void 0,!0);return{colors:{hard:f.colors,soft:t.colors,shared:{hard:f.shared,soft:t.shared},pass:r.pass},mixedHardBins:r.mixedHardBins}}export function binPassSets(s,n){return d([{dies:s,passBins:n}]).pass}export function binPassSetsByWafer(s){return d(s).pass}function d(s){const n=new Map,e=new Map,a=new Set;for(const t of s){const i=new Set(t.passBins??S);for(const l of t.dies){const o=w(l,i)===!0;l.hbin!=null&&(n.set(l.hbin,(n.get(l.hbin)??!0)&&o),o&&a.add(l.hbin)),l.sbin!=null&&e.set(l.sbin,(e.get(l.sbin)??!0)&&o)}}const r=t=>new Set([...t].filter(([,i])=>i).map(([i])=>i)),f={hard:r(n),soft:r(e)};return{hardBins:[...n.keys()],softBins:[...e.keys()],pass:f,mixedHardBins:[...a].filter(t=>!f.hard.has(t)).sort((t,i)=>t-i)}}function g(s){return s.length>8?`${s.slice(0,8).join(", ")} and ${s.length-8} more`:s.join(", ")}export function mixedPassBinsWarning(s){if(!s.length)return null;const n=s.length===1;return{code:"pass-bins-mixed",severity:"warning",message:`Hard ${n?"bin":"bins"} ${g(s)} ${n?"passes":"pass"} on some wafers and ${n?"fails":"fail"} on others: these wafers were built with different pass bins. Each wafer's own yield and die verdicts are correct, but a bin has one colour and one legend row, so ${n?"it is":"they are"} shown as failing there.`}}export function binColorsCover(s,n){if(!s.pass)return!1;for(const e of n)if(e.hbin!=null&&!s.hard.has(e.hbin)||e.sbin!=null&&!s.soft.has(e.sbin))return!1;return!0}export function binColorWarning(s,n){const e=n==="softBin"?"soft":n==="hardBin"?"hard":null;if(!e)return null;const a=s.shared[e];if(!a.length)return null;const r=g(a);return{code:"bin-colors-shared",severity:"warning",message:`${e==="hard"?"Hard":"Soft"} bins ${r} are drawn in a colour another bin also has, so colour alone cannot tell them apart. There are more bins than the colour scheme has distinct colours (or a bin definition repeats one). Use the legend, the tooltip, or highlight one bin at a time.`}}const C=1,P=2;function m(s,n,e,a){const r=s.length,f=a?Math.floor(r/2):0;return s[((n-e+f)%r+r)%r]}function B(s,n,e,a,r){const f=new Map;for(const o of a??[])o.color&&f.set(o.bin,o.color);const t=new Map;for(const o of s)t.set(o,f.get(o)??(n(o)?m(e.pass,o,C,r):m(e.fail,o,P,r)));const i=new Map;for(const[o,h]of t){const c=h.toLowerCase();i.set(c,[...i.get(c)??[],o])}const l=[...i.values()].filter(o=>o.length>1).flat().sort((o,h)=>o-h);return{colors:t,shared:l}}export function binColorsEqual(s,n){return s===n?!0:!s||!n?!1:p(s.hard,n.hard)&&p(s.soft,n.soft)&&u(s.shared.hard,n.shared.hard)&&u(s.shared.soft,n.shared.soft)&&b(s.pass.hard,n.pass.hard)&&b(s.pass.soft,n.pass.soft)}
1
+ import{diePassStatus as w}from"../core/dies.js";import{INPUT_DEFAULT_PASS_BINS as S,itemPassBins as x}from"../core/passBins.js";import{getBinColorScheme as y}from"./colorSchemes.js";import{arrayEqual as u,mapEqual as p,setEqual as b}from"../core/utils.js";export function resolveBinColors(s,n={}){return resolveBinColorsByWafer([{dies:s,passBins:n.passBins}],n).colors}export function binColorsForMaps(s,n={}){return resolveBinColorsForMaps(Array.isArray(s)?s:[s],n).colors}export function resolveBinColorsForMaps(s,n={}){return resolveBinColorsByWafer(s.map(e=>({dies:e.dies,passBins:x(e)})),{...n,hbinDefs:mergeBinDefs(s.map(e=>e.hbinDefs)),sbinDefs:mergeBinDefs(s.map(e=>e.sbinDefs))})}export function mergeBinDefs(s){const n=new Set,e=[];for(const a of s)for(const r of a??[])n.has(r.bin)||(n.add(r.bin),e.push(r));return e}export function resolveBinColorsByWafer(s,n={}){const e=y(n.binColorScheme),a=n.useDefinedBinColors??!0,r=d(s),f=B(r.hardBins,i=>r.pass.hard.has(i),e,a?n.hbinDefs:void 0,!1),t=B(r.softBins,i=>r.pass.soft.has(i),e,a?n.sbinDefs:void 0,!0);return{colors:{hard:f.colors,soft:t.colors,shared:{hard:f.shared,soft:t.shared},pass:r.pass},mixedHardBins:r.mixedHardBins}}export function binPassSets(s,n){return d([{dies:s,passBins:n}]).pass}export function binPassSetsByWafer(s){return d(s).pass}function d(s){const n=new Map,e=new Map,a=new Set;for(const t of s){const i=new Set(t.passBins??S);for(const l of t.dies){const o=w(l,i)===!0;l.hbin!=null&&(n.set(l.hbin,(n.get(l.hbin)??!0)&&o),o&&a.add(l.hbin)),l.sbin!=null&&e.set(l.sbin,(e.get(l.sbin)??!0)&&o)}}const r=t=>new Set([...t].filter(([,i])=>i).map(([i])=>i)),f={hard:r(n),soft:r(e)};return{hardBins:[...n.keys()],softBins:[...e.keys()],pass:f,mixedHardBins:[...a].filter(t=>!f.hard.has(t)).sort((t,i)=>t-i)}}function g(s){return s.length>8?`${s.slice(0,8).join(", ")} and ${s.length-8} more`:s.join(", ")}export function mixedPassBinsWarning(s){if(!s.length)return null;const n=s.length===1;return{code:"pass-bins-mixed",severity:"warning",message:`Hard ${n?"bin":"bins"} ${g(s)} ${n?"passes":"pass"} on some wafers and ${n?"fails":"fail"} on others: these wafers were built with different pass bins. Each wafer's own yield and die verdicts are correct, but a bin has one colour and one legend row, so ${n?"it is":"they are"} shown as failing there.`}}export function binColorsCover(s,n){if(!s.pass)return!1;for(const e of n)if(e.hbin!=null&&!s.hard.has(e.hbin)||e.sbin!=null&&!s.soft.has(e.sbin))return!1;return!0}export function binColorWarning(s,n){const e=n==="softBin"?"soft":n==="hardBin"?"hard":null;if(!e)return null;const a=s.shared[e];if(!a.length)return null;const r=g(a);return{code:"bin-colors-shared",severity:"warning",message:`${e==="hard"?"Hard":"Soft"} bins ${r} are drawn in a colour another bin also has, so colour alone cannot tell them apart. There are more bins than the colour scheme has distinct colours (or a bin definition repeats one). Use the legend, the tooltip, or highlight one bin at a time.`}}const C=1,P=2;function m(s,n,e,a){const r=s.length,f=a?Math.floor(r/2):0;return s[((Math.floor(n)-e+f)%r+r)%r]}function B(s,n,e,a,r){const f=new Map;for(const o of a??[])o.color&&f.set(o.bin,o.color);const t=new Map;for(const o of s)t.set(o,f.get(o)??(n(o)?m(e.pass,o,C,r):m(e.fail,o,P,r)));const i=new Map;for(const[o,h]of t){const c=h.toLowerCase();i.set(c,[...i.get(c)??[],o])}const l=[...i.values()].filter(o=>o.length>1).flat().sort((o,h)=>o-h);return{colors:t,shared:l}}export function binColorsEqual(s,n){return s===n?!0:!s||!n?!1:p(s.hard,n.hard)&&p(s.soft,n.soft)&&u(s.shared.hard,n.shared.hard)&&u(s.shared.soft,n.shared.soft)&&b(s.pass.hard,n.pass.hard)&&b(s.pass.soft,n.pass.soft)}
@@ -1 +1 @@
1
- import{hasPosition as de}from"../core/dies.js";import{getReticleCell as he}from"../core/reticle.js";import{metadataCategoricalValue as me,metadataDisplayValue as pe}from"../core/metadata.js";import{affineRotation as bt,affineMirror as Ht,affineCompose as st,affinePoint as G,affineVector as xt,affineSwapsAxes as ye}from"../core/transforms.js";import{contrastTextColor as Q,SPEC_PASS_FILL as Mt,SPEC_FAIL_LOW as ge,SPEC_FAIL_HIGH as St}from"./colorMap.js";import{resolveValueColorFn as be}from"./colorSchemes.js";import{resolveBinColors as xe,binColorsCover as Me}from"./binColors.js";import{NO_DATA_FILL as z}from"./colorMap.js";import{diePassStatus as Se}from"../core/dies.js";import{getDieTestValue as P,getTestPassStatus as U,isParametricTest as vt}from"./buildWaferMap.js";import{classifySpec as ot}from"./spec.js";export{ot as classifySpec};import{fmt as kt,fmtColorbarAxis as $t,fmtAggregationMethod as _t}from"./fmt.js";import{metadataValueColor as ve}from"./colorMap.js";import{clamp01 as Kt,compareNatural as ke,escHtml as L,minOf as $e}from"../core/utils.js";import{prettyKey as Wt}from"../core/utils.js";import{testLabel as rt,markedTestLabel as lt,derivedTestNote as jt,isDerivedTest as Be,DERIVED_MARK as Te,DERIVED_KEY as Ie}from"./testLabel.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function collectMetadataValues(t,e){const i=new Set;for(const a of t){if(a.partial||a.edgeExcluded)continue;const s=ct(a,e);s!==void 0&&i.add(s)}return[...i].sort(ke)}export function findTestDef(t,e){return t?.find(i=>i.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,i)=>e-i)}const De="#d3d6db",wt="#e8e9ea",Ve="#aab0ba";function ct(t,e){return e?me(t.metadata?.[e]):void 0}function Ce(t,e,i){const{x:a,y:s}=t.center,n=bt(t.orientation,a,s),o=st(Ht(e?.x??!1,e?.y??!1,a,s),bt(t.orientation,a,s)),l=st(Ht(i?.flipX??!1,i?.flipY??!1,a,s),bt(i?.rotation??0,a,s));return{bakedToScreen:l,physicalToScreen:st(l,n),gridToScreen:st(l,o)}}function q(t,e=!1){return{points:[t],closed:e}}function Fe(t,e){const{center:i,radius:a,notch:s}=t;let n=i.x+a*Math.cos(e),o=i.y+a*Math.sin(e);if(!s)return{x:n,y:o};const l=Math.sqrt(a**2-(s.length/2)**2),d=s.length/2,g=n-i.x,p=o-i.y;return s.type==="bottom"&&p<-l?(o=i.y-l,n=i.x+Math.max(-d,Math.min(d,g))):s.type==="top"&&p>l?(o=i.y+l,n=i.x+Math.max(-d,Math.min(d,g))):s.type==="left"&&g<-l?(n=i.x-l,o=i.y+Math.max(-d,Math.min(d,p))):s.type==="right"&&g>l&&(n=i.x+l,o=i.y+Math.max(-d,Math.min(d,p))),{x:n,y:o}}function zt(t,e){return t.map(e).join(" / ")}function Pe(t,e=256){const i=Array.from({length:e},(a,s)=>t(s/(e-1)));return a=>i[Math.min(e-1,Math.max(0,Math.round(a*(e-1))))]}function Le(t,e){const i=Math.max(1,Math.min(t.width,t.height)),a=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(i*.55,a*1.8))))}function Re(t,e,i){if(e?.length){const a=e.flatMap(s=>{const n=s.testNumber;if(!vt(s)){const l=U(t,n,s);return l===void 0?[]:[{key:n,label:lt(s,n),value:l?"Pass":"Fail"}]}const o=P(t,n);return o===void 0?[]:[{key:n,label:lt(s,n),value:kt(o,s.unit,i),recordedFail:t.testPass?.[n]===!1}]});if(a.length)return a}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([a,s])=>({key:Number(a),label:rt(void 0,Number(a)),value:kt(s,void 0,i)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([a,s])=>({key:Number(a),label:rt(void 0,Number(a)),value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,i={}){const{testDefs:a,hbinDefs:s,sbinDefs:n,fallbackFormat:o,aggrMethod:l,lotSize:d,activeTest:g,reticleConfig:p,metadataFields:B}=i,F=s?new Map(s.map(f=>[f.bin,f])):null,T=n?new Map(n.map(f=>[f.bin,f])):null,b=[L(`Die (${t.x}, ${t.y})`)];if(p&&de(t)){const f=he(t,p);b.push(L(`Reticle (${f.column}, ${f.row})`))}if(e==="stackedValues"){const f=P(t,0);if(f!==void 0){const u=a?.[0],h=u?lt(u,u.testNumber):"Value",v=l?` (${l})`:"";b.push(L(`${h}${v}: ${kt(f,u?.unit,o)}`));const m=jt(u);m&&b.push(`<i>${L(m)}</i>`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const f=P(t,0),u=e==="stackedSoftBins"?t.sbin:t.hbin;if(f!==void 0){const h=e==="stackedSoftBins"?T:F,v=u!==void 0?h?.get(u)?.name?`${u} \xB7 ${h.get(u).name}`:`Bin ${u}`:"Bin";let m;if(l==="percent")m=`${f.toFixed(0)}%`;else{const V=d?` (${(f/d*100).toFixed(0)}% of stacked wafers)`:"";m=`${f}${V}`}const x=l?` [${_t(l)}]`:"";b.push(L(`${v}: ${m}${x}`))}}else{const f=Re(t,a,o);if(e==="value"&&f.length){const{testNumber:u}=resolveTestNumber(g??0,a),h=findTestDef(a,u),v=P(t,u);let m=f.findIndex(A=>A.key===u);m<0&&(m=0);const x=f[m];let V=`<b>${L(`${x.label}: ${x.value}`)}${x.recordedFail?" <i>(recorded fail)</i>":""}</b>`;if(x.key===u&&h){const A=ot(v,h);(A==="failLow"||A==="failHigh")&&(V+=" <i>(out of spec)</i>")}b.push(V);const E=jt(findTestDef(a,x.key));E&&b.push(`<i>${L(E)}</i>`);const y=f.length-1;y>0&&b.push(`<i>+${y} more test${y===1?"":"s"}</i>`)}else if(f.length){const u=f.length;b.push(`<i>${u} test result${u===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const u=[];if(t.hbin!==void 0){const h=F?.get(t.hbin)?.name,v=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);u.push(`HBin: ${v}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,v=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);u.push(`SBin: ${v}`)}b.push(u.map(L).join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&b.push(L(`Retests: ${t.retestCount}`)),t.siteNum!==void 0&&b.push(L(`Site: ${t.siteNum}`)),t.partId!==void 0&&b.push(L(`Part ID: ${t.partId}`)),t.partial&&b.push("<i>partial die</i>"),t.probeIndex!==void 0&&b.push(L(`Probe: #${t.probeIndex}`));const $={...t.metadata??{}};for(const[f,u]of Object.entries($)){const h=pe(u);if(h===void 0)continue;const v=B?.find(m=>m.key===f)?.label??Wt(f);b.push(L(`${v}: ${h}`))}return b.join("<br>")}function Bt(t){return t?.name?lt(t,t.testNumber):void 0}function Tt(t){return Be(t)?{note:`${Te} ${Ie}`}:{}}export function buildMapTitle(t,e="engineering",i){const a=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=n=>{const o=i?.length===1?i[0]:void 0;return o===void 0?`${n} Bin`:o.name?`${n} Bin ${o.bin} \xB7 ${o.name}`:`${n} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:a};case"stackedSoftBins":return{primary:s("Soft"),secondary:a};case"metadata":{const n=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?Wt(t.activeMetadataKey):"Metadata";return{primary:n?.label??o,secondary:""}}case"stackedValues":{const n=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=$t(o,Bt(n),n?.unit,e),d=l||"Value",g=t.aggrMethod?` \xB7 ${_t(t.aggrMethod)}`:"";return{primary:`${d}${g}`,secondary:a,...Tt(n)}}default:{const n=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=$t(o,Bt(n),n?.unit,e),d=l||Bt(n);if(t.passFailDisplay!=="off"){const g=n?.testNumber??t.activeTest,p=d?`${d} \xB7 #${g}`:rt(void 0,g),B=t.passFailDisplay==="spec"?"Spec pass/fail":n!==void 0&&!vt(n)?"Functional pass/fail":"Tester pass/fail";return{primary:p,secondary:B,...Tt(n)}}return{primary:d??rt(void 0,t.activeTest),secondary:"",...Tt(n)}}}}export function generateTextOverlay(t,e,i){const{plotMode:a,colorFns:s,normalize:n,activeTest:o,valueRange:l,testDefs:d,fallbackFormat:g,passFailDisplay:p="off",activeMetadataKey:B,metadataColorMap:F,allIntegerValues:T}=i,b=findTestDef(d,o),{tickFmt:$}=$t(l[1],b?.name,b?.unit,g),f=T?h=>String(Math.round(h)):$,{testNumber:u}=resolveTestNumber(o,d);return t.flatMap((h,v)=>{let m="",x="#111111";if(a==="value"&&p==="test"){const y=U(h,u,b);if(y===void 0)return[];m=y?"P":"F",x=Q(y?Mt:St)}else if(a==="value"){const y=P(h,u);if(y===void 0)return[];m=zt([y],f),x=Q(s.forValue(n(y)))}else if(a==="hardBin"||a==="softBin"){const y=a==="softBin"?h.sbin:h.hbin;if(y===void 0)return[];m=String(y),x=Q(s.forBin(y))}else if(a==="metadata"){const y=ct(h,B);if(y===void 0)return[];m=y,x=Q(F?.get(m)??z)}else{const y=P(h,0);if(y===void 0)return[];m=zt([y],f),x=Q(s.forValue(n(y)))}const V=e?e[v*2]:h.physX,E=e?e[v*2+1]:h.physY;return[{x:V,y:E,text:m,fontSize:Le(h,m),color:x,align:"center"}]})}function qt(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function Ne(t,e,i=720){const{center:a,radius:s,notch:n}=t,o=$=>G(e,$.x,$.y);if(!(n!==void 0&&t.diameter>150)){const $=[];for(let f=0;f<=i;f++){const u=2*Math.PI*f/i;$.push(o(Fe(t,u)))}return[{kind:"wafer-boundary",...q($,!0),lineColor:"#888888",lineWidth:1}]}const d=1.25,g=n.length,p=qt(n.type),B=Math.atan2(g,s),F=s-d,T=[];let b=!1;for(let $=0;$<=i;$++){const f=2*Math.PI*$/i;let u=f-p;for(;u>Math.PI;)u-=2*Math.PI;for(;u<-Math.PI;)u+=2*Math.PI;if(Math.abs(u)<=B){if(!b){b=!0;const h={x:a.x+s*Math.cos(p-B),y:a.y+s*Math.sin(p-B)},v={x:a.x+F*Math.cos(p),y:a.y+F*Math.sin(p)},m={x:a.x+s*Math.cos(p+B),y:a.y+s*Math.sin(p+B)};T.push(o(h),o(v),o(m))}continue}T.push(o({x:a.x+s*Math.cos(f),y:a.y+s*Math.sin(f)}))}return[{kind:"wafer-boundary",...q(T,!0),lineColor:"#888888",lineWidth:1}]}function Oe(t,e,i,a=360){const s=[],n=Math.max(1,Math.floor(i));for(let o=1;o<n;o++){const l=t.radius*o/n,d=[];for(let g=0;g<=a;g++){const p=2*Math.PI*g/a,B={x:t.center.x+l*Math.cos(p),y:t.center.y+l*Math.sin(p)};d.push(G(e,B.x,B.y))}s.push({kind:"ring-boundary",...q(d,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function Ee(t,e,i,a){const{x:s,y:n}=t.center,o=t.radius,l=(f,u)=>G(e,f,u),d=i-s,g=Math.sqrt(Math.max(0,o*o-d*d)),p=l(i,n-g),B=l(i,n+g),F=a-n,T=Math.sqrt(Math.max(0,o*o-F*F)),b=l(s-T,a),$=l(s+T,a);return[{kind:"quadrant-boundary",...q([p,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...q([b,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function Ae(t,e){return t.map(i=>{const a=i.width/2,s=i.height/2;return{kind:"reticle",points:[[[i.x-a,i.y-s],[i.x+a,i.y-s],[i.x+a,i.y+s],[i.x-a,i.y+s]].map(([o,l])=>G(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function He(t){const e=t.filter(i=>i.probeIndex!==void 0).sort((i,a)=>(i.probeIndex??0)-(a.probeIndex??0));return e.length?[{kind:"probe-path",...q(e.map(i=>({x:i.physX,y:i.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function _e(t,e,i,a,s,n,o,l,d,g,p,B,F,T,b="off",$,f,u){const h=e.width-o,v=e.height-o,m=n?v:h,x=n?h:v,V=S=>s==="softBin"?S.sbin:S.hbin,E=S=>ct(S,$);if(e.partial){t.push({x:i,y:a,width:m,height:x,fill:De,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:i,y:a,width:m,height:x,fill:Ve,type:"stacked",metadata:e.metadata});return}if(d!==void 0&&(s==="hardBin"||s==="softBin")&&V(e)!==d){t.push({x:i,y:a,width:m,height:x,fill:wt,type:"hardBin",metadata:e.metadata});return}if(u!==void 0&&s==="metadata"&&E(e)!==u){t.push({x:i,y:a,width:m,height:x,fill:wt,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(b==="test"){const j=U(e,p,T),ut=j===void 0?z:j?Mt:St;t.push({x:i,y:a,width:m,height:x,fill:ut,type:"value",metadata:e.metadata});return}const S=P(e,p),N=ot(S,T);let W,J;S===void 0?W=z:b==="spec"?W=N==="failLow"?ge:N==="failHigh"?St:Mt:(W=l.forValue(g(S)),(N==="failLow"||N==="failHigh")&&(J=N)),t.push({x:i,y:a,width:m,height:x,fill:W,type:"value",specMark:J,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const S=V(e),N=S!=null?l.forBin(S):z,W=S!=null&&Se(e,F)===!1?!0:void 0;t.push({x:i,y:a,width:m,height:x,fill:N,type:s,binFail:W,metadata:e.metadata});return}if(s==="metadata"){const S=E(e),N=S!==void 0?f?.get(S)??z:z;t.push({x:i,y:a,width:m,height:x,fill:N,type:"metadata",metadata:e.metadata});return}const y=P(e,0),A=y!==void 0?l.forValue(g(y)):z;t.push({x:i,y:a,width:m,height:x,fill:A,type:"value",metadata:e.metadata})}function Ke(t,e,i){const a=t.radius*.15,s=xt(e,a,0),n=xt(e,0,a),o=s.x+n.x>=0?-1:1,l=s.y+n.y>=0?-1:1,d={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},g={x:d.x+s.x,y:d.y+s.y},p={x:d.x+n.x,y:d.y+n.y};return i.push({x:g.x+s.x*.35,y:g.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:p.x+n.x*.35,y:p.y+n.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...q([d,g]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...q([d,p]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,i={},a){const s=i.reticles??[],{plotMode:n="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:d=!1,ringCount:g=4,showRingBoundaries:p=!1,showQuadrantBoundaries:B=!1,showXYIndicator:F=!1,binColorScheme:T="default",valueColorScheme:b="default",reverseValueScheme:$=!1,useDefinedBinColors:f=!0,binColors:u,passBins:h=[1],highlightBin:v,highlightMetadataValue:m,interactiveTransform:x,valueRange:V,metadataValueOrder:E,testDefs:y,activeTest:A=0,activeMetadataKey:S,fallbackFormat:N="engineering",aggregationMethod:W,lotSize:J,dataAxisFlip:j,isLotStack:ut=!1,logScale:Xt,colorbarRangeMode:Yt="spec",passFailDisplay:Gt,showPartialDies:Qt=!0}=i,It=Gt??"off",ft=a?.hbinDefs,dt=a?.sbinDefs,Dt=a?.metadataFields,Ut=Dt?.find(c=>c.key===S),Jt={x:(j?.x??!1)!==(x?.flipX??!1),y:(j?.y??!1)!==(x?.flipY??!1)},Zt=ft?new Map(ft.map(c=>[c.bin,c])):null,te=dt?new Map(dt.map(c=>[c.bin,c])):null,ee=n==="softBin"?te:Zt,ne=new Set(h),ht=u&&Me(u,e)?u:xe(e,{passBins:h,binColorScheme:T,hbinDefs:ft,sbinDefs:dt,useDefinedBinColors:f}),ae=n==="softBin"?ht.soft:ht.hard,Vt={forValue:Pe(be(b,$)),forBin:c=>ae.get(c)??z};let{testNumber:H}=resolveTestNumber(A,y);if(!y?.length&&n==="value"&&!e.some(r=>r.testValues&&H in r.testValues)){const r=e.reduce((M,D)=>{if(!D.testValues)return M;const k=Object.keys(D.testValues).map(Number),R=k.length?$e(k):void 0;return R!==void 0&&(M===void 0||R<M)?R:M},void 0);r!==void 0&&(H=r)}let Z;if(Array.isArray(V))Z=V;else if(V){const{testNumber:c}=resolveTestNumber(V.test,y);Z=c===H?V.range:void 0}const I=findTestDef(y,H),Ct=n==="value"&&I!==void 0&&!vt(I),ie=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let K="off";n==="value"&&(Ct?K="test":It==="spec"&&ie?K="spec":It==="test"&&e.some(c=>U(c,H,I)!==void 0)&&(K="test"));const Ft=K==="spec"?"spec":Yt,mt=n==="stackedValues"||n==="stackedBins"||n==="stackedSoftBins";let O,_,tt=!1;if(Z){[O,_]=Z;let c=!0,r=!1;for(const M of e){const D=mt?P(M,0):P(M,H);if(D!==void 0&&(r=!0,!Number.isInteger(D))){c=!1;break}}tt=r&&c}else{const c=!mt&&n==="value"&&Ft==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let r=1/0,M=-1/0,D=!0;for(const k of e){const R=mt?P(k,0):P(k,H);R!==void 0&&(R<r&&(r=R),R>M&&(M=R),Number.isInteger(R)||(D=!1))}isFinite(r)||(D=!1),tt=D,c?(O=I.limitLow!==void 0?I.limitLow:isFinite(r)?r:0,_=I.limitHigh!==void 0?I.limitHigh:isFinite(M)?M:1):(O=isFinite(r)?r:0,_=isFinite(M)?M:1)}O===_&&(_=O+1);const Pt=Ct?!1:Xt??I?.logScale??!1,Lt=Pt&&O>0&&_>0,se=Lt;let et;if(Lt){const c=Math.log10(O),r=Math.log10(_)-c;et=M=>M<=0?0:Kt((Math.log10(M)-c)/r)}else et=c=>Kt((c-O)/(_-O));const C=Ce(t,j,x);let Rt=null;if(t.notch){const c=qt(t.notch.type),r=xt(C.physicalToScreen,Math.cos(c)*t.radius,Math.sin(c)*t.radius),M=Math.hypot(r.x,r.y);M>0&&(Rt={x:r.x/M,y:r.y/M})}let X=1/0;for(let c=0;c<e.length;c++){const r=e[c];r.width<X&&(X=r.width),r.height<X&&(X=r.height)}const Nt=1,oe=X!==1/0?Math.min(Nt,X*.12):Nt,Ot=[],pt=[],re=C.bakedToScreen.a===1&&C.bakedToScreen.b===0&&C.bakedToScreen.c===0&&C.bakedToScreen.d===1&&C.bakedToScreen.e===0&&C.bakedToScreen.f===0;let w=null;if(!re){w=new Float64Array(e.length*2);for(let c=0;c<e.length;c++){const r=G(C.bakedToScreen,e[c].physX,e[c].physY);w[c*2]=r.x,w[c*2+1]=r.y}}const le=ye(C.gridToScreen),ce=n==="hardBin"||n==="softBin"||n==="stackedBins"||n==="stackedSoftBins",ue=n==="softBin"||n==="stackedSoftBins";let nt=ce?new Map:void 0;const yt=n==="value"&&K==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,gt=n==="value"&&K==="test"?{pass:0,fail:0}:void 0,Et=n==="metadata";let at=Et?new Map:void 0,it=null;if(Et&&S){const c=collectMetadataValues(e,S),r=E?.key===S?E.values:void 0,M=r?[...r,...c.filter(D=>!r.includes(D))]:c;it=new Map(M.map((D,k)=>[D,Ut?.values?.find(R=>R.value===D)?.color??ve(k)]))}for(let c=0;c<e.length;c++){const r=e[c],M=w?w[c*2]:r.physX,D=w?w[c*2+1]:r.physY;if(pt.push({x:M,y:D}),nt&&!r.partial&&!r.edgeExcluded){const k=ue?r.sbin:r.hbin;k!=null&&nt.set(k,(nt.get(k)??0)+1)}if(yt&&!r.partial&&!r.edgeExcluded){const k=ot(P(r,H),I);k&&yt[k]++}if(gt&&!r.partial&&!r.edgeExcluded){const k=U(r,H,I);k!==void 0&&gt[k?"pass":"fail"]++}if(at&&!r.partial&&!r.edgeExcluded){const k=ct(r,S);k!==void 0&&at.set(k,(at.get(k)??0)+1)}r.partial&&!Qt||_e(Ot,r,M,D,n,le,oe,Vt,v,et,H,ee,ne,I,K,S,it,m)}const At=o?generateTextOverlay(e,w,{plotMode:n,colorFns:Vt,normalize:et,activeTest:A,valueRange:[O,_],testDefs:y,fallbackFormat:N,passFailDisplay:K,activeMetadataKey:S,metadataColorMap:it,allIntegerValues:tt}):[],Y=Ne(t,C.physicalToScreen);p&&Y.push(...Oe(t,C.physicalToScreen,g)),B&&Y.push(...Ee(t,C.physicalToScreen,t.center.x,t.center.y)),l&&Y.push(...Ae(s,C.gridToScreen)),d&&Y.push(...He(e)),F&&Y.push(...Ke(t,C.gridToScreen,At));const fe=pt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Ot,hoverPoints:pt,texts:At,overlays:Y,plotMode:n,binColorScheme:T,valueColorScheme:b,reverseValueScheme:$,binColors:ht,metadata:t.metadata??null,dies:e,valueRange:[O,_],allIntegerValues:tt,testDefs:y,activeTest:A,logScale:se,logScaleRequested:Pt,aggrMethod:W,lotSize:J,axisFlip:Jt,dataAxisFlip:{x:j?.x??!1,y:j?.y??!1},gridToScreen:C.gridToScreen,rotation:((t.orientation+(x?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:ut,passFailDisplay:K,colorbarRangeMode:Ft,waferCenter:t.center,waferRadius:t.radius,notchDir:Rt,binCounts:nt,metadataCounts:at,metadataColorMap:it,activeMetadataKey:S,metadataFields:Dt,specCounts:yt,passFailCounts:gt,dieBounds:fe}}export{getDieKey}from"../core/dies.js";
1
+ import{hasPosition as de}from"../core/dies.js";import{getReticleCell as he}from"../core/reticle.js";import{metadataCategoricalValue as me,metadataDisplayValue as pe}from"../core/metadata.js";import{affineRotation as bt,affineMirror as Ht,affineCompose as st,affinePoint as G,affineVector as xt,affineSwapsAxes as ye}from"../core/transforms.js";import{contrastTextColor as Q,SPEC_PASS_FILL as Mt,SPEC_FAIL_LOW as ge,SPEC_FAIL_HIGH as St}from"./colorMap.js";import{resolveValueColorFn as be}from"./colorSchemes.js";import{resolveBinColors as xe,binColorsCover as Me}from"./binColors.js";import{NO_DATA_FILL as z}from"./colorMap.js";import{diePassStatus as Se}from"../core/dies.js";import{getDieTestValue as P,getTestPassStatus as U,isParametricTest as vt}from"./buildWaferMap.js";import{classifySpec as ot}from"./spec.js";export{ot as classifySpec};import{fmt as kt,fmtColorbarAxis as $t,fmtAggregationMethod as _t}from"./fmt.js";import{metadataValueColor as ve}from"./colorMap.js";import{clamp01 as Kt,compareNatural as ke,escHtml as L,minOf as $e}from"../core/utils.js";import{prettyKey as Wt}from"../core/utils.js";import{testLabel as rt,markedTestLabel as lt,derivedTestNote as jt,isDerivedTest as Be,DERIVED_MARK as Te,DERIVED_KEY as Ie}from"./testLabel.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function collectMetadataValues(t,e){const i=new Set;for(const a of t){if(a.partial||a.edgeExcluded)continue;const s=ct(a,e);s!==void 0&&i.add(s)}return[...i].sort(ke)}export function findTestDef(t,e){return t?.find(i=>i.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,i)=>e-i)}const De="#d3d6db",wt="#e8e9ea",Ve="#aab0ba";function ct(t,e){return e?me(t.metadata?.[e]):void 0}function Ce(t,e,i){const{x:a,y:s}=t.center,n=bt(t.orientation,a,s),o=st(Ht(e?.x??!1,e?.y??!1,a,s),bt(t.orientation,a,s)),l=st(Ht(i?.flipX??!1,i?.flipY??!1,a,s),bt(i?.rotation??0,a,s));return{bakedToScreen:l,physicalToScreen:st(l,n),gridToScreen:st(l,o)}}function q(t,e=!1){return{points:[t],closed:e}}function Fe(t,e){const{center:i,radius:a,notch:s}=t;let n=i.x+a*Math.cos(e),o=i.y+a*Math.sin(e);if(!s)return{x:n,y:o};const l=Math.sqrt(a**2-(s.length/2)**2),d=s.length/2,g=n-i.x,p=o-i.y;return s.type==="bottom"&&p<-l?(o=i.y-l,n=i.x+Math.max(-d,Math.min(d,g))):s.type==="top"&&p>l?(o=i.y+l,n=i.x+Math.max(-d,Math.min(d,g))):s.type==="left"&&g<-l?(n=i.x-l,o=i.y+Math.max(-d,Math.min(d,p))):s.type==="right"&&g>l&&(n=i.x+l,o=i.y+Math.max(-d,Math.min(d,p))),{x:n,y:o}}function zt(t,e){return t.map(e).join(" / ")}function Pe(t,e=256){const i=Array.from({length:e},(a,s)=>t(s/(e-1)));return a=>i[Math.min(e-1,Math.max(0,Math.round(a*(e-1))))]}function Le(t,e){const i=Math.max(1,Math.min(t.width,t.height)),a=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(i*.55,a*1.8))))}function Re(t,e,i){if(e?.length){const a=e.flatMap(s=>{const n=s.testNumber;if(!vt(s)){const l=U(t,n,s);return l===void 0?[]:[{key:n,label:lt(s,n),value:l?"Pass":"Fail"}]}const o=P(t,n);return o===void 0?[]:[{key:n,label:lt(s,n),value:kt(o,s.unit,i),recordedFail:t.testPass?.[n]===!1}]});if(a.length)return a}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([a,s])=>({key:Number(a),label:rt(void 0,Number(a)),value:kt(s,void 0,i)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([a,s])=>({key:Number(a),label:rt(void 0,Number(a)),value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,i={}){const{testDefs:a,hbinDefs:s,sbinDefs:n,fallbackFormat:o,aggrMethod:l,lotSize:d,activeTest:g,reticleConfig:p,metadataFields:B}=i,F=s?new Map(s.map(f=>[f.bin,f])):null,T=n?new Map(n.map(f=>[f.bin,f])):null,b=[L(`Die (${t.x}, ${t.y})`)];if(p&&de(t)){const f=he(t,p);b.push(L(`Reticle (${f.column}, ${f.row})`))}if(e==="stackedValues"){const f=P(t,0);if(f!==void 0){const u=a?.[0],h=u?lt(u,u.testNumber):"Value",v=l?` (${l})`:"";b.push(L(`${h}${v}: ${kt(f,u?.unit,o)}`));const m=jt(u);m&&b.push(`<i>${L(m)}</i>`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const f=P(t,0),u=e==="stackedSoftBins"?t.sbin:t.hbin;if(f!==void 0){const h=e==="stackedSoftBins"?T:F,v=u!==void 0?h?.get(u)?.name?`${u} \xB7 ${h.get(u).name}`:`Bin ${u}`:"Bin";let m;if(l==="percent")m=`${f.toFixed(0)}%`;else{const V=d?` (${(f/d*100).toFixed(0)}% of stacked wafers)`:"";m=`${f}${V}`}const x=l?` [${_t(l)}]`:"";b.push(L(`${v}: ${m}${x}`))}}else{const f=Re(t,a,o);if(e==="value"&&f.length){const{testNumber:u}=resolveTestNumber(g??0,a),h=findTestDef(a,u),v=P(t,u);let m=f.findIndex(A=>A.key===u);m<0&&(m=0);const x=f[m];let V=`<b>${L(`${x.label}: ${x.value}`)}${x.recordedFail?" <i>(recorded fail)</i>":""}</b>`;if(x.key===u&&h){const A=ot(v,h);(A==="failLow"||A==="failHigh")&&(V+=" <i>(outside test limits)</i>")}b.push(V);const E=jt(findTestDef(a,x.key));E&&b.push(`<i>${L(E)}</i>`);const y=f.length-1;y>0&&b.push(`<i>+${y} more test${y===1?"":"s"}</i>`)}else if(f.length){const u=f.length;b.push(`<i>${u} test result${u===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const u=[];if(t.hbin!==void 0){const h=F?.get(t.hbin)?.name,v=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);u.push(`HBin: ${v}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,v=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);u.push(`SBin: ${v}`)}b.push(u.map(L).join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&b.push(L(`Retests: ${t.retestCount}`)),t.siteNum!==void 0&&b.push(L(`Site: ${t.siteNum}`)),t.partId!==void 0&&b.push(L(`Part ID: ${t.partId}`)),t.partial&&b.push("<i>partial die</i>"),t.probeIndex!==void 0&&b.push(L(`Probe: #${t.probeIndex}`));const $={...t.metadata??{}};for(const[f,u]of Object.entries($)){const h=pe(u);if(h===void 0)continue;const v=B?.find(m=>m.key===f)?.label??Wt(f);b.push(L(`${v}: ${h}`))}return b.join("<br>")}function Bt(t){return t?.name?lt(t,t.testNumber):void 0}function Tt(t){return Be(t)?{note:`${Te} ${Ie}`}:{}}export function buildMapTitle(t,e="engineering",i){const a=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=n=>{const o=i?.length===1?i[0]:void 0;return o===void 0?`${n} Bin`:o.name?`${n} Bin ${o.bin} \xB7 ${o.name}`:`${n} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:a};case"stackedSoftBins":return{primary:s("Soft"),secondary:a};case"metadata":{const n=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?Wt(t.activeMetadataKey):"Metadata";return{primary:n?.label??o,secondary:""}}case"stackedValues":{const n=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=$t(o,Bt(n),n?.unit,e),d=l||"Value",g=t.aggrMethod?` \xB7 ${_t(t.aggrMethod)}`:"";return{primary:`${d}${g}`,secondary:a,...Tt(n)}}default:{const n=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=$t(o,Bt(n),n?.unit,e),d=l||Bt(n);if(t.passFailDisplay!=="off"){const g=n?.testNumber??t.activeTest,p=d?`${d} \xB7 #${g}`:rt(void 0,g),B=t.passFailDisplay==="spec"?"Limit pass/fail":n!==void 0&&!vt(n)?"Functional pass/fail":"Tester pass/fail";return{primary:p,secondary:B,...Tt(n)}}return{primary:d??rt(void 0,t.activeTest),secondary:"",...Tt(n)}}}}export function generateTextOverlay(t,e,i){const{plotMode:a,colorFns:s,normalize:n,activeTest:o,valueRange:l,testDefs:d,fallbackFormat:g,passFailDisplay:p="off",activeMetadataKey:B,metadataColorMap:F,allIntegerValues:T}=i,b=findTestDef(d,o),{tickFmt:$}=$t(l[1],b?.name,b?.unit,g),f=T?h=>String(Math.round(h)):$,{testNumber:u}=resolveTestNumber(o,d);return t.flatMap((h,v)=>{let m="",x="#111111";if(a==="value"&&p==="test"){const y=U(h,u,b);if(y===void 0)return[];m=y?"P":"F",x=Q(y?Mt:St)}else if(a==="value"){const y=P(h,u);if(y===void 0)return[];m=zt([y],f),x=Q(s.forValue(n(y)))}else if(a==="hardBin"||a==="softBin"){const y=a==="softBin"?h.sbin:h.hbin;if(y===void 0)return[];m=String(y),x=Q(s.forBin(y))}else if(a==="metadata"){const y=ct(h,B);if(y===void 0)return[];m=y,x=Q(F?.get(m)??z)}else{const y=P(h,0);if(y===void 0)return[];m=zt([y],f),x=Q(s.forValue(n(y)))}const V=e?e[v*2]:h.physX,E=e?e[v*2+1]:h.physY;return[{x:V,y:E,text:m,fontSize:Le(h,m),color:x,align:"center"}]})}function qt(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function Ne(t,e,i=720){const{center:a,radius:s,notch:n}=t,o=$=>G(e,$.x,$.y);if(!(n!==void 0&&t.diameter>150)){const $=[];for(let f=0;f<=i;f++){const u=2*Math.PI*f/i;$.push(o(Fe(t,u)))}return[{kind:"wafer-boundary",...q($,!0),lineColor:"#888888",lineWidth:1}]}const d=1.25,g=n.length,p=qt(n.type),B=Math.atan2(g,s),F=s-d,T=[];let b=!1;for(let $=0;$<=i;$++){const f=2*Math.PI*$/i;let u=f-p;for(;u>Math.PI;)u-=2*Math.PI;for(;u<-Math.PI;)u+=2*Math.PI;if(Math.abs(u)<=B){if(!b){b=!0;const h={x:a.x+s*Math.cos(p-B),y:a.y+s*Math.sin(p-B)},v={x:a.x+F*Math.cos(p),y:a.y+F*Math.sin(p)},m={x:a.x+s*Math.cos(p+B),y:a.y+s*Math.sin(p+B)};T.push(o(h),o(v),o(m))}continue}T.push(o({x:a.x+s*Math.cos(f),y:a.y+s*Math.sin(f)}))}return[{kind:"wafer-boundary",...q(T,!0),lineColor:"#888888",lineWidth:1}]}function Oe(t,e,i,a=360){const s=[],n=Math.max(1,Math.floor(i));for(let o=1;o<n;o++){const l=t.radius*o/n,d=[];for(let g=0;g<=a;g++){const p=2*Math.PI*g/a,B={x:t.center.x+l*Math.cos(p),y:t.center.y+l*Math.sin(p)};d.push(G(e,B.x,B.y))}s.push({kind:"ring-boundary",...q(d,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function Ee(t,e,i,a){const{x:s,y:n}=t.center,o=t.radius,l=(f,u)=>G(e,f,u),d=i-s,g=Math.sqrt(Math.max(0,o*o-d*d)),p=l(i,n-g),B=l(i,n+g),F=a-n,T=Math.sqrt(Math.max(0,o*o-F*F)),b=l(s-T,a),$=l(s+T,a);return[{kind:"quadrant-boundary",...q([p,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...q([b,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function Ae(t,e){return t.map(i=>{const a=i.width/2,s=i.height/2;return{kind:"reticle",points:[[[i.x-a,i.y-s],[i.x+a,i.y-s],[i.x+a,i.y+s],[i.x-a,i.y+s]].map(([o,l])=>G(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function He(t){const e=t.filter(i=>i.probeIndex!==void 0).sort((i,a)=>(i.probeIndex??0)-(a.probeIndex??0));return e.length?[{kind:"probe-path",...q(e.map(i=>({x:i.physX,y:i.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function _e(t,e,i,a,s,n,o,l,d,g,p,B,F,T,b="off",$,f,u){const h=e.width-o,v=e.height-o,m=n?v:h,x=n?h:v,V=S=>s==="softBin"?S.sbin:S.hbin,E=S=>ct(S,$);if(e.partial){t.push({x:i,y:a,width:m,height:x,fill:De,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:i,y:a,width:m,height:x,fill:Ve,type:"stacked",metadata:e.metadata});return}if(d!==void 0&&(s==="hardBin"||s==="softBin")&&V(e)!==d){t.push({x:i,y:a,width:m,height:x,fill:wt,type:"hardBin",metadata:e.metadata});return}if(u!==void 0&&s==="metadata"&&E(e)!==u){t.push({x:i,y:a,width:m,height:x,fill:wt,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(b==="test"){const j=U(e,p,T),ut=j===void 0?z:j?Mt:St;t.push({x:i,y:a,width:m,height:x,fill:ut,type:"value",metadata:e.metadata});return}const S=P(e,p),N=ot(S,T);let W,J;S===void 0?W=z:b==="spec"?W=N==="failLow"?ge:N==="failHigh"?St:Mt:(W=l.forValue(g(S)),(N==="failLow"||N==="failHigh")&&(J=N)),t.push({x:i,y:a,width:m,height:x,fill:W,type:"value",specMark:J,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const S=V(e),N=S!=null?l.forBin(S):z,W=S!=null&&Se(e,F)===!1?!0:void 0;t.push({x:i,y:a,width:m,height:x,fill:N,type:s,binFail:W,metadata:e.metadata});return}if(s==="metadata"){const S=E(e),N=S!==void 0?f?.get(S)??z:z;t.push({x:i,y:a,width:m,height:x,fill:N,type:"metadata",metadata:e.metadata});return}const y=P(e,0),A=y!==void 0?l.forValue(g(y)):z;t.push({x:i,y:a,width:m,height:x,fill:A,type:"value",metadata:e.metadata})}function Ke(t,e,i){const a=t.radius*.15,s=xt(e,a,0),n=xt(e,0,a),o=s.x+n.x>=0?-1:1,l=s.y+n.y>=0?-1:1,d={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},g={x:d.x+s.x,y:d.y+s.y},p={x:d.x+n.x,y:d.y+n.y};return i.push({x:g.x+s.x*.35,y:g.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:p.x+n.x*.35,y:p.y+n.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...q([d,g]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...q([d,p]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,i={},a){const s=i.reticles??[],{plotMode:n="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:d=!1,ringCount:g=4,showRingBoundaries:p=!1,showQuadrantBoundaries:B=!1,showXYIndicator:F=!1,binColorScheme:T="default",valueColorScheme:b="default",reverseValueScheme:$=!1,useDefinedBinColors:f=!0,binColors:u,passBins:h=[1],highlightBin:v,highlightMetadataValue:m,interactiveTransform:x,valueRange:V,metadataValueOrder:E,testDefs:y,activeTest:A=0,activeMetadataKey:S,fallbackFormat:N="engineering",aggregationMethod:W,lotSize:J,dataAxisFlip:j,isLotStack:ut=!1,logScale:Xt,colorbarRangeMode:Yt="spec",passFailDisplay:Gt,showPartialDies:Qt=!0}=i,It=Gt??"off",ft=a?.hbinDefs,dt=a?.sbinDefs,Dt=a?.metadataFields,Ut=Dt?.find(c=>c.key===S),Jt={x:(j?.x??!1)!==(x?.flipX??!1),y:(j?.y??!1)!==(x?.flipY??!1)},Zt=ft?new Map(ft.map(c=>[c.bin,c])):null,te=dt?new Map(dt.map(c=>[c.bin,c])):null,ee=n==="softBin"?te:Zt,ne=new Set(h),ht=u&&Me(u,e)?u:xe(e,{passBins:h,binColorScheme:T,hbinDefs:ft,sbinDefs:dt,useDefinedBinColors:f}),ae=n==="softBin"?ht.soft:ht.hard,Vt={forValue:Pe(be(b,$)),forBin:c=>ae.get(c)??z};let{testNumber:H}=resolveTestNumber(A,y);if(!y?.length&&n==="value"&&!e.some(r=>r.testValues&&H in r.testValues)){const r=e.reduce((M,D)=>{if(!D.testValues)return M;const k=Object.keys(D.testValues).map(Number),R=k.length?$e(k):void 0;return R!==void 0&&(M===void 0||R<M)?R:M},void 0);r!==void 0&&(H=r)}let Z;if(Array.isArray(V))Z=V;else if(V){const{testNumber:c}=resolveTestNumber(V.test,y);Z=c===H?V.range:void 0}const I=findTestDef(y,H),Ct=n==="value"&&I!==void 0&&!vt(I),ie=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let K="off";n==="value"&&(Ct?K="test":It==="spec"&&ie?K="spec":It==="test"&&e.some(c=>U(c,H,I)!==void 0)&&(K="test"));const Ft=K==="spec"?"spec":Yt,mt=n==="stackedValues"||n==="stackedBins"||n==="stackedSoftBins";let O,_,tt=!1;if(Z){[O,_]=Z;let c=!0,r=!1;for(const M of e){const D=mt?P(M,0):P(M,H);if(D!==void 0&&(r=!0,!Number.isInteger(D))){c=!1;break}}tt=r&&c}else{const c=!mt&&n==="value"&&Ft==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let r=1/0,M=-1/0,D=!0;for(const k of e){const R=mt?P(k,0):P(k,H);R!==void 0&&(R<r&&(r=R),R>M&&(M=R),Number.isInteger(R)||(D=!1))}isFinite(r)||(D=!1),tt=D,c?(O=I.limitLow!==void 0?I.limitLow:isFinite(r)?r:0,_=I.limitHigh!==void 0?I.limitHigh:isFinite(M)?M:1):(O=isFinite(r)?r:0,_=isFinite(M)?M:1)}O===_&&(_=O+1);const Pt=Ct?!1:Xt??I?.logScale??!1,Lt=Pt&&O>0&&_>0,se=Lt;let et;if(Lt){const c=Math.log10(O),r=Math.log10(_)-c;et=M=>M<=0?0:Kt((Math.log10(M)-c)/r)}else et=c=>Kt((c-O)/(_-O));const C=Ce(t,j,x);let Rt=null;if(t.notch){const c=qt(t.notch.type),r=xt(C.physicalToScreen,Math.cos(c)*t.radius,Math.sin(c)*t.radius),M=Math.hypot(r.x,r.y);M>0&&(Rt={x:r.x/M,y:r.y/M})}let X=1/0;for(let c=0;c<e.length;c++){const r=e[c];r.width<X&&(X=r.width),r.height<X&&(X=r.height)}const Nt=1,oe=X!==1/0?Math.min(Nt,X*.12):Nt,Ot=[],pt=[],re=C.bakedToScreen.a===1&&C.bakedToScreen.b===0&&C.bakedToScreen.c===0&&C.bakedToScreen.d===1&&C.bakedToScreen.e===0&&C.bakedToScreen.f===0;let w=null;if(!re){w=new Float64Array(e.length*2);for(let c=0;c<e.length;c++){const r=G(C.bakedToScreen,e[c].physX,e[c].physY);w[c*2]=r.x,w[c*2+1]=r.y}}const le=ye(C.gridToScreen),ce=n==="hardBin"||n==="softBin"||n==="stackedBins"||n==="stackedSoftBins",ue=n==="softBin"||n==="stackedSoftBins";let nt=ce?new Map:void 0;const yt=n==="value"&&K==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,gt=n==="value"&&K==="test"?{pass:0,fail:0}:void 0,Et=n==="metadata";let at=Et?new Map:void 0,it=null;if(Et&&S){const c=collectMetadataValues(e,S),r=E?.key===S?E.values:void 0,M=r?[...r,...c.filter(D=>!r.includes(D))]:c;it=new Map(M.map((D,k)=>[D,Ut?.values?.find(R=>R.value===D)?.color??ve(k)]))}for(let c=0;c<e.length;c++){const r=e[c],M=w?w[c*2]:r.physX,D=w?w[c*2+1]:r.physY;if(pt.push({x:M,y:D}),nt&&!r.partial&&!r.edgeExcluded){const k=ue?r.sbin:r.hbin;k!=null&&nt.set(k,(nt.get(k)??0)+1)}if(yt&&!r.partial&&!r.edgeExcluded){const k=ot(P(r,H),I);k&&yt[k]++}if(gt&&!r.partial&&!r.edgeExcluded){const k=U(r,H,I);k!==void 0&&gt[k?"pass":"fail"]++}if(at&&!r.partial&&!r.edgeExcluded){const k=ct(r,S);k!==void 0&&at.set(k,(at.get(k)??0)+1)}r.partial&&!Qt||_e(Ot,r,M,D,n,le,oe,Vt,v,et,H,ee,ne,I,K,S,it,m)}const At=o?generateTextOverlay(e,w,{plotMode:n,colorFns:Vt,normalize:et,activeTest:A,valueRange:[O,_],testDefs:y,fallbackFormat:N,passFailDisplay:K,activeMetadataKey:S,metadataColorMap:it,allIntegerValues:tt}):[],Y=Ne(t,C.physicalToScreen);p&&Y.push(...Oe(t,C.physicalToScreen,g)),B&&Y.push(...Ee(t,C.physicalToScreen,t.center.x,t.center.y)),l&&Y.push(...Ae(s,C.gridToScreen)),d&&Y.push(...He(e)),F&&Y.push(...Ke(t,C.gridToScreen,At));const fe=pt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Ot,hoverPoints:pt,texts:At,overlays:Y,plotMode:n,binColorScheme:T,valueColorScheme:b,reverseValueScheme:$,binColors:ht,metadata:t.metadata??null,dies:e,valueRange:[O,_],allIntegerValues:tt,testDefs:y,activeTest:A,logScale:se,logScaleRequested:Pt,aggrMethod:W,lotSize:J,axisFlip:Jt,dataAxisFlip:{x:j?.x??!1,y:j?.y??!1},gridToScreen:C.gridToScreen,rotation:((t.orientation+(x?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:ut,passFailDisplay:K,colorbarRangeMode:Ft,waferCenter:t.center,waferRadius:t.radius,notchDir:Rt,binCounts:nt,metadataCounts:at,metadataColorMap:it,activeMetadataKey:S,metadataFields:Dt,specCounts:yt,passFailCounts:gt,dieBounds:fe}}export{getDieKey}from"../core/dies.js";
@@ -57,12 +57,19 @@ export interface DieResult {
57
57
  */
58
58
  siteNum?: number;
59
59
  /**
60
- * STDF `pir.part_id` — tester-assigned identifier for this tested unit.
61
- * At most fabs this encodes the probe sequence (the order in which the prober
62
- * stepped across the wafer), but the field is semantically neutral — its
63
- * meaning is fab-specific.
60
+ * STDF PRR `PART_ID` — the tester's part identification, text in STDF (a
61
+ * number is accepted too). Data about the part, not an identifier: a die's
62
+ * identity is its position. Its meaning is fab-specific; at many fabs it
63
+ * encodes the probe sequence.
64
64
  */
65
- partId?: number;
65
+ partId?: number | string;
66
+ /**
67
+ * The tester marked this record as replacing an earlier one (STDF PRR
68
+ * `PART_FLG` bit 0/1): `'partId'` — the earlier record with the same part ID;
69
+ * `'position'` — the earlier record at the same position. Such a record always
70
+ * wins, whatever `retestPolicy` says.
71
+ */
72
+ supersedes?: 'partId' | 'position';
66
73
  /** Per-die metadata — all fields appear automatically in hover tooltips. See `DieMetadata → §12.4`. */
67
74
  metadata?: DieMetadata;
68
75
  }
@@ -214,16 +221,28 @@ export interface TestDef {
214
221
  */
215
222
  logScale?: boolean;
216
223
  /**
217
- * Lower specification limit in the same units as the test value.
218
- * When set, values below this limit are considered out-of-spec.
219
- * Both limits are optional independently — some tests have one-sided limits.
224
+ * Lower **test limit** (STDF `LO_LIMIT`) — the pass/fail limit, in the test
225
+ * value's units. A value below it is outside the test limits. Both limits are
226
+ * optional independently — some tests have one-sided limits.
220
227
  */
221
228
  limitLow?: number;
229
+ /** Upper test limit (STDF `HI_LIMIT`). A value above it is outside the test limits. */
230
+ limitHigh?: number;
222
231
  /**
223
- * Upper specification limit in the same units as the test value.
224
- * When set, values above this limit are considered out-of-spec.
232
+ * Whether a value exactly equal to `limitLow` passes. Default `true`. STDF
233
+ * states it per test in `PARM_FLG` bit 6, ATDF in the Limit Compare field.
225
234
  */
226
- limitHigh?: number;
235
+ limitLowInclusive?: boolean;
236
+ /** Whether a value exactly equal to `limitHigh` passes. Default `true`; STDF `PARM_FLG` bit 7. */
237
+ limitHighInclusive?: boolean;
238
+ /**
239
+ * Lower **specification limit** (STDF `LO_SPEC`) — separate from the test limits,
240
+ * and what process capability (Cp/Cpk/Pp/Ppk) is judged against when both spec
241
+ * limits are given. Without them, capability uses the test limits and says so.
242
+ */
243
+ specLow?: number;
244
+ /** Upper specification limit (STDF `HI_SPEC`). See `specLow`. */
245
+ specHigh?: number;
227
246
  /**
228
247
  * Test kind: `'P'` (parametric — a continuous measured value) or `'F'`
229
248
  * (functional — a pass/fail outcome, conventionally recorded as 1 = pass,
@@ -567,6 +586,15 @@ export interface WaferWarning {
567
586
  * were given as text (or verdicts as something other than true/false), as a
568
587
  * CSV parser produces. They are not converted, so those dies are judged and
569
588
  * plotted wrongly — a bin of "1" is not pass bin 1. The message counts them.
589
+ * - `'input-values-outside-stdf'` — raised by `buildWaferMap`: bins, coordinates,
590
+ * test numbers, site numbers or `waferConfig.orientation` outside the STDF V4
591
+ * ranges, or test values that are not finite. Used as given for now; a future
592
+ * release treats them as missing. A `NaN` bin is already treated as no bin, and
593
+ * is counted here too.
594
+ * - `'retests-by-part-id'` — raised by `buildWaferMap` (severity `'info'`): dies
595
+ * with no position that share a part ID were treated as retests of one die.
596
+ * Blank part IDs never match, and part IDs are not used on a wafer where one
597
+ * value covers more than 20% of the unpositioned records (a default, not an ID).
570
598
  * - `'derived-test-invalid'` — raised by `buildWaferMap`: a `derivedTests` entry
571
599
  * could not be compiled — a parse or type error in its `expression`, a
572
600
  * `testNumber` that collides with a measured test, a `testType` that
@@ -586,7 +614,7 @@ export interface WaferWarning {
586
614
  * The union is intentionally open to string so future advisory codes can be
587
615
  * added without a breaking change; switch with a `default` branch.
588
616
  */
589
- code: 'partial-coverage' | 'geometry-conflict' | 'non-standard-diameter' | 'diameter-exceeds-die-extent' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | 'analysis-option-corrected' | 'bin-colors-shared' | 'pass-bins-mixed' | 'ring-count-mixed' | 'input-field-removed' | 'input-values-not-numbers' | 'derived-test-invalid' | (string & {});
617
+ code: 'partial-coverage' | 'geometry-conflict' | 'non-standard-diameter' | 'diameter-exceeds-die-extent' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | 'analysis-option-corrected' | 'bin-colors-shared' | 'pass-bins-mixed' | 'ring-count-mixed' | 'input-field-removed' | 'input-values-not-numbers' | 'input-values-outside-stdf' | 'retests-by-part-id' | 'derived-test-invalid' | (string & {});
590
618
  /** Human-readable explanation, suitable for direct display. */
591
619
  message: string;
592
620
  /**
@@ -1 +1 @@
1
- import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as ge,getDieKey as V,hasPosition as R}from"../core/dies.js";import{applyOrientation as U,transformDies as pe,clipDiesToWafer as ye}from"../core/transforms.js";import{generateDies as be}from"../core/dies.js";import{affineRotation as we,affineMirror as xe,affineCompose as De,affinePoint as Me}from"../core/transforms.js";import{inferWaferFromXY as ve}from"../core/inference/wafer.js";import{resolveGridPitch as Ce}from"../core/inference/pitch.js";import{assignGridIndices as Oe}from"../core/inference/grid.js";import{generateReticleGrid as Se}from"../core/reticle.js";import{applyDerivedTests as J}from"./derivedTests/apply.js";import{buildView as Q}from"./buildView.js";import{maxOf as K,minOf as Z,modeOf as ke}from"../core/utils.js";import{aggregateValues as ee,aggregateBinCounts as Ae}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function Re(t){const n=(c,i)=>({ringCount:c,ringCountWarning:{code:"analysis-option-corrected",severity:"warning",message:`ringCount=${String(t)} ${i} (using ${c}).`}});if(t===void 0)return{ringCount:4,ringCountWarning:void 0};if(typeof t!="number"||!Number.isFinite(t))return n(4,"is not a finite number");const e=Math.max(1,Math.round(t));return e===t?{ringCount:t,ringCountWarning:void 0}:n(e,"is not a whole number of at least 1")}function te(t){const n=Array.isArray(t)?{}:t,e=Array.isArray(n.lotStack?.results)?n.lotStack.results:[],c=(Array.isArray(t)?[t]:[n.results,...e]).filter(a=>Array.isArray(a)),i={hbin:0,sbin:0,testValues:0,testPass:0};let o;const u=(a,s)=>{i[a]++,o??=`${a} ${JSON.stringify(s)}`};for(const a of c){const s=new Set,m=new Set;for(let p=0;p<a.length;p+=p<50?1:100){const h=a[p];if(!(h===null||typeof h!="object")){if(h.testValues&&typeof h.testValues=="object")for(const x of Object.keys(h.testValues))s.add(x);if(h.testPass&&typeof h.testPass=="object")for(const x of Object.keys(h.testPass))m.add(x)}}const f=[...s],l=[...m];for(const p of a){if(p===null||typeof p!="object")continue;const h=p;h.hbin!=null&&typeof h.hbin!="number"&&u("hbin",h.hbin),h.sbin!=null&&typeof h.sbin!="number"&&u("sbin",h.sbin);const x=h.testValues;if(x&&typeof x=="object")for(let D=0;D<f.length;D++){const b=x[f[D]];b!=null&&typeof b!="number"&&u("testValues",b)}const C=h.testPass;if(C&&typeof C=="object")for(let D=0;D<l.length;D++){const b=C[l[D]];b!=null&&typeof b!="boolean"&&u("testPass",b)}}}const r=[];if(i.hbin&&r.push(`${i.hbin} hard bin${i.hbin===1?"":"s"}`),i.sbin&&r.push(`${i.sbin} soft bin${i.sbin===1?"":"s"}`),i.testValues&&r.push(`${i.testValues} test value${i.testValues===1?"":"s"}`),i.testPass&&r.push(`${i.testPass} pass/fail verdict${i.testPass===1?"":"s"}`),!r.length)return;const g=`buildWaferMap received ${r.join(", ")} of the wrong type (for example ${o}). Bins and test values must be numbers and verdicts true/false \u2014 a bin of "1" is not pass bin 1, so those dies count as fails and yield is wrong, and a test value given as text is not plotted or analysed correctly. Convert them with Number() (and verdicts to booleans) and rebuild.`;return console.warn(`[wafermap] ${g}`),{code:"input-values-not-numbers",message:g,severity:"error"}}function ne(t){const n=(a,s)=>a!==null&&typeof a=="object"&&a[s]!==void 0,e=Array.isArray(t)?{}:t,c=[],i=(a,s,m)=>{a&&c.push(`\`${s}\` (now \`${m}\`)`)};i(n(e,"data"),"data","results"),i(n(e,"die"),"die","dieConfig"),i(n(e,"stack"),"stack","lotStack"),i(n(e.dieConfig,"origin"),"dieConfig.origin","dieConfig.coordinateOrigin"),i(n(e.waferConfig,"flat"),"waferConfig.flat","waferConfig.notch"),i(n(e.reticleConfig,"anchor"),"reticleConfig.anchor","reticleConfig.anchorDie"),i(n(e.lotStack,"aggr"),"lotStack.aggr","lotStack.method"),i(Array.isArray(e.testDefs)&&e.testDefs.some(a=>n(a,"index")&&!n(a,"testNumber")),"TestDef.index","TestDef.testNumber");const o=n(e.lotStack,"results")?e.lotStack.results:[],u=(Array.isArray(t)?[t]:[e.results,...Array.isArray(o)?o:[]]).filter(a=>Array.isArray(a));if(i(u.some(a=>a.slice(0,100).some(s=>n(s,"values"))),"values","testValues"),!c.length)return;const r=c.length>1,g=`buildWaferMap ignored ${c.join(", ")}: ${r?"these names were":"that name was"} removed in an earlier release, so what ${r?"they":"it"} described is not on this map. Rename and rebuild.`;return console.warn(`[wafermap] ${g}`),{code:"input-field-removed",message:g,severity:"error"}}function We(t){if(t.results!==void 0||t.lotStack!==void 0)throw new Error("buildWaferMap: pass `layout`, `results` or `lotStack` \u2014 only one.");const n=t.waferConfig?.diameter,e=t.dieConfig?.width,c=t.dieConfig?.height;if(!(n>0&&e>0&&c>0))throw new Error("buildWaferMap: `layout` needs waferConfig.diameter and dieConfig.width and height, all positive.");const i=j({diameter:n,notch:t.waferConfig?.notch,orientation:0}),o=ye(be(i,{width:e,height:c}),i,{width:e,height:c}).filter(s=>!s.partial),{flipX:u,flipY:r}=se(t.dieConfig,{type:"center"}),{layout:g,...a}=t;return{...a,results:o.map(s=>({x:u?-s.x:s.x,y:r?-s.y:s.y})),waferConfig:{...t.waferConfig,center:{x:0,y:0}},dieConfig:{...t.dieConfig,coordinateOrigin:{type:"center"}}}}function $e(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],ringCount:4,ringCountWarning:void 0,removedFieldWarning:ne(t),nonNumericWarning:te(t),standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,derivedTests:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],...Re(t.ringCount),removedFieldWarning:ne(t),nonNumericWarning:te(t),standardDiameters:t.standardDiameters,testDefs:t.testDefs?.length?t.testDefs:void 0,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,derivedTests:t.derivedTests?.length?t.derivedTests:void 0,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Ee(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Fe(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let c=1/0,i=-1/0,o=1/0,u=-1/0;for(const r of t)r.x<c&&(c=r.x),r.x>i&&(i=r.x),r.y<o&&(o=r.y),r.y>u&&(u=r.y);return{offsetX:Math.round((i+c)/2),offsetY:Math.round((u+o)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Pe(t,n,e,c,i,o){if(o)return{colMidX:(Math.round(o.x)-n)*c,colMidY:(Math.round(o.y)-e)*i,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let u=1/0,r=-1/0,g=1/0,a=-1/0;for(const s of t){const m=Math.round(s.x)-n,f=Math.round(s.y)-e;m<u&&(u=m),m>r&&(r=m),f<g&&(g=f),f>a&&(a=f)}return{colMidX:(u+r)/2*c,colMidY:(g+a)/2*i,anchored:!1}}function Te(t,n,e){let c=1/0,i=-1/0,o=1/0,u=-1/0,r=0,g=0;for(const m of t){const f=Math.round(m.x)-n,l=Math.round(m.y)-e;r+=f,g+=l,f<c&&(c=f),f>i&&(i=f),l<o&&(o=l),l>u&&(u=l)}const a=t.length,s=(m,f,l)=>{const p=(l-f)/2;return p<=0?0:Math.abs(m/a-(f+l)/2)/p};return s(r,c,i)>.11||s(g,o,u)>.11}function se(t,n){let e=t?.xAxisDirection==="left",c=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(c=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:c}}function Ie(t,n){const{method:e,targetBin:c}=t,i=t.results.map(o=>o.filter(R));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const o=new Set((n??[]).filter(s=>!isParametricTest(s)).map(s=>s.testNumber)),u=new Set;for(const s of i)for(const m of s)if(m.testValues)for(const f of Object.keys(m.testValues)){const l=Number(f);o.has(l)||u.add(l)}if(u.size===0)return ee(i,e);const r=[...u],g=r.map(s=>ee(i,e,s)),a=new Map;for(let s=0;s<r.length;s++){const m=r[s];for(const f of g[s]){const l=V(f);a.has(l)||a.set(l,{template:f,testValues:{}});const p=a.get(l),h=f.testValues?.[0];h!==void 0&&(p.testValues[m]=h)}}return[...a.values()].map(({template:s,testValues:m})=>({...s,testValues:Object.keys(m).length>0?m:void 0}))}if(e==="countBin"||e==="percent"){if(c===void 0)return[];const o=Ae(i,c,"hard");if(e==="countBin")return o;const u=i.length;return o.map(r=>({...r,testValues:{0:u>0?(r.testValues?.[0]??0)/u*100:0}}))}if(e==="mode"){const o=new Map;for(const r of i)for(const g of r){const a=V(g);o.has(a)||o.set(a,[]),o.get(a).push(g)}const u=[];for(const[r,g]of o){const a=r.split(","),s=Number(a[0]),m=Number(a[1]),f=g.map(p=>p.hbin).filter(p=>p!==void 0),l=ke(f);l!==null&&u.push({x:s,y:m,hbin:l})}return u}return[]}function ie(t){const n=t.filter(R),e=n.length,c=n.filter(o=>o.edgeExcluded).length,i=n.filter(o=>dieHasTestData(o)||o.hbin!==void 0||o.sbin!==void 0).length;return{filledDies:i,totalDies:e,edgeExcludedDies:c,unpositionedDies:t.length-n.length,ratio:e>0?i/e:0}}function re(t,n,e="exclude"){const c=new Set(n),o=t.filter(l=>!l.partial).filter(l=>l.edgeExcluded).length,u=t.filter(l=>l.partial).length;let r=0,g=0,a=!1;for(const l of t){if(!ge(l))continue;const p=l.hbin??l.sbin;p!==void 0&&(a=!0,c.has(p)?r++:g++)}const s=r+g,m=a&&s>0?r/s*100:null;let f=null;if(e==="denominator-only"){const l=s+o;f=a&&l>0?r/l*100:null}return{passDies:r,failDies:g,edgeExcludedDies:o,partialDies:u,totalDies:s,yieldPercent:m,yieldPercentGross:f}}function oe(t,n,e,c,i,o=0,u=0,r=0,g=0,a=0,s=!1,m=!1){if(!t)return[];const f=t.anchorDie??{x:0,y:0},l=Se(n,{width:t.width,height:t.height,diePitchX:c,diePitchY:i,anchorDie:{x:f.x-o,y:f.y-u},gridOrigin:{x:-r,y:-g}}),p=De(xe(s,m,n.center.x,n.center.y),we(a,n.center.x,n.center.y));return l.filter(h=>{const x=h.width/2,C=h.height/2,D=[[h.x-x,h.y-C],[h.x+x,h.y-C],[h.x+x,h.y+C],[h.x-x,h.y+C]].map(([w,P])=>Me(p,w,P)),b=Z(D.map(w=>w.x)),S=K(D.map(w=>w.x)),W=Z(D.map(w=>w.y)),$=K(D.map(w=>w.y));return e.some(w=>w.physX>=b&&w.physX<S&&w.physY>=W&&w.physY<$)})}function Ne(t,n,e){const i=Math.max(0,n.radius-e)**2;return t.map(o=>{const u=o.physX-n.center.x,r=o.physY-n.center.y;return u*u+r*r>i?{...o,edgeExcluded:!0}:o})}function Ve(t,n,e){const c=t.filter(R),i=t.filter(s=>!R(s)),o=new Map;for(const s of c){let m=o.get(s.x);m||(m=new Map,o.set(s.x,m)),m.set(s.y,(m.get(s.y)??0)+1)}const u=new Set(e);function r(s,m){const f=s.hbin,l=m.hbin;if(f===void 0||l===void 0)return!1;const p=u.has(f),h=u.has(l);return n==="best"?h!==p?h:l<f:h!==p?p:l>f}const g=new Map;for(const s of c){const m=V(s),f=g.get(m);n==="first"&&f||(n==="best"||n==="worst")&&f&&!r(f,s)||g.set(m,s)}return[...Array.from(g.values()).map(s=>{const f=o.get(s.x)?.get(s.y)??1;return f>1?{...s,retestCount:f}:s}),...i]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const c=t.testPass?.[n];if(c!==void 0)return c;if(e!==void 0&&!isParametricTest(e)){const i=t.testValues?.[n];if(i===0||i===1)return i===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function _(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function ae(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function de(t,n){return t.map(({code:e,message:c})=>({code:e,message:c,severity:e==="non-standard-diameter"||e==="diameter-exceeds-die-extent"?"warning":"error",confidence:n.wafer.confidence}))}const Be="do not fit inside the supplied",_e="Inferred wafer diameter",Ye="The probed dies reach only",Le=.75,Xe=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const je=.02;function fe(t,n){return n.reduce((e,c)=>Math.abs(c-t)<Math.abs(e-t)?c:e)}function Ke(t,n){if(!n.length)return!0;const e=fe(t,n);return Math.abs(e-t)/e<=je}function ze(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=$e(!Array.isArray(t)&&t.layout?We(t):t),{debug:c,...i}=n??{},o=[];if(e.derivedTests){const d=new Set(e.passBins);if(e.lotStackOpts){const y=e.lotStackOpts.results.map(v=>J(v,e.testDefs,e.derivedTests,d));e.lotStackOpts={...e.lotStackOpts,results:y.map(v=>v.results)},e.testDefs=y[0]?.testDefs??e.testDefs,o.push(...y[0]?.warnings??[])}else{const y=J(e.results,e.testDefs,e.derivedTests,d);e.results=y.results,e.testDefs=y.testDefs,o.push(...y.warnings)}}const u=e.lotStackOpts?Ie(e.lotStackOpts,e.testDefs):e.results;for(let d=0;d<Math.min(5,u.length);d++){const y=u[d];if(typeof y.x=="string"||typeof y.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const d of u)if(d.x==null!=(d.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(d.x)}, y=${String(d.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const r=Ve(u,e.retestPolicy,e.passBins),g={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}},a=[];if(e.explicitDies){let d=e.explicitDies;const y=r.filter(R),v=r.filter(N=>!R(N));if(y.length>0){const N=new Map(y.map(A=>[V(A),A]));d=d.map(A=>{const H=N.get(V(A));return H?_(A,H):A})}const I=e.waferOpts?.diameter??300,F=j({diameter:I,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});F.orientation!==0&&(d=U(d,F));const q=oe(e.reticleOpts,F,d,1,1,0,0,0,0,F.orientation),he=i.showReticle??e.reticleOpts!==void 0,L=Q(F,d,{...i,reticles:q,showReticle:he,plotMode:ae(r,i),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),me=v.map((N,A)=>_({id:`unpositioned_${A}`,width:d[0]?.width??1,height:d[0]?.height??1},N)),X=[...d,...me];return{wafer:F,dies:X,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:g,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...de(a,g),...o,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:ie(X),passBins:e.passBins,ringCount:e.ringCount,yield:re(X,e.passBins,e.edgeDieYieldMode),reticles:q,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const s=r.filter(R),m=r.filter(d=>!R(d)),f=s.map(d=>({x:d.x,y:d.y})),l=Ce(f,e.dieOpts,e.waferOpts?.diameter);g.diePitch={confidence:l.confidence,units:l.units};const{pitchX:p,pitchY:h}=l,x=l.units,C=Ee(s,e.dieOpts),D=Oe(f);g.grid={confidence:D.confidence};const{offsetX:b,offsetY:S}=Fe(f,C,D),{flipX:W,flipY:$}=se(e.dieOpts,C),{colMidX:w,colMidY:P,anchored:ce}=Pe(f,b,S,p,h,e.waferOpts?.center);let M=e.waferOpts?.diameter;const B=f.map(d=>({x:(Math.round(d.x)-b)*p-w,y:(Math.round(d.y)-S)*h-P})),E=K(B.map(({x:d,y})=>Math.hypot(Math.abs(d)+p/2,Math.abs(y)+h/2)));if(M===void 0)if(f.length>0)if(x==="mm"){const d=ve(B,{minRadius:E});M=d.diameter,g.wafer={confidence:d.confidence,method:d.method};const y=ze(e.standardDiameters);if(!Ke(M,y)){const v=fe(M,y);a.push({code:"non-standard-diameter",message:`${_e} ${M.toFixed(1)} mm is not a standard wafer size (nearest is ${v} mm). The diameter was derived from the die extent, which assumes the probed grid reaches the wafer edge \u2014 a non-standard result usually means it does not, because the outer dies were never probed. Every die is then placed against a wafer that is too small, which moves dies between rings and changes ring and edge findings. Supply waferConfig.diameter.`})}}else M=E*2,g.wafer={confidence:l.confidence*.8,method:"extent"};else M=x==="mm"?300:30,g.wafer={confidence:0,method:"default"};if(!ce&&f.length>0&&Te(f,b,S)&&(g.wafer.method="inferred-partial",a.push({code:"partial-coverage",message:"Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly."})),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&E>0&&f.length>=Xe){const d=E/(M/2);d<Le&&a.push({code:"diameter-exceeds-die-extent",message:`${Ye} ${(d*100).toFixed(0)}% of the ${M} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`})}if(e.waferOpts?.diameter!==void 0){const d=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(d&&E>M/2+1e-9){const y=B.filter(({x:v,y:I})=>Math.hypot(Math.abs(v)+p/2,Math.abs(I)+h/2)>M/2+1e-9).length;a.push({code:"geometry-conflict",message:`${y} of ${B.length} probed die positions ${Be} ${M} mm wafer at the supplied die pitch of ${p} \xD7 ${h} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(E*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`})}}const k=j({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let O=s.map(d=>{const y=Math.round(d.x)-b,v=Math.round(d.y)-S,I={id:`${y}_${v}`,x:y,y:v,physX:y*p-w,physY:v*h-P,width:p,height:h,insideWafer:!0,partial:!1};return _(I,d)});(b!==0||S!==0)&&(O=O.map(d=>({...d,x:d.x+b,y:d.y+S,id:`${d.x+b}_${d.y+S}`}))),O=U(O,k),(W||$)&&(O=pe(O,{flipX:W,flipY:$},k.center));const z=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(O=Ne(O,k,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>k.radius&&z.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${k.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const G=oe(e.reticleOpts,k,O,p,h,b,S,w,P,k.orientation,W,$),le=i.showReticle??e.reticleOpts!==void 0,T=Q(k,O,{...i,reticles:G,showReticle:le,plotMode:ae(r,i),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,dataAxisFlip:{x:W,y:$},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ue=m.map((d,y)=>_({id:`unpositioned_${y}`,width:p,height:h},d)),Y=[...O,...ue];return{wafer:k,dies:Y,view:T,reticleConfig:e.reticleOpts,units:x,inference:g,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...de(a,g),...z,...o,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:T.plotMode,metadata:T.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:ie(Y),passBins:e.passBins,ringCount:e.ringCount,yield:re(Y,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:T.aggrMethod,lotSize:T.lotSize}}
1
+ import{createWafer as z}from"../core/wafer.js";import{isYieldEligibleDie as xe,getDieKey as B,hasPosition as E}from"../core/dies.js";import{applyOrientation as ee,transformDies as De,clipDiesToWafer as Me}from"../core/transforms.js";import{generateDies as ve}from"../core/dies.js";import{affineRotation as Ce,affineMirror as Oe,affineCompose as Se,affinePoint as $e}from"../core/transforms.js";import{inferWaferFromXY as ke}from"../core/inference/wafer.js";import{resolveGridPitch as Ae}from"../core/inference/pitch.js";import{assignGridIndices as Ne}from"../core/inference/grid.js";import{generateReticleGrid as Re}from"../core/reticle.js";import{applyDerivedTests as te}from"./derivedTests/apply.js";import{buildView as ne}from"./buildView.js";import{maxOf as G,minOf as se,modeOf as We}from"../core/utils.js";import{aggregateValues as ie,aggregateBinCounts as Ee}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function Ie(t){const n=(f,i)=>({ringCount:f,ringCountWarning:{code:"analysis-option-corrected",severity:"warning",message:`ringCount=${String(t)} ${i} (using ${f}).`}});if(t===void 0)return{ringCount:4,ringCountWarning:void 0};if(typeof t!="number"||!Number.isFinite(t))return n(4,"is not a finite number");const e=Math.max(1,Math.round(t));return e===t?{ringCount:t,ringCountWarning:void 0}:n(e,"is not a whole number of at least 1")}function re(t){const n=Array.isArray(t)?{}:t,e=Array.isArray(n.lotStack?.results)?n.lotStack.results:[],f=(Array.isArray(t)?[t]:[n.results,...e]).filter(h=>Array.isArray(h)),i={hbin:0,sbin:0,testValues:0,testPass:0};let o;const u=(h,S)=>{i[h]++,o??=`${h} ${JSON.stringify(S)}`},s={bins:0,positions:0,testNumbers:0,values:0,sites:0,orientation:0};let m=0;const c=h=>Number.isInteger(h)&&h>=0&&h<=oe,a=h=>{typeof h=="number"&&(Number.isNaN(h)?m++:c(h)||s.bins++)},g=h=>{typeof h=="number"&&!(Number.isInteger(h)&&Math.abs(h)<=H)&&s.positions++};for(const h of f){const S=new Set,w=new Set;for(let v=0;v<h.length;v+=v<50?1:100){const r=h[v];if(!(r===null||typeof r!="object")){if(r.testValues&&typeof r.testValues=="object")for(const $ of Object.keys(r.testValues))S.add($);if(r.testPass&&typeof r.testPass=="object")for(const $ of Object.keys(r.testPass))w.add($)}}const p=[...S],D=[...w];for(const v of new Set([...p,...D])){const r=Number(v);Number.isInteger(r)&&r>=0&&r<=ae||s.testNumbers++}for(const v of h){if(v===null||typeof v!="object")continue;const r=v;r.hbin!=null&&typeof r.hbin!="number"&&u("hbin",r.hbin),r.sbin!=null&&typeof r.sbin!="number"&&u("sbin",r.sbin),a(r.hbin),a(r.sbin),g(r.x),g(r.y),typeof r.siteNum=="number"&&!(Number.isInteger(r.siteNum)&&r.siteNum>=0&&r.siteNum<=255)&&s.sites++;const $=r.testValues;if($&&typeof $=="object")for(let N=0;N<p.length;N++){const O=$[p[N]];O!=null&&(typeof O!="number"?u("testValues",O):Number.isFinite(O)||s.values++)}const I=r.testPass;if(I&&typeof I=="object")for(let N=0;N<D.length;N++){const O=I[D[N]];O!=null&&typeof O!="boolean"&&u("testPass",O)}}}const y=n.waferConfig?.orientation;typeof y=="number"&&![0,90,180,270].includes((y%360+360)%360)&&s.orientation++;const l=[],b=(h,S,w=`${S}s`)=>`${h} ${h===1?S:w}`,x=[];if(i.hbin&&x.push(b(i.hbin,"hard bin")),i.sbin&&x.push(b(i.sbin,"soft bin")),i.testValues&&x.push(b(i.testValues,"test value")),i.testPass&&x.push(b(i.testPass,"pass/fail verdict")),x.length){const h=`buildWaferMap received ${x.join(", ")} of the wrong type (for example ${o}). Bins and test values must be numbers and verdicts true/false \u2014 a bin of "1" is not pass bin 1, so those dies count as fails and yield is wrong, and a test value given as text is not plotted or analysed correctly. Convert them with Number() (and verdicts to booleans) and rebuild.`;console.warn(`[wafermap] ${h}`),l.push({code:"input-values-not-numbers",message:h,severity:"error"})}const M=[];if(s.bins&&M.push(`${b(s.bins,"bin")} (legal: whole numbers 0\u2013${oe})`),s.positions&&M.push(`${b(s.positions,"coordinate")} (legal: whole numbers \u2212${H} to ${H})`),s.testNumbers&&M.push(`${b(s.testNumbers,"test number")} (legal: whole numbers 0\u2013${ae})`),s.values&&M.push(`${b(s.values,"test value")} that ${s.values===1?"is":"are"} not finite`),s.sites&&M.push(`${b(s.sites,"site number")} (legal: 0\u2013255)`),s.orientation&&M.push("a waferConfig.orientation other than 0, 90, 180 or 270"),M.length||m){const h=[];M.length&&h.push(`buildWaferMap received values outside the STDF V4 ranges: ${M.join("; ")}. They are used as given for now; a future release treats them as missing.`),m&&h.push(`${b(m,"bin was","bins were")} NaN and ${m===1?"is":"are"} treated as no bin.`);const S=h.join(" ");console.warn(`[wafermap] ${S}`),l.push({code:"input-values-outside-stdf",message:S,severity:"warning"})}return l}const oe=32767,H=32767,ae=4294967295;function de(t){const n=(c,a)=>c!==null&&typeof c=="object"&&c[a]!==void 0,e=Array.isArray(t)?{}:t,f=[],i=(c,a,g)=>{c&&f.push(`\`${a}\` (now \`${g}\`)`)};i(n(e,"data"),"data","results"),i(n(e,"die"),"die","dieConfig"),i(n(e,"stack"),"stack","lotStack"),i(n(e.dieConfig,"origin"),"dieConfig.origin","dieConfig.coordinateOrigin"),i(n(e.waferConfig,"flat"),"waferConfig.flat","waferConfig.notch"),i(n(e.reticleConfig,"anchor"),"reticleConfig.anchor","reticleConfig.anchorDie"),i(n(e.lotStack,"aggr"),"lotStack.aggr","lotStack.method"),i(Array.isArray(e.testDefs)&&e.testDefs.some(c=>n(c,"index")&&!n(c,"testNumber")),"TestDef.index","TestDef.testNumber");const o=n(e.lotStack,"results")?e.lotStack.results:[],u=(Array.isArray(t)?[t]:[e.results,...Array.isArray(o)?o:[]]).filter(c=>Array.isArray(c));if(i(u.some(c=>c.slice(0,100).some(a=>n(a,"values"))),"values","testValues"),!f.length)return;const s=f.length>1,m=`buildWaferMap ignored ${f.join(", ")}: ${s?"these names were":"that name was"} removed in an earlier release, so what ${s?"they":"it"} described is not on this map. Rename and rebuild.`;return console.warn(`[wafermap] ${m}`),{code:"input-field-removed",message:m,severity:"error"}}function Te(t){if(t.results!==void 0||t.lotStack!==void 0)throw new Error("buildWaferMap: pass `layout`, `results` or `lotStack` \u2014 only one.");const n=t.waferConfig?.diameter,e=t.dieConfig?.width,f=t.dieConfig?.height;if(!(n>0&&e>0&&f>0))throw new Error("buildWaferMap: `layout` needs waferConfig.diameter and dieConfig.width and height, all positive.");const i=z({diameter:n,notch:t.waferConfig?.notch,orientation:0}),o=Me(ve(i,{width:e,height:f}),i,{width:e,height:f}).filter(a=>!a.partial),{flipX:u,flipY:s}=fe(t.dieConfig,{type:"center"}),{layout:m,...c}=t;return{...c,results:o.map(a=>({x:u?-a.x:a.x,y:s?-a.y:a.y})),waferConfig:{...t.waferConfig,center:{x:0,y:0}},dieConfig:{...t.dieConfig,coordinateOrigin:{type:"center"}}}}function Fe(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],ringCount:4,ringCountWarning:void 0,removedFieldWarning:de(t),inputValueWarnings:re(t),standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,derivedTests:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],...Ie(t.ringCount),removedFieldWarning:de(t),inputValueWarnings:re(t),standardDiameters:t.standardDiameters,testDefs:t.testDefs?.length?t.testDefs:void 0,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,derivedTests:t.derivedTests?.length?t.derivedTests:void 0,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function _e(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Pe(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let f=1/0,i=-1/0,o=1/0,u=-1/0;for(const s of t)s.x<f&&(f=s.x),s.x>i&&(i=s.x),s.y<o&&(o=s.y),s.y>u&&(u=s.y);return{offsetX:Math.round((i+f)/2),offsetY:Math.round((u+o)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Ve(t,n,e,f,i,o){if(o)return{colMidX:(Math.round(o.x)-n)*f,colMidY:(Math.round(o.y)-e)*i,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let u=1/0,s=-1/0,m=1/0,c=-1/0;for(const a of t){const g=Math.round(a.x)-n,y=Math.round(a.y)-e;g<u&&(u=g),g>s&&(s=g),y<m&&(m=y),y>c&&(c=y)}return{colMidX:(u+s)/2*f,colMidY:(m+c)/2*i,anchored:!1}}function Be(t,n,e){let f=1/0,i=-1/0,o=1/0,u=-1/0,s=0,m=0;for(const g of t){const y=Math.round(g.x)-n,l=Math.round(g.y)-e;s+=y,m+=l,y<f&&(f=y),y>i&&(i=y),l<o&&(o=l),l>u&&(u=l)}const c=t.length,a=(g,y,l)=>{const b=(l-y)/2;return b<=0?0:Math.abs(g/c-(y+l)/2)/b};return a(s,f,i)>.11||a(m,o,u)>.11}function fe(t,n){let e=t?.xAxisDirection==="left",f=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(f=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:f}}function Ye(t,n){const{method:e,targetBin:f}=t,i=t.results.map(o=>o.filter(E));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const o=new Set((n??[]).filter(a=>!isParametricTest(a)).map(a=>a.testNumber)),u=new Set;for(const a of i)for(const g of a)if(g.testValues)for(const y of Object.keys(g.testValues)){const l=Number(y);o.has(l)||u.add(l)}if(u.size===0)return ie(i,e);const s=[...u],m=s.map(a=>ie(i,e,a)),c=new Map;for(let a=0;a<s.length;a++){const g=s[a];for(const y of m[a]){const l=B(y);c.has(l)||c.set(l,{template:y,testValues:{}});const b=c.get(l),x=y.testValues?.[0];x!==void 0&&(b.testValues[g]=x)}}return[...c.values()].map(({template:a,testValues:g})=>({...a,testValues:Object.keys(g).length>0?g:void 0}))}if(e==="countBin"||e==="percent"){if(f===void 0)return[];const o=Ee(i,f,"hard");if(e==="countBin")return o;const u=i.length;return o.map(s=>({...s,testValues:{0:u>0?(s.testValues?.[0]??0)/u*100:0}}))}if(e==="mode"){const o=new Map;for(const s of i)for(const m of s){const c=B(m);o.has(c)||o.set(c,[]),o.get(c).push(m)}const u=[];for(const[s,m]of o){const c=s.split(","),a=Number(c[0]),g=Number(c[1]),y=m.map(b=>b.hbin).filter(b=>b!==void 0),l=We(y);l!==null&&u.push({x:a,y:g,hbin:l})}return u}return[]}function ce(t){const n=t.filter(E),e=n.length,f=n.filter(o=>o.edgeExcluded).length,i=n.filter(o=>dieHasTestData(o)||o.hbin!==void 0||o.sbin!==void 0).length;return{filledDies:i,totalDies:e,edgeExcludedDies:f,unpositionedDies:t.length-n.length,ratio:e>0?i/e:0}}function le(t,n,e="exclude"){const f=new Set(n),o=t.filter(l=>!l.partial).filter(l=>l.edgeExcluded).length,u=t.filter(l=>l.partial).length;let s=0,m=0,c=!1;for(const l of t){if(!xe(l))continue;const b=l.hbin??l.sbin;b!==void 0&&(c=!0,f.has(b)?s++:m++)}const a=s+m,g=c&&a>0?s/a*100:null;let y=null;if(e==="denominator-only"){const l=a+o;y=c&&l>0?s/l*100:null}return{passDies:s,failDies:m,edgeExcludedDies:o,partialDies:u,totalDies:a,yieldPercent:g,yieldPercentGross:y}}function ue(t,n,e,f,i,o=0,u=0,s=0,m=0,c=0,a=!1,g=!1){if(!t)return[];const y=t.anchorDie??{x:0,y:0},l=Re(n,{width:t.width,height:t.height,diePitchX:f,diePitchY:i,anchorDie:{x:y.x-o,y:y.y-u},gridOrigin:{x:-s,y:-m}}),b=Se(Oe(a,g,n.center.x,n.center.y),Ce(c,n.center.x,n.center.y));return l.filter(x=>{const M=x.width/2,h=x.height/2,S=[[x.x-M,x.y-h],[x.x+M,x.y-h],[x.x+M,x.y+h],[x.x-M,x.y+h]].map(([r,$])=>$e(b,r,$)),w=se(S.map(r=>r.x)),p=G(S.map(r=>r.x)),D=se(S.map(r=>r.y)),v=G(S.map(r=>r.y));return e.some(r=>r.physX>=w&&r.physX<p&&r.physY>=D&&r.physY<v)})}function Le(t,n,e){const i=Math.max(0,n.radius-e)**2;return t.map(o=>{const u=o.physX-n.center.x,s=o.physY-n.center.y;return u*u+s*s>i?{...o,edgeExcluded:!0}:o})}const Xe=.2;function je(t,n,e){const f=new Set(e);function i(w,p){if(n==="last")return!0;if(n==="first")return!1;const D=L(w.hbin),v=L(p.hbin);if(D===void 0||v===void 0)return!1;const r=f.has(D),$=f.has(v);return n==="best"?$!==r?$:v<D:$!==r?r:v>D}const o=(w,p)=>p.supersedes!==void 0||i(w,p),u=w=>{const p=w.partId===void 0?"":String(w.partId).trim();return p===""?void 0:p},s=t.filter(w=>!E(w)),m=new Map;for(const w of s){const p=u(w);p!==void 0&&m.set(p,(m.get(p)??0)+1)}const a=(s.length===0?0:Math.max(0,...m.values())/s.length)<=Xe,g=new Map,y=[],l=new Map;let b=0,x=0;for(const w of t){const p=u(w);let D;if(E(w)?D=`p:${B(w)}`:a&&p!==void 0?D=`i:${p}`:D=`u:${x++}`,w.supersedes==="partId"&&p!==void 0){const r=l.get(p);r!==void 0&&r!==D&&g.has(r)&&g.delete(r)}const v=g.get(D);v===void 0?(g.set(D,{winner:w,count:1}),y.push(D)):(v.count++,D.startsWith("i:")&&b++,o(v.winner,w)&&(v.winner=w)),p!==void 0&&l.set(p,D)}const M=[],h=[...y.filter(w=>w.startsWith("p:")),...y.filter(w=>!w.startsWith("p:"))];for(const w of h){const p=g.get(w);p!==void 0&&M.push(p.count>1?{...p.winner,retestCount:p.count}:p.winner)}const S=b>0?{code:"retests-by-part-id",severity:"info",message:`${b} record${b===1?"":"s"} with no position shared a part ID with an earlier record and ${b===1?"was":"were"} treated as a retest of it.`}:void 0;return{results:M,warning:S}}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const f=t.testPass?.[n];if(f!==void 0)return f;if(e!==void 0&&!isParametricTest(e)){const i=t.testValues?.[n];if(i===0||i===1)return i===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function L(t){return t===void 0||Number.isNaN(t)?void 0:t}function X(t,n){const e={},f=L(n.hbin),i=L(n.sbin);return f!==void 0&&(e.hbin=f),i!==void 0&&(e.sbin=i),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function he(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function pe(t,n){return t.map(({code:e,message:f})=>({code:e,message:f,severity:e==="non-standard-diameter"||e==="diameter-exceeds-die-extent"?"warning":"error",confidence:n.wafer.confidence}))}const Ke="do not fit inside the supplied",Ue="Inferred wafer diameter",ze="The probed dies reach only",Ge=.75,He=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const qe=.02;function me(t,n){return n.reduce((e,f)=>Math.abs(f-t)<Math.abs(e-t)?f:e)}function Je(t,n){if(!n.length)return!0;const e=me(t,n);return Math.abs(e-t)/e<=qe}function Qe(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=Fe(!Array.isArray(t)&&t.layout?Te(t):t),{debug:f,...i}=n??{},o=[];if(e.derivedTests){const d=new Set(e.passBins);if(e.lotStackOpts){const C=e.lotStackOpts.results.map(k=>te(k,e.testDefs,e.derivedTests,d));e.lotStackOpts={...e.lotStackOpts,results:C.map(k=>k.results)},e.testDefs=C[0]?.testDefs??e.testDefs,o.push(...C[0]?.warnings??[])}else{const C=te(e.results,e.testDefs,e.derivedTests,d);e.results=C.results,e.testDefs=C.testDefs,o.push(...C.warnings)}}const u=e.lotStackOpts?Ye(e.lotStackOpts,e.testDefs):e.results;for(let d=0;d<Math.min(5,u.length);d++){const C=u[d];if(typeof C.x=="string"||typeof C.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const d of u)if(d.x==null!=(d.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(d.x)}, y=${String(d.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const{results:s,warning:m}=je(u,e.retestPolicy,e.passBins),c={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}},a=[];if(e.explicitDies){let d=e.explicitDies;const C=s.filter(E),k=s.filter(V=>!E(V));if(C.length>0){const V=new Map(C.map(W=>[B(W),W]));d=d.map(W=>{const Z=V.get(B(W));return Z?X(W,Z):W})}const P=e.waferOpts?.diameter??300,F=z({diameter:P,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});F.orientation!==0&&(d=ee(d,F));const Q=ue(e.reticleOpts,F,d,1,1,0,0,0,0,F.orientation),be=i.showReticle??e.reticleOpts!==void 0,K=ne(F,d,{...i,reticles:Q,showReticle:be,plotMode:he(s,i),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),we=k.map((V,W)=>X({id:`unpositioned_${W}`,width:d[0]?.width??1,height:d[0]?.height??1},V)),U=[...d,...we];return{wafer:F,dies:U,view:K,reticleConfig:e.reticleOpts,units:"mm",inference:c,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.inputValueWarnings,...m?[m]:[],...pe(a,c),...o,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:K.plotMode,metadata:K.metadata,isLotStack:!1,dataCoverage:ce(U),passBins:e.passBins,ringCount:e.ringCount,yield:le(U,e.passBins,e.edgeDieYieldMode),reticles:Q,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const g=s.filter(E),y=s.filter(d=>!E(d)),l=g.map(d=>({x:d.x,y:d.y})),b=Ae(l,e.dieOpts,e.waferOpts?.diameter);c.diePitch={confidence:b.confidence,units:b.units};const{pitchX:x,pitchY:M}=b,h=b.units,S=_e(g,e.dieOpts),w=Ne(l);c.grid={confidence:w.confidence};const{offsetX:p,offsetY:D}=Pe(l,S,w),{flipX:v,flipY:r}=fe(e.dieOpts,S),{colMidX:$,colMidY:I,anchored:N}=Ve(l,p,D,x,M,e.waferOpts?.center);let O=e.waferOpts?.diameter;const Y=l.map(d=>({x:(Math.round(d.x)-p)*x-$,y:(Math.round(d.y)-D)*M-I})),T=G(Y.map(({x:d,y:C})=>Math.hypot(Math.abs(d)+x/2,Math.abs(C)+M/2)));if(O===void 0)if(l.length>0)if(h==="mm"){const d=ke(Y,{minRadius:T});O=d.diameter,c.wafer={confidence:d.confidence,method:d.method};const C=Qe(e.standardDiameters);if(!Je(O,C)){const k=me(O,C);a.push({code:"non-standard-diameter",message:`${Ue} ${O.toFixed(1)} mm is not a standard wafer size (nearest is ${k} mm). The diameter was derived from the die extent, which assumes the probed grid reaches the wafer edge \u2014 a non-standard result usually means it does not, because the outer dies were never probed. Every die is then placed against a wafer that is too small, which moves dies between rings and changes ring and edge findings. Supply waferConfig.diameter.`})}}else O=T*2,c.wafer={confidence:b.confidence*.8,method:"extent"};else O=h==="mm"?300:30,c.wafer={confidence:0,method:"default"};if(!N&&l.length>0&&Be(l,p,D)&&(c.wafer.method="inferred-partial",a.push({code:"partial-coverage",message:"Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly."})),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&T>0&&l.length>=He){const d=T/(O/2);d<Ge&&a.push({code:"diameter-exceeds-die-extent",message:`${ze} ${(d*100).toFixed(0)}% of the ${O} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`})}if(e.waferOpts?.diameter!==void 0){const d=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(d&&T>O/2+1e-9){const C=Y.filter(({x:k,y:P})=>Math.hypot(Math.abs(k)+x/2,Math.abs(P)+M/2)>O/2+1e-9).length;a.push({code:"geometry-conflict",message:`${C} of ${Y.length} probed die positions ${Ke} ${O} mm wafer at the supplied die pitch of ${x} \xD7 ${M} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(T*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`})}}const R=z({diameter:O,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let A=g.map(d=>{const C=Math.round(d.x)-p,k=Math.round(d.y)-D,P={id:`${C}_${k}`,x:C,y:k,physX:C*x-$,physY:k*M-I,width:x,height:M,insideWafer:!0,partial:!1};return X(P,d)});(p!==0||D!==0)&&(A=A.map(d=>({...d,x:d.x+p,y:d.y+D,id:`${d.x+p}_${d.y+D}`}))),A=ee(A,R),(v||r)&&(A=De(A,{flipX:v,flipY:r},R.center));const q=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(A=Le(A,R,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>R.radius&&q.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${R.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const J=ue(e.reticleOpts,R,A,x,M,p,D,$,I,R.orientation,v,r),ge=i.showReticle??e.reticleOpts!==void 0,_=ne(R,A,{...i,reticles:J,showReticle:ge,plotMode:he(s,i),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,dataAxisFlip:{x:v,y:r},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ye=y.map((d,C)=>X({id:`unpositioned_${C}`,width:x,height:M},d)),j=[...A,...ye];return{wafer:R,dies:j,view:_,reticleConfig:e.reticleOpts,units:h,inference:c,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.inputValueWarnings,...m?[m]:[],...pe(a,c),...q,...o,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:_.plotMode,metadata:_.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:ce(j),passBins:e.passBins,ringCount:e.ringCount,yield:le(j,e.passBins,e.edgeDieYieldMode),reticles:J,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:_.aggrMethod,lotSize:_.lotSize}}