@wafertools/wafermap 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +162 -1
  3. package/README.md +2 -2
  4. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +2 -12
  5. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  6. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  7. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  8. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +30 -0
  9. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  10. package/dist/packages/canvas-adapter/summaryPanel.d.ts +22 -3
  11. package/dist/packages/canvas-adapter/summaryPanel.js +4 -3
  12. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  13. package/dist/packages/canvas-adapter/toolbar.d.ts +54 -1
  14. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  15. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  16. package/dist/packages/canvas-adapter/userGuideHtml.js +76 -13
  17. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  18. package/dist/packages/canvas-adapter/version.js +1 -1
  19. package/dist/packages/core/aggregates.d.ts +6 -0
  20. package/dist/packages/core/aggregates.js +1 -1
  21. package/dist/packages/core/compact.d.ts +205 -0
  22. package/dist/packages/core/compact.js +3 -0
  23. package/dist/packages/core/passBins.d.ts +6 -0
  24. package/dist/packages/core/passBins.js +1 -1
  25. package/dist/packages/renderer/axisTicks.d.ts +14 -0
  26. package/dist/packages/renderer/axisTicks.js +1 -1
  27. package/dist/packages/renderer/buildView.d.ts +26 -1
  28. package/dist/packages/renderer/buildView.js +1 -1
  29. package/dist/packages/renderer/buildWaferMap.js +1 -1
  30. package/dist/packages/renderer/compactView.d.ts +22 -0
  31. package/dist/packages/renderer/compactView.js +1 -0
  32. package/dist/packages/renderer/xyIndicator.d.ts +27 -0
  33. package/dist/packages/renderer/xyIndicator.js +1 -0
  34. package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
  35. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  36. package/dist/packages/stats/analyzeWaferMap.d.ts +36 -1
  37. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  38. package/dist/packages/stats/binRows.d.ts +38 -0
  39. package/dist/packages/stats/binRows.js +1 -0
  40. package/dist/packages/stats/filterFindings.d.ts +25 -0
  41. package/dist/packages/stats/filterFindings.js +1 -1
  42. package/dist/packages/stats/findingText.d.ts +9 -0
  43. package/dist/packages/stats/findingText.js +2 -0
  44. package/dist/packages/stats/lotDrift.d.ts +16 -0
  45. package/dist/packages/stats/lotDrift.js +1 -0
  46. package/dist/packages/stats/lotPattern.d.ts +33 -0
  47. package/dist/packages/stats/lotPattern.js +1 -0
  48. package/dist/packages/stats/math.d.ts +51 -0
  49. package/dist/packages/stats/math.js +1 -1
  50. package/dist/packages/stats/patternClassification.d.ts +31 -0
  51. package/dist/packages/stats/patternClassification.js +1 -1
  52. package/dist/packages/stats/presentation.d.ts +42 -0
  53. package/dist/packages/stats/presentation.js +1 -0
  54. package/dist/packages/stats/regions.d.ts +9 -0
  55. package/dist/packages/stats/regions.js +1 -1
  56. package/dist/packages/stats/renderSummaryReport.d.ts +15 -2
  57. package/dist/packages/stats/renderSummaryReport.js +32 -27
  58. package/dist/packages/stats/reportHtml.d.ts +54 -9
  59. package/dist/packages/stats/reportHtml.js +130 -47
  60. package/dist/packages/stats/synthesis.d.ts +66 -0
  61. package/dist/packages/stats/synthesis.js +2 -0
  62. package/dist/packages/stats/types.d.ts +1 -1
  63. package/dist/packages/stats/yieldRows.d.ts +31 -0
  64. package/dist/packages/stats/yieldRows.js +1 -0
  65. package/llms.txt +1 -1
  66. package/package.json +1 -1
  67. package/dist/packages/stats/findingsNarrative.d.ts +0 -3
  68. package/dist/packages/stats/findingsNarrative.js +0 -1
@@ -72,7 +72,7 @@ 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-27T14:44:40.685Z">v0.32.0</span></h1>
75
+ <h1 id="wafer-map-user-guide">Wafer Map \u2014 User Guide<span class="wmap-guide-version" title="Built 2026-10-02T10:01:50.772Z">v0.33.0</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
@@ -97,7 +97,10 @@ display does not change the coordinate labels \u2014 a die at (3, \u22122) alway
97
97
  <h3 id="12-wafer-orientation">1.2 Wafer orientation</h3>
98
98
  <p>The notch (shown as a V-notch or flat edge) marks the physical reference edge of
99
99
  the wafer as configured. Use the <strong>Orientation</strong> toolbar controls to rotate or flip
100
- the display to match your convention; die coordinates are unaffected.</p>
100
+ the display to match your convention; die coordinates are unaffected. The first rotate
101
+ or flip also switches on the <strong>XY indicator</strong> (the +X/+Y arrows in the Overlays menu), so
102
+ you can see which way the die coordinates now run. It stays on until you switch it off
103
+ there; <strong>Reset orientation</strong> leaves it as it is.</p>
101
104
  <div data-wmap-demo="bin-map" class="wmap-demo"></div><p><em>An example wafer bin map above and below the same wafer rotated 90\xB0. The notch has moved, but die coordinates \u2014 shown in tooltips \u2014 remain their original grid/prober values.</em></p>
102
105
  <div data-wmap-demo="orientation" class="wmap-demo"></div><h3 id="13-die-appearance">1.3 Die appearance</h3>
103
106
  <table>
@@ -281,7 +284,7 @@ shows die tooltips \u2014 a separate thing from the toolbar, which is always pre
281
284
  <tr>
282
285
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yIDEyczMuNi03IDEwLTcgMTAgNyAxMCA3LTMuNiA3LTEwIDctMTAtNy0xMC03eiIvPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjMiLz48L3N2Zz4K" width="20" height="20"></td>
283
286
  <td>Overlays</td>
284
- <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>
287
+ <td>Check-menu of optional display layers: axis labels, 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>Compact layout</strong> and <strong>Layout diagnostics</strong> sit at the top of the menu (see <a href="#compact-layout" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'compact-layout\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Compact layout</a>). <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>
285
288
  </tr>
286
289
  <tr>
287
290
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Im0xNC42MjIgMTcuODk3LTEwLjY4LTIuOTEzIi8+PHBhdGggZD0iTTE4LjM3NiAyLjYyMmExIDEgMCAxIDEgMy4wMDIgMy4wMDJMMTcuMzYgOS42NDNhLjUuNSAwIDAgMCAwIC43MDdsLjk0NC45NDRhMi40MSAyLjQxIDAgMCAxIDAgMy40MDhsLS45NDQuOTQ0YS41LjUgMCAwIDEtLjcwNyAwTDguMzU0IDcuMzQ4YS41LjUgMCAwIDEgMC0uNzA3bC45NDQtLjk0NGEyLjQxIDIuNDEgMCAwIDEgMy40MDggMGwuOTQ0Ljk0NGEuNS41IDAgMCAwIC43MDcgMHoiLz48cGF0aCBkPSJNOSA4Yy0xLjgwNCAyLjcxLTMuOTcgMy40Ni02LjU4MyAzLjk0OGEuNTA3LjUwNyAwIDAgMC0uMzAyLjgxOWw3LjMyIDguODgzYTEgMSAwIDAgMCAxLjE4NS4yMDRDMTIuNzM1IDIwLjQwNSAxNiAxNi43OTIgMTYgMTUiLz48L3N2Zz4K" width="20" height="20"></td>
@@ -306,7 +309,7 @@ shows die tooltips \u2014 a separate thing from the toolbar, which is always pre
306
309
  <tr>
307
310
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBvbHlnb24gcG9pbnRzPSIxNi4yNCA3Ljc2IDE0LjEyIDE0LjEyIDcuNzYgMTYuMjQgOS44OCA5Ljg4IDE2LjI0IDcuNzYiIGZpbGw9IiMzNzQxNTEiIHN0cm9rZT0ibm9uZSIvPjwvc3ZnPgo=" width="20" height="20"></td>
308
311
  <td>Orientation</td>
309
- <td>Menu of display transforms: <strong>Rotate 90\xB0 clockwise</strong> (applies cumulatively), <strong>Flip horizontal</strong>, <strong>Flip vertical</strong>, and <strong>Reset orientation</strong>. These change only how the wafer is drawn \u2014 die coordinates in tooltips and labels are always the original values, whatever the orientation. Reset is worth knowing about: rotation and mirroring do not combine in the order you applied them, so undoing a few clicks by clicking again does not reliably get you back \u2014 Reset does, and is greyed when you are already there.</td>
312
+ <td>Menu of display transforms: <strong>Rotate 90\xB0 clockwise</strong> (applies cumulatively), <strong>Flip horizontal</strong>, <strong>Flip vertical</strong>, and <strong>Reset orientation</strong>. The first rotate or flip also switches on the XY indicator (see Overlays), which stays on until you switch it off. These change only how the wafer is drawn \u2014 die coordinates in tooltips and labels are always the original values, whatever the orientation. Reset is worth knowing about: rotation and mirroring do not combine in the order you applied them, so undoing a few clicks by clicking again does not reliably get you back \u2014 Reset does, and is greyed when you are already there.</td>
310
313
  </tr>
311
314
  <tr>
312
315
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Im0xMyAxMy41IDItMi41LTItMi41Ii8+PHBhdGggZD0ibTIxIDIxLTQuMy00LjMiLz48cGF0aCBkPSJNOSA4LjUgNyAxMWwyIDIuNSIvPjxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjgiLz48L3N2Zz4K" width="20" height="20"></td>
@@ -351,7 +354,7 @@ shows die tooltips \u2014 a separate thing from the toolbar, which is always pre
351
354
  <tr>
352
355
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAzaDZ2NiIvPjxwYXRoIGQ9Ik05IDIxSDN2LTYiLz48cGF0aCBkPSJNMjEgM2wtNyA3Ii8+PHBhdGggZD0iTTMgMjFsNy03Ii8+PC9zdmc+Cg==" width="20" height="20"></td>
353
356
  <td>Expand</td>
354
- <td>Opens the map in an enlarged modal overlay. A maximise button in the modal grows it to fill the window (or press <strong>F</strong>). Press <strong>Esc</strong> or click outside to close. Useful for detailed inspection without changing the main view. Works in the Insights view too, where it opens the whole chart suite in a wide modal \u2014 useful because those charts interact and are best read side by side. Individual charts also have their own expand button.</td>
357
+ <td>Opens the map in an enlarged modal overlay. Opening the Summary panel in it widens the modal by the panel&#39;s width, so the map keeps its size. A maximise button in the modal grows it to fill the window (or press <strong>F</strong>). Press <strong>Esc</strong> or click outside to close. Useful for detailed inspection without changing the main view. Works in the Insights view too, where it opens the whole chart suite in a wide modal \u2014 useful because those charts interact and are best read side by side. Individual charts also have their own expand button.</td>
355
358
  </tr>
356
359
  <tr>
357
360
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMS44IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMy45OTcgNGEyIDIgMCAwIDEgMS43NiAxLjA1bC40ODYuOUEyIDIgMCAwIDAgMTguMDAzIDdIMjBhMiAyIDAgMCAxIDIgMnY5YTIgMiAwIDAgMS0yIDJINGEyIDIgMCAwIDEtMi0yVjlhMiAyIDAgMCAxIDItMmgxLjk5N2EyIDIgMCAwIDAgMS43NTktMS4wNDhsLjQ4OS0uOTA0QTIgMiAwIDAgMSAxMC4wMDQgNHoiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjEzIiByPSIzIi8+PC9zdmc+Cg==" width="20" height="20"></td>
@@ -396,6 +399,8 @@ highlighting one bin at a time.</p>
396
399
  <h3 id="overlays">Overlays</h3>
397
400
  <p>Use the <strong>Overlays</strong> menu to toggle optional display layers on and off:</p>
398
401
  <ul>
402
+ <li><strong>Axis labels</strong> \u2014 the die coordinates along the bottom and left edges. They appear when you zoom; this row shows
403
+ them all the time, or hides them. <strong>Clear overlays</strong> returns to showing them on zoom</li>
399
404
  <li><strong>XY axis indicator</strong> \u2014 shows X and Y axis lines through the wafer centre</li>
400
405
  <li><strong>Ring boundaries</strong> \u2014 concentric ring divisions that match the spatial analysis zones</li>
401
406
  <li><strong>Quadrant lines</strong> \u2014 divides the wafer into N, S, E, W quadrants</li>
@@ -405,6 +410,33 @@ highlighting one bin at a time.</p>
405
410
  <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>
406
411
  </ul>
407
412
  <div data-wmap-demo="overlays" class="wmap-demo"></div><p><em>Ring boundaries, quadrant lines, and XY indicator all active.</em></p>
413
+ <h3 id="compact-layout">Compact layout</h3>
414
+ <p>On a multi-project wafer, each reticle holds only a few of your dies, so the map is mostly empty and the dies you care
415
+ about are small. When the occupied columns and rows repeat at a regular pitch, the Overlays menu offers
416
+ <strong>Compact layout</strong>. Switch it on and the empty rows and columns disappear, so your dies are drawn as one grid at a
417
+ much larger size, with each group of dies outlined so you can still see the reticles they came from.</p>
418
+ <ul>
419
+ <li><strong>Nothing is left out.</strong> Only the arrangement changes. Every die is still drawn and counted, so the legend, yield
420
+ and statistics are the same as in the wafer view. A die that has no bin (for example one that carries metadata only)
421
+ keeps its place, shown in the no-data grey.</li>
422
+ <li><strong>Coordinates stay original.</strong> Hovering a die and the axis labels give its real die coordinates, not its position
423
+ in the compact grid.</li>
424
+ <li><strong>Orientation.</strong> The notch marker shows the wafer orientation and follows rotation and flips. The <strong>XY indicator</strong>
425
+ works as it does on the wafer view, drawn in a margin beside the grid.</li>
426
+ <li><strong>No wafer outline.</strong> The wafer circle, ring boundaries, quadrant lines and reticle grid describe the physical
427
+ wafer, which the compact grid no longer follows, so those rows are greyed out while the layout is on.</li>
428
+ <li><strong>Galleries.</strong> All cards share one layout, built from every wafer shown, so wafers can be compared cell for cell.
429
+ Each card is as tall as its map needs, so more of them fit on screen; a quarter turn swaps a card&#39;s width and height.</li>
430
+ <li><strong>Axis labels.</strong> With the Axis labels row on, the first column of each group of dies is labelled with its real die
431
+ coordinate, which marks the reticle boundaries; zoom in far enough and every die is labelled.</li>
432
+ <li><strong>When it is offered.</strong> The layout is offered when the occupied columns and rows repeat. If you supply
433
+ <code>reticleConfig</code>, it is offered when they repeat at the reticle width and height or a multiple of it (a product on every second reticle repeats at twice the width). Random missing dies do not repeat, so
434
+ a wafer that merely has holes is not offered it. Developers can switch it on regardless with
435
+ <code>viewOptions: { compact: true }</code>.</li>
436
+ </ul>
437
+ <p><strong>Layout diagnostics</strong> (also in the Overlays menu) shows what the detector saw as counts and scores only: it holds no
438
+ die positions, bins, test values or wafer names. If a layout is not recognised the way you expect, copy the text or save
439
+ it as a file and send it to whoever supports this software.</p>
408
440
  <h3 id="keyboard-shortcuts">Keyboard shortcuts</h3>
409
441
  <table>
410
442
  <thead>
@@ -624,7 +656,7 @@ there does what it normally does.</p>
624
656
  detected patterns on the wafer. Open it from the toolbar.</p>
625
657
  <p>Each finding shows:</p>
626
658
  <ul>
627
- <li><strong>Severity</strong> \u2014 Unusual, Notable, or Info (ordered most to least significant)</li>
659
+ <li><strong>Severity</strong> \u2014 Unusual, Notable, or Minor (ordered most to least significant)</li>
628
660
  <li><strong>Description</strong> \u2014 plain-language summary of what was detected and where</li>
629
661
  <li><strong>Click to highlight</strong> \u2014 clicking a finding fades the rest of the wafer and outlines the
630
662
  affected dies. A finding about a bin also filters the legend to that bin, so the legend
@@ -698,31 +730,56 @@ highlight the affected dies.</em></p>
698
730
  panel is computed over. For several wafers the heading names them: <strong>Lot
699
731
  LOT123 \xB7 13 wafers</strong> when every wafer comes from that one lot, otherwise
700
732
  <strong>26 wafers from 2 lots</strong> (or just <strong>13 wafers</strong> when the data records no lot).
701
- The panel only says &quot;lot&quot; when it really is one lot. &quot;Mean wafer yield&quot; is an
733
+ The panel only says &quot;lot&quot; when it really is one lot. &quot;Mean per-wafer yield&quot; is an
702
734
  <em>unweighted</em> mean of each wafer&#39;s own yield; it is deliberately not the same
703
735
  statistic as the bin breakdown&#39;s pass-bin share, which weights every die
704
736
  equally. The two agree only when die counts are even across the wafers.</li>
705
- <li><strong>Findings</strong> \u2014 detected anomalies, most severe first. Severity chips narrow
737
+ <li><strong>What stands out</strong> \u2014 the headline (the yield, and for a lot its spread across
738
+ wafers), then up to three items ranked by how many dies each costs, an &quot;also&quot;
739
+ line for any others over a yield point, and a line saying what was compared. An
740
+ item names a region, a wafer or a test and, for a region, the failing bins that make up
741
+ its shortfall; a test is an item when it is outside its limits (or a functional test
742
+ fails) on a share of the dies that clears the same floor. Its three dots and word (High, Moderate or Low impact) say how
743
+ much of the lot it costs: the higher of its share of all the dies (2% moderate, 4% high)
744
+ and its share of the dies that fail (15% moderate, 40% high), so a small area that is most
745
+ of a good lot&#39;s loss still reads as high. Click a name to highlight that finding on the map.
746
+ A <strong>Watch</strong> line (at most two) follows for things that cost no dies yet: a lot of five or more
747
+ wafers whose yield, or a test&#39;s mean, trends up or down across the wafers <em>in the order given</em>
748
+ (said as &quot;input order&quot;: the order is only a physical one if you know it is), and a test whose
749
+ Ppk against its limits is under 1.0.
750
+ A wafer or lot with nothing over a yield point says so: &quot;Nothing stands out&quot;.
751
+ The reports open with the same section, and <strong>Full report</strong> at the foot of it opens the
752
+ report; in a report opened from the panel, clicking a finding&#39;s row shows it on the map.</li>
753
+ <li><strong>Findings</strong> \u2014 detected anomalies, most severe first, each group marked with
754
+ three dots filled by severity (Unusual, Notable, Minor). Severity chips narrow
706
755
  the list; Kind and Region dropdowns appear once there are enough findings to
707
- be worth narrowing.</li>
756
+ be worth narrowing. <strong>Detail</strong> opens the same sentences above a readable list.</li>
708
757
  <li><strong>Bin breakdown</strong> \u2014 bars as a share of dies, pass bins first and then failing
709
758
  bins by descending count, so the dominant failure mode is at the top. It
710
759
  follows the map&#39;s plot mode: a soft-bin map gets a soft-bin breakdown. When a
711
760
  wafer carries both bin types, a <strong>Hard / Soft</strong> selector in the section header
712
761
  overrides that.</li>
713
762
  <li><strong>Region yield</strong> \u2014 ring yield by default, with a <strong>Ring / Quadrant</strong> selector.
763
+ Each row prints its difference from the wafer&#39;s yield in points and is tinted
764
+ when it is 1, 2 or 4 or more points below it (nothing above is tinted).
714
765
  Ring is the default because edge roll-off is the pattern that dominates real
715
766
  wafer maps; a genuinely asymmetric quadrant is reported as a finding above,
716
767
  with a significance test behind it.</li>
717
768
  <li><strong>Wafer yield</strong> (several wafers only) \u2014 one bar per wafer with the median
718
- marked. Wafers are in slot order by default, since that is what makes a
769
+ marked, each row&#39;s difference from the median in points, and a tint when it is
770
+ 1, 2 or 4 or more points below it. Wafers are in slot order by default, since that is what makes a
719
771
  slot-correlated pattern visible; a <strong>Slot / Yield</strong> selector re-sorts. Wafers
720
- far below the rest are labelled &quot;low outlier&quot;.</li>
772
+ well below or above the rest are labelled &quot;low outlier&quot; or &quot;high outlier&quot;
773
+ (3 or more wafers, and at least 3 points from the median \u2014 the same rule as the
774
+ outlier-wafer findings).</li>
721
775
  <li><strong>Test values</strong> \u2014 per test: mean, <strong>Ppk</strong>, and limit yield. Ppk (not Cpk)
722
776
  because it measures against the <em>overall</em> spread, including wafer-to-wafer
723
777
  variation, which is what the dies actually ship against. The full descriptive
724
778
  statistics \u2014 min, quartiles, median, max, \u03C3, both test limits, and all four
725
- capability indices \u2014 are in the summary report and the CSV export.</li>
779
+ capability indices \u2014 are in the CSV export, and the wafer summary report shows
780
+ N, min, quartiles, median, mean, max, \u03C3 and limit yield (a lot&#39;s report has N, min, mean,
781
+ max, \u03C3 and limit yield: quartiles cannot be combined from each wafer&#39;s own). Both reports
782
+ also carry the full Cp, Cpk, Pp and Ppk table.</li>
726
783
  <li><strong>Functional tests</strong> \u2014 pass/fail counts and pass rate per functional test.</li>
727
784
  </ul>
728
785
  <p>Every section header can be collapsed, and stays collapsed as the panel
@@ -730,7 +787,13 @@ re-renders.</p>
730
787
  <p>A <strong>Summary report</strong> button (when present) opens a printable full-detail
731
788
  report \u2014 yield, bin breakdown, ring and quadrant statistics, the full per-test
732
789
  table with Cp/Cpk/Pp/Ppk, and the findings list \u2014 and can be saved as a PDF
733
- from your browser&#39;s print dialog.</p>
790
+ from your browser&#39;s print dialog. It opens with <strong>What stands out</strong>: the yield,
791
+ up to three regions, fail bins or wafers ranked by how many dies each costs, and a
792
+ line saying what was compared. An item is listed only when it costs at least one
793
+ yield point of the dies analysed; a lot with none says so. Each figure links to its
794
+ row in the Findings table below. Severity is drawn as three dots and a word, so it reads in a
795
+ black-and-white print; yields carry a bar, and a wafer or region is tinted only when it is
796
+ at least a point below the rest.</p>
734
797
  <h3 id="why-some-findings-name-two-bins">Why some findings name two bins</h3>
735
798
  <p>A finding may read <strong>&quot;hard bin and soft bin 3 (Fail) (same dies)&quot;</strong>. That is one
736
799
  group of dies counted in two bin spaces, not two separate groups added together.
@@ -1,3 +1,3 @@
1
- export declare const WMAP_VERSION = "0.32.0";
2
- export declare const WMAP_BUILD_TIME = "2026-09-27T14:45:45.383Z";
1
+ export declare const WMAP_VERSION = "0.33.0";
2
+ export declare const WMAP_BUILD_TIME = "2026-10-02T10:03:26.613Z";
3
3
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- export const WMAP_VERSION="0.32.0",WMAP_BUILD_TIME="2026-09-27T14:45:45.383Z";
1
+ export const WMAP_VERSION="0.33.0",WMAP_BUILD_TIME="2026-10-02T10:03:26.613Z";
@@ -1,4 +1,10 @@
1
1
  export type AggregationMethod = 'mean' | 'median' | 'stddev' | 'min' | 'max' | 'count';
2
+ /**
3
+ * Whether a stack by this method holds measurements of the test, in its units.
4
+ * A mean, median, minimum or maximum does; a standard deviation is a spread and a
5
+ * count is a tally, so the test's limits say nothing about them.
6
+ */
7
+ export declare function stackHoldsMeasurements(method: string): boolean;
2
8
  /** Minimal die shape required by aggregation functions — satisfied by both Die and DieResult. */
3
9
  export interface DieLike {
4
10
  x: number;
@@ -1 +1 @@
1
- import{copyDie as p,testValue as m}from"./dieTable.js";import{minOf as M,maxOf as V}from"./utils.js";import{positionKey as h}from"./dies.js";export function aggregateValues(l,n,c=0){if(!l.length)return[];const a=new Map,o=new Map;for(const g of l)for(const t of g){const e=h(t),s=m(t,c);s!==void 0?(a.has(e)||(a.set(e,[]),o.set(e,t)),a.get(e).push(s)):o.has(e)||o.set(e,t)}const u=[];for(const[g,t]of o){const e=a.get(g);if(!e?.length){u.push(p(t,{testValues:void 0,testPass:void 0}));continue}let s;if(n==="mean")s=e.reduce((f,i)=>f+i,0)/e.length;else if(n==="median"){const f=[...e].sort((r,d)=>r-d),i=Math.floor(f.length/2);s=f.length%2?f[i]:(f[i-1]+f[i])/2}else if(n==="stddev"){if(e.length<2){u.push(p(t,{testValues:{0:0},testPass:void 0}));continue}const f=e.reduce((i,r)=>i+r,0)/e.length;s=Math.sqrt(e.reduce((i,r)=>i+(r-f)**2,0)/(e.length-1))}else n==="min"?s=M(e):n==="max"?s=V(e):s=e.length;u.push(p(t,{testValues:{0:s},testPass:void 0}))}return u}export function aggregateBinCounts(l,n,c="hard"){if(!l.length)return[];const a=new Map;for(const o of l)for(const u of o)if((c==="soft"?u.sbin:u.hbin)===n){const t=h(u);a.set(t,(a.get(t)??0)+1)}return(l[0]??[]).map(o=>p(o,{testValues:{0:a.get(h(o))??0},testPass:void 0,...c==="soft"?{sbin:n}:{hbin:n}}))}
1
+ import{copyDie as p,testValue as m}from"./dieTable.js";import{minOf as M,maxOf as V}from"./utils.js";import{positionKey as d}from"./dies.js";export function stackHoldsMeasurements(n){return n!=="stddev"&&n!=="count"}export function aggregateValues(n,o,c=0){if(!n.length)return[];const a=new Map,f=new Map;for(const g of n)for(const t of g){const e=d(t),s=m(t,c);s!==void 0?(a.has(e)||(a.set(e,[]),f.set(e,t)),a.get(e).push(s)):f.has(e)||f.set(e,t)}const r=[];for(const[g,t]of f){const e=a.get(g);if(!e?.length){r.push(p(t,{testValues:void 0,testPass:void 0}));continue}let s;if(o==="mean")s=e.reduce((i,u)=>i+u,0)/e.length;else if(o==="median"){const i=[...e].sort((l,h)=>l-h),u=Math.floor(i.length/2);s=i.length%2?i[u]:(i[u-1]+i[u])/2}else if(o==="stddev"){if(e.length<2){r.push(p(t,{testValues:{0:0},testPass:void 0}));continue}const i=e.reduce((u,l)=>u+l,0)/e.length;s=Math.sqrt(e.reduce((u,l)=>u+(l-i)**2,0)/(e.length-1))}else o==="min"?s=M(e):o==="max"?s=V(e):s=e.length;r.push(p(t,{testValues:{0:s},testPass:void 0}))}return r}export function aggregateBinCounts(n,o,c="hard"){if(!n.length)return[];const a=new Map;for(const f of n)for(const r of f)if((c==="soft"?r.sbin:r.hbin)===o){const t=d(r);a.set(t,(a.get(t)??0)+1)}return(n[0]??[]).map(f=>p(f,{testValues:{0:a.get(d(f))??0},testPass:void 0,...c==="soft"?{sbin:o}:{hbin:o}}))}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Detection of a repeating multi-project-wafer (MPW) layout, the gate for the compact view.
3
+ *
4
+ * On an MPW only a few parts sit in each reticle, so the occupied columns and rows form a
5
+ * pattern that repeats at the reticle pitch. Random missing data does not repeat, which is
6
+ * what separates the two: hiding the empty rows of a wafer that merely has holes could hide
7
+ * a real problem, so compaction is only offered when a clear period is found.
8
+ *
9
+ * "Occupied" means a die EXISTS at that index, whether or not it carries a bin: a die with
10
+ * only metadata still has a position, and the layout must not change with the colour mode.
11
+ */
12
+ export interface MpwDetectOptions {
13
+ /** Fewest whole repeats of the period the axis must span. Default 3. */
14
+ minRepeats?: number;
15
+ /**
16
+ * Lowest lag correlation of the occupancy signal that counts as a period. Default 0.75:
17
+ * random sparse layouts score under 0.5 at every lag, while a clean MPW with a few
18
+ * reticle columns missing outright still scores above it.
19
+ */
20
+ minScore?: number;
21
+ /** Occupied fraction above which an axis is dense: nothing worth compacting. Default 0.8. */
22
+ maxDensity?: number;
23
+ /**
24
+ * A unit the host already knows, such as a reticle width or height in dies. Only that unit and
25
+ * its multiples are tested (devices on every second reticle repeat at twice the reticle, a
26
+ * cluster that repeats within a reticle at a divisor of it, which the multiples reach too), so
27
+ * the check still has to pass (the occupied indices must actually repeat) but nothing else is
28
+ * inferred.
29
+ */
30
+ expectedPeriod?: number;
31
+ }
32
+ /** A period found on one axis. */
33
+ export interface MpwPeriod {
34
+ /** Indices between one repeat and the next. */
35
+ period: number;
36
+ /** Lag correlation of the occupancy signal at `period`, 0 to 1. */
37
+ score: number;
38
+ /** Whole repeats of the period within the occupied span. */
39
+ repeats: number;
40
+ /** Occupied indices as a fraction of the span between the first and last. */
41
+ occupiedFraction: number;
42
+ }
43
+ /**
44
+ * Find the smallest period at which the occupied indices of one axis repeat.
45
+ *
46
+ * `indices` are the distinct integer indices that hold at least one die, in any order.
47
+ * Returns `null` when the axis is nearly full (nothing to compact), spans too few repeats,
48
+ * or does not repeat: random sparse data scores near zero at every period.
49
+ */
50
+ export declare function detectMpwPeriod(indices: readonly number[], options?: MpwDetectOptions): MpwPeriod | null;
51
+ /** What one axis looks like for the purposes of the compact gate. */
52
+ export type AxisPattern = ({
53
+ kind: 'periodic';
54
+ } & MpwPeriod) | {
55
+ kind: 'dense';
56
+ occupiedFraction: number;
57
+ } | {
58
+ kind: 'none';
59
+ };
60
+ export interface MpwLayout {
61
+ x: AxisPattern;
62
+ y: AxisPattern;
63
+ /** True when compaction should be offered. */
64
+ offered: boolean;
65
+ }
66
+ /**
67
+ * Decide whether the compact view applies to a set of dies.
68
+ *
69
+ * Offered when both axes repeat, or one repeats and the other is dense (a layout that is
70
+ * sparse in one direction only still has empty rows or columns worth removing). Pass the
71
+ * dies of every wafer in the selection, so the answer, and the layout built from it, is
72
+ * the same for every card in a gallery. Unpositioned dies are ignored.
73
+ */
74
+ export declare function detectMpwLayout(dies: readonly {
75
+ x?: number | null;
76
+ y?: number | null;
77
+ }[], options?: Omit<MpwDetectOptions, 'expectedPeriod'> & {
78
+ reticle?: {
79
+ width: number;
80
+ height: number;
81
+ };
82
+ }): MpwLayout;
83
+ /**
84
+ * The compact layout of a set of dies: which original columns and rows survive, and the
85
+ * indices they take. Only the layout, never the dies themselves, so one map can be built
86
+ * from every wafer in a selection and applied to each, giving all gallery cards one grid.
87
+ */
88
+ export interface CompactMap {
89
+ /** Original die `x` of each compact column, ascending. All coordinates shown to a user are these. */
90
+ readonly columns: readonly number[];
91
+ /** Original die `y` of each compact row, ascending. */
92
+ readonly rows: readonly number[];
93
+ /** Compact columns that follow a skipped range of original columns (so a group of columns starts there). */
94
+ readonly columnBreaks: readonly number[];
95
+ /** Compact rows that follow a skipped range of original rows. */
96
+ readonly rowBreaks: readonly number[];
97
+ /**
98
+ * The blocks the layout falls into: each is a run of compact columns and a run of compact
99
+ * rows, `[start, end)`, with no skipped original index inside either, and at least one
100
+ * die of the layout in it. On an MPW these are the reticle groups the dies sit in, so
101
+ * drawing each as a box keeps them distinct once the gaps between them are gone.
102
+ */
103
+ readonly groups: readonly CompactGroup[];
104
+ /** Compact column of an original `x`, or `undefined` when no die of the layout sits in it. */
105
+ columnOf(x: number): number | undefined;
106
+ /** Compact row of an original `y`, or `undefined` when no die of the layout sits in it. */
107
+ rowOf(y: number): number | undefined;
108
+ }
109
+ /** A block of compact cells with no skipped original index inside it; see `CompactMap.groups`. */
110
+ export interface CompactGroup {
111
+ readonly columns: readonly [start: number, end: number];
112
+ readonly rows: readonly [start: number, end: number];
113
+ }
114
+ /**
115
+ * Build the compact layout for `dies`: the columns and rows that hold at least one die,
116
+ * renumbered 0..n-1 in order. A die EXISTS there whether or not it carries a bin (see the
117
+ * note at the top of this file), so the layout never depends on colour mode, filters or
118
+ * which wafer has data. Unpositioned dies are ignored.
119
+ */
120
+ export declare function buildCompactMap(dies: readonly {
121
+ x?: number | null;
122
+ y?: number | null;
123
+ }[]): CompactMap;
124
+ /** A die with the compact cell it is drawn in. */
125
+ export interface CompactPlacement<T> {
126
+ die: T;
127
+ column: number;
128
+ row: number;
129
+ }
130
+ /**
131
+ * Place `dies` on a compact grid. With no `basis` the layout is built from `dies`; pass the
132
+ * dies of every wafer in a selection as `basis` so each wafer lands on the same grid. Every
133
+ * positioned die is placed, none dropped: a die whose column or row is missing from the
134
+ * layout throws, since that is a layout built from the wrong dies, not data to hide.
135
+ */
136
+ export declare function compactDies<T extends {
137
+ x?: number | null;
138
+ y?: number | null;
139
+ }>(dies: readonly T[], basis?: readonly {
140
+ x?: number | null;
141
+ y?: number | null;
142
+ }[] | CompactMap): {
143
+ placements: CompactPlacement<T>[];
144
+ map: CompactMap;
145
+ };
146
+ /**
147
+ * Whether the compact layout should be offered for the dies of one map or of every map in a
148
+ * gallery. The single rule both renderers use: `reticles` holds one entry per map, and a
149
+ * reticle size fixes the period only when every map supplies the same one; otherwise the
150
+ * period is inferred from the dies.
151
+ */
152
+ export declare function compactLayoutOffered(dies: readonly {
153
+ x?: number | null;
154
+ y?: number | null;
155
+ }[], reticles: readonly ({
156
+ width: number;
157
+ height: number;
158
+ } | undefined)[], options?: Omit<MpwDetectOptions, 'expectedPeriod'>): boolean;
159
+ /** What the detector saw on one axis. Counts and scores only: no index or coordinate. */
160
+ export interface AxisDiagnostics {
161
+ /** Distinct indices that hold a die. */
162
+ occupied: number;
163
+ /** Indices from the first occupied to the last. */
164
+ span: number;
165
+ /** Maximal runs of consecutive occupied indices. */
166
+ runs: number;
167
+ pattern: AxisPattern;
168
+ /** The best-scoring candidate periods, whether or not any passed. */
169
+ bestPeriods: {
170
+ period: number;
171
+ score: number;
172
+ }[];
173
+ }
174
+ export interface MpwDiagnostics {
175
+ wafers: number;
176
+ positionedDies: number;
177
+ unpositionedDies: number;
178
+ /** Blocks of compact cells that hold dies. */
179
+ groups: number;
180
+ reticle: {
181
+ width: number;
182
+ height: number;
183
+ } | undefined;
184
+ offered: boolean;
185
+ thresholds: Required<Omit<MpwDetectOptions, 'expectedPeriod'>>;
186
+ columns: AxisDiagnostics;
187
+ rows: AxisDiagnostics;
188
+ }
189
+ /**
190
+ * Everything the compact gate saw, as numbers a host can show a user and ask them to send
191
+ * back. It exists because the detector's thresholds cannot be tuned on data nobody can
192
+ * share, so what it needs to report must carry no die position, bin, value or identity:
193
+ * counts, fractions, scores and periods only. `reticles` holds one entry per map, as for
194
+ * `compactLayoutOffered`, which this agrees with by construction.
195
+ */
196
+ export declare function diagnoseMpwLayout(dies: readonly {
197
+ x?: number | null;
198
+ y?: number | null;
199
+ }[], reticles: readonly ({
200
+ width: number;
201
+ height: number;
202
+ } | undefined)[], options?: Omit<MpwDetectOptions, 'expectedPeriod'>): MpwDiagnostics;
203
+ /** The text a user copies and sends back. Plain lines, so it can be read before it is sent. */
204
+ export declare function formatMpwDiagnostics(d: MpwDiagnostics, library: string): string;
205
+ //# sourceMappingURL=compact.d.ts.map
@@ -0,0 +1,3 @@
1
+ import{hasPosition as a}from"./dies.js";const h={minRepeats:3,minScore:.75,maxDensity:.8};function w(t,e){const s=t.length-e;if(s<2)return 0;let r=0,o=0,n=0,i=0,d=0;for(let u=0;u<s;u++){const p=t[u],l=t[u+e];r+=p,o+=l,n+=p*p,i+=l*l,d+=p*l}const f=n-r*r/s,c=i-o*o/s;return f<=0||c<=0?0:(d-r*o/s)/Math.sqrt(f*c)}function x(t,e,s){const r=Math.floor(t/e),o=[];if(s!==void 0&&s>=1){const n=Math.floor(s);for(let i=n;i<=r;i+=n)i>=2&&o.push(i)}else for(let n=2;n<=r;n++)o.push(n);return o}function g(t){if(t.length===0)return null;let e=1/0,s=-1/0;for(const n of t)n<e&&(e=n),n>s&&(s=n);const r=new Uint8Array(s-e+1);let o=0;for(const n of t)r[n-e]||(r[n-e]=1,o++);return{signal:r,count:o}}export function detectMpwPeriod(t,e={}){const{minRepeats:s,minScore:r,maxDensity:o}={...h,...e},{expectedPeriod:n}=e,i=g(t);if(!i)return null;const d=i.signal.length,f=i.count/d;if(f>o)return null;for(const c of x(d,s,n)){const u=w(i.signal,c);if(u>=r)return{period:c,score:u,repeats:Math.floor(d/c),occupiedFraction:f}}return null}function m(t,e){const{minRepeats:s,maxDensity:r}={...h,...e},o=detectMpwPeriod(t,e);if(o)return{kind:"periodic",...o};const n=g(t);return n&&n.count>=s&&n.count/n.signal.length>r?{kind:"dense",occupiedFraction:n.count/n.signal.length}:{kind:"none"}}export function detectMpwLayout(t,e={}){const{reticle:s,...r}=e,o=new Set,n=new Set;for(const c of t)a(c)&&(o.add(c.x),n.add(c.y));const i=m([...o],{...r,expectedPeriod:s?.width}),d=m([...n],{...r,expectedPeriod:s?.height}),f=i.kind==="periodic"&&(d.kind==="periodic"||d.kind==="dense")||d.kind==="periodic"&&i.kind==="dense";return{x:i,y:d,offered:f}}function y(t,e){const s=[0,...e,t],r=new Int32Array(t);for(let o=0;o+1<s.length;o++)r.fill(o,s[o],s[o+1]);return{ids:r,bounds:s}}function $(t){const e=[...t].sort((o,n)=>o-n),s=[],r=new Map;return e.forEach((o,n)=>{r.set(o,n),n>0&&o-e[n-1]>1&&s.push(n)}),{sorted:e,breaks:s,indexOf:r}}export function buildCompactMap(t){const e=new Set,s=new Set;for(const c of t)a(c)&&(e.add(c.x),s.add(c.y));const r=$(e),o=$(s),n=y(r.sorted.length,r.breaks),i=y(o.sorted.length,o.breaks),d=new Set;for(const c of t)a(c)&&d.add(n.ids[r.indexOf.get(c.x)]*i.bounds.length+i.ids[o.indexOf.get(c.y)]);return{groups:[...d].sort((c,u)=>c-u).map(c=>{const u=Math.floor(c/i.bounds.length),p=c%i.bounds.length;return{columns:[n.bounds[u],n.bounds[u+1]],rows:[i.bounds[p],i.bounds[p+1]]}}),columns:r.sorted,rows:o.sorted,columnBreaks:r.breaks,rowBreaks:o.breaks,columnOf:c=>r.indexOf.get(c),rowOf:c=>o.indexOf.get(c)}}export function compactDies(t,e){const s=e&&"columnOf"in e?e:buildCompactMap(e??t),r=[];for(const o of t){if(!a(o))continue;const n=s.columnOf(o.x),i=s.rowOf(o.y);if(n===void 0||i===void 0)throw new RangeError(`compactDies: die (${o.x}, ${o.y}) is outside the compact layout`);r.push({die:o,column:n,row:i})}return{placements:r,map:s}}export function compactLayoutOffered(t,e,s={}){return detectMpwLayout(t,{...s,reticle:b(e)}).offered}function b(t){const e=t[0];return e&&t.every(s=>s&&s.width===e.width&&s.height===e.height)?{width:e.width,height:e.height}:void 0}function k(t,e){const{minRepeats:s}={...h,...e},r=g(t);if(!r)return{occupied:0,span:0,runs:0,pattern:{kind:"none"},bestPeriods:[]};let o=0;r.signal.forEach((i,d)=>{i&&!r.signal[d-1]&&o++});const n=[];for(const i of x(r.signal.length,s,e.expectedPeriod))n.push({period:i,score:w(r.signal,i)});return n.sort((i,d)=>d.score-i.score||i.period-d.period),{occupied:r.count,span:r.signal.length,runs:o,pattern:m(t,e),bestPeriods:n.slice(0,5)}}export function diagnoseMpwLayout(t,e,s={}){const r=b(e),o=new Set,n=new Set;let i=0;for(const d of t)a(d)&&(i++,o.add(d.x),n.add(d.y));return{wafers:e.length,positionedDies:i,unpositionedDies:t.length-i,groups:buildCompactMap(t).groups.length,reticle:r,offered:detectMpwLayout(t,{...s,reticle:r}).offered,thresholds:{...h,...s},columns:k([...o],{...s,expectedPeriod:r?.width}),rows:k([...n],{...s,expectedPeriod:r?.height})}}function D(t,e){const s=e.span>0?` (${(100*e.occupied/e.span).toFixed(1)}%)`:"",r=e.pattern.kind==="periodic"?`periodic, period ${e.pattern.period}, score ${e.pattern.score.toFixed(3)}, ${e.pattern.repeats} repeats`:e.pattern.kind==="dense"?"dense, nothing to compact":"no repeating pattern found",o=e.bestPeriods.length?`
2
+ best periods: ${e.bestPeriods.map(n=>`${n.period} (${n.score.toFixed(3)})`).join(", ")}`:"";return`${t}: ${e.occupied} occupied of ${e.span}${s}, ${e.runs} runs; ${r}${o}`}export function formatMpwDiagnostics(t,e){return["wafermap compact-layout diagnostics (format 1)",`library: ${e}`,"Counts and scores only: no die coordinates, bins, test values or wafer identity.",`wafers: ${t.wafers}`,`dies: ${t.positionedDies} positioned, ${t.unpositionedDies} without a position`,`reticle size: ${t.reticle?`${t.reticle.width} x ${t.reticle.height} (supplied by every wafer)`:"not supplied"}`,`compact layout offered: ${t.offered?"yes":"no"}`,`groups of dies: ${t.groups}`,`thresholds: minRepeats ${t.thresholds.minRepeats}, minScore ${t.thresholds.minScore}, maxDensity ${t.thresholds.maxDensity}`,D("columns",t.columns),D("rows",t.rows)].join(`
3
+ `)}
@@ -23,6 +23,12 @@ export declare function itemPassBins(item: {
23
23
  } | null | undefined, fallback?: readonly number[]): readonly number[];
24
24
  /** Same bins, in any order. */
25
25
  export declare function sameBins(a: readonly number[], b: readonly number[]): boolean;
26
+ /**
27
+ * The pass bins every wafer is judged by, or `undefined` when they differ (or there are none to
28
+ * ask). For a surface that must name one set — a headline, or which bins count as failures — and
29
+ * so says nothing rather than naming one wafer's set for a population judged several ways.
30
+ */
31
+ export declare function commonPassBins(sets: Iterable<readonly number[] | undefined>): readonly number[] | undefined;
26
32
  /**
27
33
  * Names the pass bins in use, for a yield label: "bin 1", "bins 1, 2", or —
28
34
  * when the wafers shown disagree — "per wafer: bin 1 · bins 1, 2". A label that
@@ -1 +1 @@
1
- export const INPUT_DEFAULT_PASS_BINS=[1];export function itemPassBins(s,e=INPUT_DEFAULT_PASS_BINS){return s?.passBins??e}export function sameBins(s,e){const t=new Set(s),n=new Set(e);return t.size===n.size&&[...t].every(i=>n.has(i))}export function passBinsLabel(s){const e=[];for(const n of s)e.some(i=>sameBins(i,n))||e.push(n);const t=n=>n.length===0?"no bins":n.length===1?`bin ${n[0]}`:`bins ${n.join(", ")}`;return e.length<=1?t(e[0]??INPUT_DEFAULT_PASS_BINS):`per wafer: ${e.map(t).join(" \xB7 ")}`}
1
+ export const INPUT_DEFAULT_PASS_BINS=[1];export function itemPassBins(s,n=INPUT_DEFAULT_PASS_BINS){return s?.passBins??n}export function sameBins(s,n){const t=new Set(s),e=new Set(n);return t.size===e.size&&[...t].every(i=>e.has(i))}export function commonPassBins(s){let n;for(const t of s){if(t===void 0)return;if(n===void 0)n=t;else if(!sameBins(n,t))return}return n}export function passBinsLabel(s){const n=[];for(const e of s)n.some(i=>sameBins(i,e))||n.push(e);const t=e=>e.length===0?"no bins":e.length===1?`bin ${e[0]}`:`bins ${e.join(", ")}`;return n.length<=1?t(n[0]??INPUT_DEFAULT_PASS_BINS):`per wafer: ${n.map(t).join(" \xB7 ")}`}
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Which columns (or rows) of a compact layout get an axis label.
3
+ *
4
+ * Group starts when the cells are too narrow to label every one: each is the first column of
5
+ * a reticle group, so the labels name the boundaries, there are few of them, and they are
6
+ * spread out. Every cell once there is room for that, so a zoomed-in map labels each die.
7
+ * A layout with a single group has no boundaries to name, so it is thinned evenly instead.
8
+ *
9
+ * @param count cells on the axis
10
+ * @param breaks compact indices that start a group after the first (see `CompactMap`)
11
+ * @param cellPx on-screen size of one cell along the axis
12
+ * @param minGapPx least distance between two labels
13
+ */
14
+ export declare function compactTickIndices(count: number, breaks: readonly number[], cellPx: number, minGapPx: number): number[];
1
15
  /**
2
16
  * Round a raw spacing to the nearest 1, 2 or 5 × 10ⁿ — THE rounding rule for
3
17
  * every tick grid in the library, the wafer map's own mm axes included.
@@ -1 +1 @@
1
- export function niceStep(i){if(!(i>0)||!Number.isFinite(i))return 0;const n=Math.pow(10,Math.floor(Math.log10(i))),t=i/n;return(t<1.5?1:t<3.5?2:t<7.5?5:10)*n}export function stepDecimals(i){if(!(i>0)||!Number.isFinite(i))return 0;for(let n=0;n<=9;n++){const t=i*10**n;if(Math.abs(t-Math.round(t))<1e-6*Math.max(1,t))return n}return 9}function M(i,n,t){const c=Math.ceil(i/t-1e-9)*t,e=[];for(let r=c;r<=n+t*1e-9;r+=t)e.push(Number((Math.round(r/t)*t).toFixed(stepDecimals(t)+2)));return e}export function fitTicks(i,n,t,c){const e=n-i;if(!(e>0)||!Number.isFinite(e)||!(t>0))return{ticks:Number.isFinite(i)?[i]:[],step:0};const r=[1,2,5];let f=Math.floor(Math.log10(e/Math.max(1,t/4))),u=0;for(let a=0;a<60;a++){const o=Number((r[u]*10**f).toPrecision(12)),s=M(i,n,o);if(s.length<=1||o/e*t>=c(o,s))return{ticks:s,step:o};++u===r.length&&(u=0,f++)}return{ticks:[i,n],step:e}}
1
+ export function compactTickIndices(e,n,t,s){if(e<=0)return[];const r=Math.max(1,Math.ceil(s/Math.max(t,1e-9)));if(r===1)return Array.from({length:e},(i,c)=>c);if(n.length===0){const i=[];for(let c=0;c<e;c+=r)i.push(c);return i}const o=[];let u=-1/0;for(const i of[0,...n])((i-u)*t>=s||o.length===0)&&(o.push(i),u=i);return o}export function niceStep(e){if(!(e>0)||!Number.isFinite(e))return 0;const n=Math.pow(10,Math.floor(Math.log10(e))),t=e/n;return(t<1.5?1:t<3.5?2:t<7.5?5:10)*n}export function stepDecimals(e){if(!(e>0)||!Number.isFinite(e))return 0;for(let n=0;n<=9;n++){const t=e*10**n;if(Math.abs(t-Math.round(t))<1e-6*Math.max(1,t))return n}return 9}function h(e,n,t){const s=Math.ceil(e/t-1e-9)*t,r=[];for(let o=s;o<=n+t*1e-9;o+=t)r.push(Number((Math.round(o/t)*t).toFixed(stepDecimals(t)+2)));return r}export function fitTicks(e,n,t,s){const r=n-e;if(!(r>0)||!Number.isFinite(r)||!(t>0))return{ticks:Number.isFinite(e)?[e]:[],step:0};const o=[1,2,5];let u=Math.floor(Math.log10(r/Math.max(1,t/4))),i=0;for(let c=0;c<60;c++){const f=Number((o[i]*10**u).toPrecision(12)),a=h(e,n,f);if(a.length<=1||f/r*t>=s(f,a))return{ticks:a,step:f};++i===o.length&&(i=0,u++)}return{ticks:[e,n],step:r}}
@@ -4,6 +4,7 @@ import type { Reticle } from '../core/reticle.js';
4
4
  import type { DieMetadata, WaferMetadata } from '../core/metadata.js';
5
5
  import { type Affine } from '../core/transforms.js';
6
6
  import { type BinColors } from './binColors.js';
7
+ import type { CompactMap } from '../core/compact.js';
7
8
  import type { TestDef, BinDef, MetadataFieldDef, ReticleConfig } from './buildWaferMap.js';
8
9
  import { classifySpec, type SpecCategory } from './spec.js';
9
10
  export { classifySpec };
@@ -89,7 +90,7 @@ export interface ViewHoverPoint {
89
90
  y: number;
90
91
  }
91
92
  export interface ViewOverlay {
92
- kind: 'wafer-boundary' | 'reticle' | 'probe-path' | 'ring-boundary' | 'quadrant-boundary' | 'xy-indicator';
93
+ kind: 'wafer-boundary' | 'reticle' | 'probe-path' | 'ring-boundary' | 'quadrant-boundary' | 'xy-indicator' | 'compact-group';
93
94
  points: Point[][];
94
95
  closed: boolean;
95
96
  lineColor: string;
@@ -267,6 +268,21 @@ export interface View {
267
268
  pass: number;
268
269
  fail: number;
269
270
  };
271
+ /**
272
+ * Present only on a compact view (see `ViewOptions.compact`): the original die coordinate of
273
+ * each compact column and row, and the compact index that sits at the display origin.
274
+ * Axis labels read this instead of inverting `gridToScreen`, so they stay original die
275
+ * coordinates, never compact indices.
276
+ */
277
+ compact?: {
278
+ columns: readonly number[];
279
+ rows: readonly number[];
280
+ /** Compact indices that start a group after the first, per axis: where the axis labels go. */
281
+ columnBreaks: readonly number[];
282
+ rowBreaks: readonly number[];
283
+ centreColumn: number;
284
+ centreRow: number;
285
+ };
270
286
  /** Bounding box of all die centres in scene coordinates (mm). */
271
287
  dieBounds: {
272
288
  minX: number;
@@ -316,6 +332,15 @@ export interface ViewOptions {
316
332
  flipX?: boolean;
317
333
  flipY?: boolean;
318
334
  };
335
+ /**
336
+ * Draw the dies on this compact layout (see `buildCompactMap`) instead of at their physical
337
+ * positions: the empty columns and rows are gone, each group of dies is outlined, and the
338
+ * wafer outline, rings, quadrants, reticle grid and XY indicator are not drawn, because the
339
+ * dies no longer sit where they are. Every die is still drawn and still counts: only the
340
+ * layout changes. Rotation, flips and the notch marker follow the same transform as the
341
+ * physical view.
342
+ */
343
+ compact?: CompactMap;
319
344
  /**
320
345
  * Explicit value colour normalization range.
321
346
  *