@wafertools/wafermap 0.30.2 → 0.30.3

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 (82) hide show
  1. package/AGENTS.md +16 -5
  2. package/CHANGELOG.md +384 -0
  3. package/README.md +6 -3
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +48 -0
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +4 -1
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +4 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +81 -7
  12. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  14. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  15. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  18. package/dist/packages/canvas-adapter/charts/sweep.d.ts +31 -0
  19. package/dist/packages/canvas-adapter/charts/sweep.js +1 -0
  20. package/dist/packages/canvas-adapter/charts/trend.d.ts +3 -1
  21. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  22. package/dist/packages/canvas-adapter/dieList.js +2 -2
  23. package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
  24. package/dist/packages/canvas-adapter/drilldown.js +1 -0
  25. package/dist/packages/canvas-adapter/icons.js +1 -1
  26. package/dist/packages/canvas-adapter/insightsTab.d.ts +55 -3
  27. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  28. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  29. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  30. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  31. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  32. package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
  33. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  34. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  35. package/dist/packages/canvas-adapter/userGuideHtml.js +126 -3
  36. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  37. package/dist/packages/canvas-adapter/version.js +1 -1
  38. package/dist/packages/canvas-adapter/warnings.js +1 -1
  39. package/dist/packages/renderer/axisTicks.d.ts +32 -0
  40. package/dist/packages/renderer/axisTicks.js +1 -0
  41. package/dist/packages/renderer/buildView.d.ts +11 -37
  42. package/dist/packages/renderer/buildView.js +1 -1
  43. package/dist/packages/renderer/buildWaferMap.d.ts +58 -1
  44. package/dist/packages/renderer/buildWaferMap.js +1 -1
  45. package/dist/packages/renderer/deprecate.d.ts +13 -5
  46. package/dist/packages/renderer/deprecate.js +1 -1
  47. package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
  48. package/dist/packages/renderer/derivedTests/apply.js +1 -0
  49. package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
  50. package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
  51. package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
  52. package/dist/packages/renderer/derivedTests/parser.js +2 -0
  53. package/dist/packages/renderer/fmt.d.ts +24 -2
  54. package/dist/packages/renderer/fmt.js +1 -1
  55. package/dist/packages/renderer/index.d.ts +1 -0
  56. package/dist/packages/renderer/index.js +1 -1
  57. package/dist/packages/renderer/spec.d.ts +49 -0
  58. package/dist/packages/renderer/spec.js +1 -0
  59. package/dist/packages/renderer/testLabel.d.ts +154 -0
  60. package/dist/packages/renderer/testLabel.js +1 -0
  61. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  62. package/dist/packages/stats/capability.d.ts +4 -0
  63. package/dist/packages/stats/capability.js +1 -1
  64. package/dist/packages/stats/correlation.d.ts +3 -0
  65. package/dist/packages/stats/correlation.js +1 -1
  66. package/dist/packages/stats/deprecated.d.ts +3 -0
  67. package/dist/packages/stats/deprecated.js +1 -1
  68. package/dist/packages/stats/index.d.ts +2 -2
  69. package/dist/packages/stats/index.js +1 -1
  70. package/dist/packages/stats/mergeTestDefs.js +1 -1
  71. package/dist/packages/stats/renderFindingsReport.js +8 -28
  72. package/dist/packages/stats/renderSummaryReport.js +21 -30
  73. package/dist/packages/stats/reportHtml.d.ts +27 -0
  74. package/dist/packages/stats/reportHtml.js +35 -14
  75. package/dist/packages/stats/sweep.d.ts +178 -0
  76. package/dist/packages/stats/sweep.js +1 -0
  77. package/dist/packages/stats/sweepXFromName.d.ts +27 -0
  78. package/dist/packages/stats/sweepXFromName.js +1 -0
  79. package/dist/packages/stats/testPassRate.js +1 -1
  80. package/dist/packages/stats/types.d.ts +28 -1
  81. package/llms.txt +3 -2
  82. package/package.json +1 -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-21T06:17:53.784Z">v0.30.2</span></h1>
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>
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
@@ -248,11 +248,25 @@ no gradient to fall back to). <strong>Test pass/fail</strong> is also available
248
248
  Overlays option on an ordinary parametric test when it carries recorded
249
249
  verdicts, as an alternative to spec-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
+ <h3 id="derived-tests">Derived tests (\u2020)</h3>
252
+ <p>A test name marked <strong>\u2020</strong> in front is a <em>derived</em> test: its value was calculated
253
+ from other tests on the same die \u2014 a difference, a ratio, a margin \u2014 rather
254
+ than measured by the tester. It behaves like any other test: you can map it,
255
+ chart it and find patterns in it.</p>
256
+ <p>The dagger appears wherever the test is named \u2014 the map title, tooltips, the
257
+ plot-mode test list, Insights charts and their test pickers, findings, the
258
+ Summary panel, the die list and reports \u2014 always in front of the name, so in a
259
+ list the derived tests line up in a column down the left edge. The words
260
+ <strong>\u2020 Derived, not measured</strong> always appear nearby. Hovering over a derived
261
+ value, or reading the key line under a table or findings list, shows the
262
+ expression it was computed from, e.g. <code>t[1020] - t[1010]</code>. The numbers in
263
+ square brackets are the test numbers it reads. In a CSV export the expression
264
+ appears in a <strong>Derived from</strong> column instead.</p>
251
265
  <h2 id="3-toolbar-controls">3. Toolbar controls</h2>
252
266
  <p>The toolbar sits at the top-right of the map and is always visible. Controls that
253
267
  are not applicable to the current mode are hidden automatically. (Hovering the map
254
268
  shows die tooltips \u2014 a separate thing from the toolbar, which is always present.)</p>
255
- <p><img src="data:image/png;base64,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" alt="Single-map toolbar"></p>
269
+ <p><img src="data:image/png;base64,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" alt="Single-map toolbar"></p>
256
270
  <h3 id="single-map">Single map</h3>
257
271
  <table>
258
272
  <thead>
@@ -328,6 +342,11 @@ shows die tooltips \u2014 a separate thing from the toolbar, which is always pre
328
342
  <td>Click and drag to select a rectangular group of dies (see <a href="#43-box-select" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'43-box-select\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Section 4.3</a>).</td>
329
343
  </tr>
330
344
  <tr>
345
+ <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMyAzdjE4aDE4Ii8+PHBhdGggZD0iTTcgMTZjMy0xIDQtOSA3LTlzMyA1IDYgNSIvPjwvc3ZnPgo=" width="20" height="20"></td>
346
+ <td>Chart</td>
347
+ <td>Opens a menu of charts drawn from the selected dies, or from the whole wafer when nothing is selected (see <a href="#44-charting-dies-and-wafers" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'44-charting-dies-and-wafers\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Section 4.4</a>). Only shown when there is something to chart.</td>
348
+ </tr>
349
+ <tr>
331
350
  <td><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMyAzdjE4aDE4Ii8+PHBhdGggZD0iTTE4IDE3VjkiLz48cGF0aCBkPSJNMTMgMTdWNSIvPjxwYXRoIGQ9Ik04IDE3di0zIi8+PC9zdmc+Cg==" width="20" height="20"></td>
332
351
  <td>Insights</td>
333
352
  <td>Swaps the map for this wafer&#39;s own chart suite \u2014 yield, bin breakdown, process capability, and more (see <a href="#8-insights-tab" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'8-insights-tab\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Section 8</a>). Only shown when the application has enabled it.</td>
@@ -414,6 +433,10 @@ highlighting one bin at a time.</p>
414
433
  <td>Add a die to the current selection</td>
415
434
  </tr>
416
435
  <tr>
436
+ <td><strong>Right-click</strong>, <strong>Menu key</strong> or <strong>Shift + F10</strong></td>
437
+ <td>Chart the selected dies, or the whole wafer (see <a href="#44-charting-dies-and-wafers" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'44-charting-dies-and-wafers\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Section 4.4</a>)</td>
438
+ </tr>
439
+ <tr>
417
440
  <td><strong>Mouse wheel / scroll</strong></td>
418
441
  <td>Zoom in and out at the cursor</td>
419
442
  </tr>
@@ -515,6 +538,9 @@ highlighting one bin at a time.</p>
515
538
  <td>Opens this guide.</td>
516
539
  </tr>
517
540
  </tbody></table>
541
+ <p>Right-click a card anywhere outside its map to chart that wafer on its own \u2014 a
542
+ histogram, process capability or a sweep (see
543
+ <a href="#44-charting-dies-and-wafers" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'44-charting-dies-and-wafers\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Section 4.4</a>).</p>
518
544
  <p>Click a card&#39;s expand button to detach it into its own separate window with the
519
545
  complete single-map toolbar (falls back to a floating window inside the page if
520
546
  separate windows aren&#39;t available in your environment). The vacated grid card
@@ -546,6 +572,48 @@ Use <strong>Ctrl / Cmd + click</strong> to add individual dies to the current se
546
572
  Press <strong>Esc</strong> to clear.</p>
547
573
  <div data-wmap-demo="box-select" class="wmap-demo"></div><p><em>A block of dies near the centre is shown pre-selected. Choose Box select in the
548
574
  toolbar and drag to make your own selection, or Ctrl/Cmd + click individual dies.</em></p>
575
+ <h3 id="44-charting-dies-and-wafers">4.4 Charting dies and wafers</h3>
576
+ <p><strong>Right-click</strong> a population to open a menu of charts drawn from just those
577
+ dies. Picking one opens it in a window over the map. This is how you look at a
578
+ cluster, a scratch or an edge region on its own, or at one wafer out of a lot.</p>
579
+ <p>What you right-click decides the population:</p>
580
+ <ul>
581
+ <li><strong>Selected dies</strong> \u2014 select some dies, then right-click on the map. Right-
582
+ clicking a die that is <strong>not</strong> selected selects that die alone first;
583
+ right-clicking a selected die, or empty space, keeps the selection.</li>
584
+ <li><strong>A whole wafer</strong> \u2014 right-click empty map space with nothing selected, a
585
+ gallery card anywhere outside its map (its header, the space around the map),
586
+ or one wafer&#39;s bar, box or point in an Insights chart (<em>Yield by wafer</em>,
587
+ <em>Test value distribution</em>, <em>Wafer-to-wafer trend</em> \u2014 their tooltips say
588
+ &quot;right-click to chart this wafer&quot;).</li>
589
+ </ul>
590
+ <p>On the map you can also press the <strong>Menu</strong> key or <strong>Shift + F10</strong>, or use the
591
+ <strong>Chart</strong> toolbar button, which charts the selection when there is one and the
592
+ wafer otherwise.</p>
593
+ <p>The menu offers:</p>
594
+ <ul>
595
+ <li><strong>Value histogram</strong> \u2014 one test&#39;s distribution, opening on the test the map
596
+ is showing.</li>
597
+ <li><strong>Process capability</strong> \u2014 every test normalised to its spec limits, with its
598
+ Ppk. Below 30 dies the chart says each Ppk is a rough estimate: a Ppk from a
599
+ handful of dies can be far from the process&#39;s real capability.</li>
600
+ <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>
601
+ </ul>
602
+ <p>What every chart opened this way does:</p>
603
+ <ul>
604
+ <li>It states its population \u2014 how many dies and which wafer they came from \u2014 in
605
+ its title and above the plot, so it cannot be mistaken for a chart of the
606
+ whole lot. Partial and edge-excluded dies are left out, as they are
607
+ everywhere else, and the chart says how many that was.</li>
608
+ <li>It is a <strong>snapshot</strong>: changing the selection afterwards does not change a
609
+ chart that is already open.</li>
610
+ <li>A chart that cannot be drawn from the population stays in the menu, greyed,
611
+ with the reason \u2014 for instance when none of the dies has values for its tests,
612
+ or when the map is a lot stack, whose dies are averages across wafers rather
613
+ than measured dies.</li>
614
+ </ul>
615
+ <p>A map with only bin data and no sweeps has nothing to chart, and right-click
616
+ there does what it normally does.</p>
549
617
  <hr>
550
618
  <h2 id="5-findings-panel">5. Findings panel</h2>
551
619
  <p>When spatial analysis has been run, the <strong>Findings</strong> panel lists statistically
@@ -756,7 +824,12 @@ toolbar. Click it again to return to the map. Independent of the Findings
756
824
  sidebar (Section 6) \u2014 the two toggle independently, and opening one never
757
825
  hides the other&#39;s toolbar button, since Findings has nothing to highlight
758
826
  against once the map is replaced.</p>
759
- <p>Insights is organized into three sub-tabs:</p>
827
+ <p>Any chart mark that stands for one wafer \u2014 a bar in <em>Yield by wafer</em>, a box in
828
+ <em>Test value distribution</em>, a point in <em>Wafer-to-wafer trend</em> \u2014 can be
829
+ right-clicked to chart that wafer on its own (see
830
+ <a href="#44-charting-dies-and-wafers" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'44-charting-dies-and-wafers\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Section 4.4</a>); its tooltip says so.</p>
831
+ <p>Insights is organised into three sub-tabs, plus a fourth, <strong>Sweeps</strong>, when
832
+ the application has defined any sweeps (see <em>Sweep cards</em> below):</p>
760
833
  <ul>
761
834
  <li><p><strong>Overview</strong> \u2014 a <strong>test pass rate</strong> chart showing which test fails most (and,
762
835
  with &quot;Group by&quot; active, whether it fails more in one split than another). It
@@ -830,6 +903,56 @@ box drives the box plot and histogram onto that test.</em></p>
830
903
  positive, orange = negative; intensity = strength) and a die-level scatter
831
904
  plot coloured by hard bin. Clicking a matrix cell drives the scatter plot
832
905
  onto that pair.</em></p>
906
+ <h3 id="sweep-cards">Sweep cards</h3>
907
+ <p>A <strong>sweep</strong> appears in its own <strong>Sweeps</strong> sub-tab when your application has
908
+ defined one. It exists for a test program that measures the same quantity at a
909
+ series of drive levels and records each level as its own test number \u2014 often one
910
+ block sweeping up and another sweeping down. Read one test at a time that is a
911
+ row of unrelated distributions; read as a sweep it is a pair of response curves,
912
+ and the card measures the pair.</p>
913
+ <p><strong>Each line is the population median, with a shaded p10\u2013p90 band</strong> \u2014 not one
914
+ trace per die. A lot is thousands of dies, so per-die traces would be a solid
915
+ block of ink. The band is the spread across the dies currently in scope, so a
916
+ band that widens at one end of the sweep is telling you the population disagrees
917
+ most at that level.</p>
918
+ <p><strong>The dies behind it are whatever Insights is scoped to.</strong> A sweep names tests,
919
+ not dies \u2014 so the group or wafer selected in the panels above it decides the
920
+ population, and the card&#39;s die count states what that came to. To sweep a
921
+ hand-picked set of dies instead, select them on a map and right-click (see
922
+ <a href="#44-charting-dies-and-wafers" onclick="(function(e){e.preventDefault();var g=e.target.closest('.wmap-guide');var el=g&&g.querySelector('[id=\\'44-charting-dies-and-wafers\\']');if(el)el.scrollIntoView({behavior:'smooth'});})(event)">Section 4.4</a>).</p>
923
+ <p>Two measurements are drawn on the plot and restated underneath:</p>
924
+ <ul>
925
+ <li><strong>Crossing</strong> \u2014 where the first two curves meet. If they cross more than once
926
+ the card says so rather than reporting the first as though it were the only
927
+ one.</li>
928
+ <li><strong>Separation</strong> \u2014 the horizontal distance between the two curves at the levels
929
+ the application asked about. With one curve rising and one falling the pair
930
+ traces a V, and this is the width of that V. A level that one of the curves
931
+ never reaches reads <strong>not measurable</strong>, naming the curve \u2014 never <code>0</code>, which
932
+ would read as &quot;they meet here&quot;.</li>
933
+ </ul>
934
+ <p>Only the <strong>first two</strong> curves are measured. Any further ones are drawn for
935
+ context.</p>
936
+ <p><strong>The x axis is the sweep, not the lot</strong> \u2014 that is what distinguishes this card
937
+ from the wafer-to-wafer trend, which walks one test across wafers. If the
938
+ application supplied the real swept quantity, the axis is in those units (dBm,
939
+ volts, \xB0C) and the crossing is reported in them. If it did not, the axis is the
940
+ position in the sequence, labelled by test, because test numbers are identifiers
941
+ \u2014 treating them as a scale would invent spacing the data never claimed.</p>
942
+ <p>Some sweeps step by multiples \u2014 thresholds of 1k, 2k, 5k \u2026 1M \u2014 and are drawn
943
+ on a <strong>logarithmic</strong> x axis, so each step gets the same room. There the crossing
944
+ is found along the log axis, and a width is given as a <strong>ratio</strong> between the two
945
+ curves, with the level each reaches: <em>\xD72.49 (15.9 k\u03A9 \u2192 39.7 k\u03A9)</em>. On a log axis
946
+ the same shift is the same multiple anywhere along it, which a difference in \u03A9
947
+ would hide.</p>
948
+ <p>The card footer reports anything that limits what you are seeing: tests in the
949
+ sweep that have no definition, functional (pass/fail) tests inside a sweep,
950
+ which have no value to plot, and series whose tests carry different units.</p>
951
+ <p>If the swept values no longer line up with the tests \u2014 usually because a test in
952
+ the sweep is missing from this data \u2014 the footer lists the tests it did find, and
953
+ the card is drawn in test order with <strong>the crossing and widths not measured</strong>.
954
+ Pairing the remaining values with the remaining tests would put every later point
955
+ at the wrong level, which gives a plausible curve and a wrong crossing.</p>
833
956
  <h3 id="exporting-a-chart">Exporting a chart</h3>
834
957
  <p>Every chart panel has a <strong>camera</strong> button that saves the current view as a PNG at the displayed
835
958
  resolution. To get a clean full-resolution render, use the panel&#39;s expand (corner-arrows)
@@ -1,3 +1,3 @@
1
- export declare const WMAP_VERSION = "0.30.2";
2
- export declare const WMAP_BUILD_TIME = "2026-09-21T06:19:56.056Z";
1
+ export declare const WMAP_VERSION = "0.30.3";
2
+ export declare const WMAP_BUILD_TIME = "2026-09-23T17:24:44.719Z";
3
3
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- export const WMAP_VERSION="0.30.2",WMAP_BUILD_TIME="2026-09-21T06:19:56.056Z";
1
+ export const WMAP_VERSION="0.30.3",WMAP_BUILD_TIME="2026-09-23T17:24:44.719Z";
@@ -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"};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 g=o.createElement("span");Object.assign(g.style,{flex:"1",minWidth:"0"}),g.textContent=`${w(e)} ${C(n)}`;const l=o.createElement("span");Object.assign(l.style,{flexShrink:"0"}),a.append(g,l);const p=o.createElement("div");Object.assign(p.style,{padding:`0 ${s.lg} ${s.sm}`}),p.textContent=n.message;let c=!1;const f=()=>{l.textContent=c?"\u25B4":"\u25BE",p.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,p),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 g=n.createElement("div");Object.assign(g.style,{padding:`${s.sm} ${s.xl} ${s.xs}`,fontSize:y.body,fontWeight:"700",letterSpacing:O,textTransform:"uppercase",color:d.label}),g.textContent=o.length===1?"1 warning":`${o.length} warnings`,e.appendChild(g);for(const l of o){const p=severityOf(l),c=$(p),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(p),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 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}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Round a raw spacing to the nearest 1, 2 or 5 × 10ⁿ — THE rounding rule for
3
+ * every tick grid in the library, the wafer map's own mm axes included.
4
+ */
5
+ export declare function niceStep(raw: number): number;
6
+ /**
7
+ * The decimal places that show every multiple of `step` exactly — 2 → 0,
8
+ * 0.5 → 1, 0.25 → 2, 0.002 → 3. Capped at 9, past which the step is noise in
9
+ * a double and more digits would print it.
10
+ */
11
+ export declare function stepDecimals(step: number): number;
12
+ /**
13
+ * The densest round tick grid that fits: walks the 1-2-5 ladder from fine to
14
+ * coarse and returns the first step whose ticks sit at least
15
+ * `minSpacingPx(step, ticks)` apart on an axis `lengthPx` long.
16
+ *
17
+ * Density comes from the space, never from a fixed tick count. A fixed count
18
+ * was the first version of this, and on a 200 px box plot it rounded the step
19
+ * up to leave three ticks — 1.0 / 1.2 / 1.4 on data spanning 0.863–1.56, fewer
20
+ * and coarser than the unrounded five it replaced. A horizontal axis passes the
21
+ * widest label its step would draw plus a gap, so the step is as fine as the
22
+ * labels allow; a vertical axis passes a line spacing.
23
+ *
24
+ * Ticks are only returned inside the range: an axis maps `[lo, hi]` to its
25
+ * pixels, and extending it to reach a round number would change what the plot
26
+ * shows, not just how it is labelled.
27
+ */
28
+ export declare function fitTicks(lo: number, hi: number, lengthPx: number, minSpacingPx: (step: number, ticks: number[]) => number): {
29
+ ticks: number[];
30
+ step: number;
31
+ };
32
+ //# sourceMappingURL=axisTicks.d.ts.map
@@ -0,0 +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}}
@@ -5,6 +5,9 @@ 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
7
  import type { TestDef, BinDef, MetadataFieldDef, ReticleConfig } from './buildWaferMap.js';
8
+ import { classifySpec, type SpecCategory } from './spec.js';
9
+ export { classifySpec };
10
+ export type { SpecCategory };
8
11
  /** Resolve a toolbar activeTest cursor to the canonical test number for getDieTestValue. */
9
12
  export declare function resolveTestNumber(activeTest: number, testDefs?: TestDef[]): {
10
13
  testNumber: number;
@@ -447,26 +450,6 @@ interface ColorFns {
447
450
  forValue: (t: number) => string;
448
451
  forBin: (bin: number) => string;
449
452
  }
450
- /** A die's spec classification for `value` mode against the active test's limits. */
451
- export type SpecCategory = 'pass' | 'failHigh' | 'failLow';
452
- /**
453
- * Classify a value against the active test's spec limits, using the SAME rules as the value-mode
454
- * die colouring in `pushDieRectangles`. Returns null when there is no value. Shared by the colour
455
- * branch and the spec-count tally so the two never diverge.
456
- *
457
- * Out-of-spec *classification* depends ONLY on whether limits are defined — never on
458
- * `colorbarRangeMode`. An out-of-spec die is always flagged when limits exist, regardless of
459
- * how the colorbar is scaled. The *form* of the indication, decided in `pushDieRectangles`,
460
- * depends only on the effective `passFailDisplay`: under `'spec'` (pass/fail mode) the die gets a solid
461
- * green/blue/red categorical fill; in normal value/gradient mode it keeps the value gradient
462
- * fill (like every other die, so the distribution stays readable and out-of-spec colours don't
463
- * collide with the scheme) and is flagged with a ▽/△ marker (`ViewRect.specMark`). Either way
464
- * the die is never drawn as plain in-spec — the silent correctness bug this guards against.
465
- */
466
- export declare function classifySpec(value: number | undefined, activeTestDef: {
467
- limitLow?: number;
468
- limitHigh?: number;
469
- } | undefined): SpecCategory | null;
470
453
  /** Options for {@link buildHoverText}. All fields are optional. */
471
454
  export interface HoverTextOptions {
472
455
  testDefs?: TestDef[];
@@ -499,24 +482,15 @@ export declare function buildHoverText(die: Die, plotMode: PlotMode, opts?: Hove
499
482
  export interface MapTitleParts {
500
483
  primary: string;
501
484
  secondary: string;
485
+ /**
486
+ * The derived-test key, `"† Derived, not measured"`, when the test the
487
+ * map shows is derived — drawn on its own line below `secondary`, because
488
+ * `primary` then carries the `†` and a glyph must never appear without its
489
+ * key. Absent for a measured test. Deliberately NOT part of the export file
490
+ * name, which reads `primary` and `secondary` only.
491
+ */
492
+ note?: string;
502
493
  }
503
- /**
504
- * Build the on-canvas map title for any plot mode, derived from the View (+ optional bin defs so a
505
- * single-bin stacked card can name its bin). Returns a primary/secondary split so the renderer can
506
- * place the key identifier above the scale and the supporting context below it — keeping each line
507
- * short and never obscured.
508
- *
509
- * Primary / secondary per mode:
510
- * - value → "{name} ({unit})" / —
511
- * - stackedValues → "{name} ({unit}) · {method}" / "stacked ({n} wafers)"
512
- * - hardBin/softBin → "Hard Bin" / "Soft Bin" / —
513
- * - stackedBins → "Hard Bin {bin}" (or "Hard Bin") / "stacked ({n} wafers)"
514
- * - stackedSoftBins → "Soft Bin {bin}" (or "Soft Bin") / "stacked ({n} wafers)"
515
- *
516
- * @param fallbackFormat unitless value formatting — matches the colorbar tick formatting.
517
- * @param binDefs the active bin defs (hbinDefs for hard modes, sbinDefs for soft) — used to name
518
- * the bin on single-bin stacked cards.
519
- */
520
494
  export declare function buildMapTitle(view: View, fallbackFormat?: 'si' | 'engineering', binDefs?: BinDef[]): MapTitleParts;
521
495
  export declare function generateTextOverlay(dies: PositionedDie[], txCoords: Float64Array | null, options: {
522
496
  plotMode: PlotMode;
@@ -1 +1 @@
1
- import{hasPosition as oe}from"../core/dies.js";import{getReticleCell as re}from"../core/reticle.js";import{metadataCategoricalValue as le,metadataDisplayValue as ce}from"../core/metadata.js";import{affineRotation as pt,affineMirror as Rt,affineCompose as st,affinePoint as G,affineVector as yt,affineSwapsAxes as ue}from"../core/transforms.js";import{contrastTextColor as Q,SPEC_PASS_FILL as gt,SPEC_FAIL_LOW as fe,SPEC_FAIL_HIGH as bt}from"./colorMap.js";import{resolveValueColorFn as de}from"./colorSchemes.js";import{resolveBinColors as he,binColorsCover as me}from"./binColors.js";import{NO_DATA_FILL as z}from"./colorMap.js";import{diePassStatus as pe}from"../core/dies.js";import{getDieTestValue as P,getTestPassStatus as U,isParametricTest as xt}from"./buildWaferMap.js";import{fmt as Mt,fmtColorbarAxis as St,fmtAggregationMethod as Nt}from"./fmt.js";import{metadataValueColor as ye}from"./colorMap.js";import{clamp01 as Ot,compareNatural as ge,escHtml as H,minOf as be}from"../core/utils.js";import{prettyKey as Ht}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function collectMetadataValues(t,e){const a=new Set;for(const n of t){if(n.partial||n.edgeExcluded)continue;const s=ot(n,e);s!==void 0&&a.add(s)}return[...a].sort(ge)}export function findTestDef(t,e){return t?.find(a=>a.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,a)=>e-a)}const xe="#d3d6db",At="#e8e9ea",Me="#aab0ba";function ot(t,e){return e?le(t.metadata?.[e]):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function Se(t,e,a){const{x:n,y:s}=t.center,i=pt(t.orientation,n,s),o=st(Rt(e?.x??!1,e?.y??!1,n,s),pt(t.orientation,n,s)),l=st(Rt(a?.flipX??!1,a?.flipY??!1,n,s),pt(a?.rotation??0,n,s));return{bakedToScreen:l,physicalToScreen:st(l,i),gridToScreen:st(l,o)}}function K(t,e=!1){return{points:[t],closed:e}}function ke(t,e){const{center:a,radius:n,notch:s}=t;let i=a.x+n*Math.cos(e),o=a.y+n*Math.sin(e);if(!s)return{x:i,y:o};const l=Math.sqrt(n**2-(s.length/2)**2),d=s.length/2,g=i-a.x,p=o-a.y;return s.type==="bottom"&&p<-l?(o=a.y-l,i=a.x+Math.max(-d,Math.min(d,g))):s.type==="top"&&p>l?(o=a.y+l,i=a.x+Math.max(-d,Math.min(d,g))):s.type==="left"&&g<-l?(i=a.x-l,o=a.y+Math.max(-d,Math.min(d,p))):s.type==="right"&&g>l&&(i=a.x+l,o=a.y+Math.max(-d,Math.min(d,p))),{x:i,y:o}}function Et(t,e){return t.map(e).join(" / ")}function ve(t,e=256){const a=Array.from({length:e},(n,s)=>t(s/(e-1)));return n=>a[Math.min(e-1,Math.max(0,Math.round(n*(e-1))))]}function $e(t,e){const a=Math.max(1,Math.min(t.width,t.height)),n=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(a*.55,n*1.8))))}function Be(t,e,a){if(e?.length){const n=e.flatMap(s=>{const i=s.testNumber;if(!xt(s)){const l=U(t,i,s);return l===void 0?[]:[{key:i,label:s.name,value:l?"Pass":"Fail"}]}const o=P(t,i);return o===void 0?[]:[{key:i,label:s.name,value:Mt(o,s.unit,a),recordedFail:t.testPass?.[i]===!1}]});if(n.length)return n}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:Mt(s,void 0,a)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,a={}){const{testDefs:n,hbinDefs:s,sbinDefs:i,fallbackFormat:o,aggrMethod:l,lotSize:d,activeTest:g,reticleConfig:p,metadataFields:B}=a,D=s?new Map(s.map(u=>[u.bin,u])):null,T=i?new Map(i.map(u=>[u.bin,u])):null,b=[H(`Die (${t.x}, ${t.y})`)];if(p&&oe(t)){const u=re(t,p);b.push(H(`Reticle (${u.column}, ${u.row})`))}if(e==="stackedValues"){const u=P(t,0);if(u!==void 0){const f=n?.[0],h=f?.testNumber,k=f?.name??(h!=null?`Test ${h}`:"Value"),m=l?` (${l})`:"";b.push(H(`${k}${m}: ${Mt(u,f?.unit,o)}`))}}else if(e==="stackedBins"||e==="stackedSoftBins"){const u=P(t,0),f=e==="stackedSoftBins"?t.sbin:t.hbin;if(u!==void 0){const h=e==="stackedSoftBins"?T:D,k=f!==void 0?h?.get(f)?.name?`${f} \xB7 ${h.get(f).name}`:`Bin ${f}`:"Bin";let m;if(l==="percent")m=`${u.toFixed(0)}%`;else{const C=d?` (${(u/d*100).toFixed(0)}% of stacked wafers)`:"";m=`${u}${C}`}const x=l?` [${Nt(l)}]`:"";b.push(H(`${k}: ${m}${x}`))}}else{const u=Be(t,n,o);if(e==="value"&&u.length){const{testNumber:f}=resolveTestNumber(g??0,n),h=findTestDef(n,f),k=P(t,f);let m=u.findIndex(y=>y.key===f);m<0&&(m=0);const x=u[m];let C=`<b>${H(`${x.label}: ${x.value}`)}${x.recordedFail?" <i>(recorded fail)</i>":""}</b>`;if(x.key===f&&h){const y=classifySpec(k,h);(y==="failLow"||y==="failHigh")&&(C+=" <i>(out of spec)</i>")}b.push(C);const R=u.length-1;R>0&&b.push(`<i>+${R} more test${R===1?"":"s"}</i>`)}else if(u.length){const f=u.length;b.push(`<i>${f} test result${f===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const f=[];if(t.hbin!==void 0){const h=D?.get(t.hbin)?.name,k=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);f.push(`HBin: ${k}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,k=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);f.push(`SBin: ${k}`)}b.push(f.map(H).join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&b.push(H(`Retests: ${t.retestCount}`)),t.siteNum!==void 0&&b.push(H(`Site: ${t.siteNum}`)),t.partId!==void 0&&b.push(H(`Part ID: ${t.partId}`)),t.partial&&b.push("<i>partial die</i>"),t.probeIndex!==void 0&&b.push(H(`Probe: #${t.probeIndex}`));const $={...t.metadata??{}};for(const[u,f]of Object.entries($)){const h=ce(f);if(h===void 0)continue;const k=B?.find(m=>m.key===u)?.label??Ht(u);b.push(H(`${k}: ${h}`))}return b.join("<br>")}export function buildMapTitle(t,e="engineering",a){const n=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=i=>{const o=a?.length===1?a[0]:void 0;return o===void 0?`${i} Bin`:o.name?`${i} Bin ${o.bin} \xB7 ${o.name}`:`${i} 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:n};case"stackedSoftBins":return{primary:s("Soft"),secondary:n};case"metadata":{const i=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?Ht(t.activeMetadataKey):"Metadata";return{primary:i?.label??o,secondary:""}}case"stackedValues":{const i=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=St(o,i?.name,i?.unit,e),d=l||"Value",g=t.aggrMethod?` \xB7 ${Nt(t.aggrMethod)}`:"";return{primary:`${d}${g}`,secondary:n}}default:{const i=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=St(o,i?.name,i?.unit,e),d=l||i?.name;if(t.passFailDisplay!=="off"){const g=i?.testNumber??t.activeTest,p=d?`${d} \xB7 #${g}`:`Test ${g}`,B=t.passFailDisplay==="spec"?"Spec pass/fail":i!==void 0&&!xt(i)?"Functional pass/fail":"Tester pass/fail";return{primary:p,secondary:B}}return{primary:d??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,a){const{plotMode:n,colorFns:s,normalize:i,activeTest:o,valueRange:l,testDefs:d,fallbackFormat:g,passFailDisplay:p="off",activeMetadataKey:B,metadataColorMap:D,allIntegerValues:T}=a,b=findTestDef(d,o),{tickFmt:$}=St(l[1],b?.name,b?.unit,g),u=T?h=>String(Math.round(h)):$,{testNumber:f}=resolveTestNumber(o,d);return t.flatMap((h,k)=>{let m="",x="#111111";if(n==="value"&&p==="test"){const y=U(h,f,b);if(y===void 0)return[];m=y?"P":"F",x=Q(y?gt:bt)}else if(n==="value"){const y=P(h,f);if(y===void 0)return[];m=Et([y],u),x=Q(s.forValue(i(y)))}else if(n==="hardBin"||n==="softBin"){const y=n==="softBin"?h.sbin:h.hbin;if(y===void 0)return[];m=String(y),x=Q(s.forBin(y))}else if(n==="metadata"){const y=ot(h,B);if(y===void 0)return[];m=y,x=Q(D?.get(m)??z)}else{const y=P(h,0);if(y===void 0)return[];m=Et([y],u),x=Q(s.forValue(i(y)))}const C=e?e[k*2]:h.physX,R=e?e[k*2+1]:h.physY;return[{x:C,y:R,text:m,fontSize:$e(h,m),color:x,align:"center"}]})}function _t(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function Te(t,e,a=720){const{center:n,radius:s,notch:i}=t,o=$=>G(e,$.x,$.y);if(!(i!==void 0&&t.diameter>150)){const $=[];for(let u=0;u<=a;u++){const f=2*Math.PI*u/a;$.push(o(ke(t,f)))}return[{kind:"wafer-boundary",...K($,!0),lineColor:"#888888",lineWidth:1}]}const d=1.25,g=i.length,p=_t(i.type),B=Math.atan2(g,s),D=s-d,T=[];let b=!1;for(let $=0;$<=a;$++){const u=2*Math.PI*$/a;let f=u-p;for(;f>Math.PI;)f-=2*Math.PI;for(;f<-Math.PI;)f+=2*Math.PI;if(Math.abs(f)<=B){if(!b){b=!0;const h={x:n.x+s*Math.cos(p-B),y:n.y+s*Math.sin(p-B)},k={x:n.x+D*Math.cos(p),y:n.y+D*Math.sin(p)},m={x:n.x+s*Math.cos(p+B),y:n.y+s*Math.sin(p+B)};T.push(o(h),o(k),o(m))}continue}T.push(o({x:n.x+s*Math.cos(u),y:n.y+s*Math.sin(u)}))}return[{kind:"wafer-boundary",...K(T,!0),lineColor:"#888888",lineWidth:1}]}function Ie(t,e,a,n=360){const s=[],i=Math.max(1,Math.floor(a));for(let o=1;o<i;o++){const l=t.radius*o/i,d=[];for(let g=0;g<=n;g++){const p=2*Math.PI*g/n,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",...K(d,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function Ve(t,e,a,n){const{x:s,y:i}=t.center,o=t.radius,l=(u,f)=>G(e,u,f),d=a-s,g=Math.sqrt(Math.max(0,o*o-d*d)),p=l(a,i-g),B=l(a,i+g),D=n-i,T=Math.sqrt(Math.max(0,o*o-D*D)),b=l(s-T,n),$=l(s+T,n);return[{kind:"quadrant-boundary",...K([p,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...K([b,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function Ce(t,e){return t.map(a=>{const n=a.width/2,s=a.height/2;return{kind:"reticle",points:[[[a.x-n,a.y-s],[a.x+n,a.y-s],[a.x+n,a.y+s],[a.x-n,a.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 Fe(t){const e=t.filter(a=>a.probeIndex!==void 0).sort((a,n)=>(a.probeIndex??0)-(n.probeIndex??0));return e.length?[{kind:"probe-path",...K(e.map(a=>({x:a.physX,y:a.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function De(t,e,a,n,s,i,o,l,d,g,p,B,D,T,b="off",$,u,f){const h=e.width-o,k=e.height-o,m=i?k:h,x=i?h:k,C=S=>s==="softBin"?S.sbin:S.hbin,R=S=>ot(S,$);if(e.partial){t.push({x:a,y:n,width:m,height:x,fill:xe,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:a,y:n,width:m,height:x,fill:Me,type:"stacked",metadata:e.metadata});return}if(d!==void 0&&(s==="hardBin"||s==="softBin")&&C(e)!==d){t.push({x:a,y:n,width:m,height:x,fill:At,type:"hardBin",metadata:e.metadata});return}if(f!==void 0&&s==="metadata"&&R(e)!==f){t.push({x:a,y:n,width:m,height:x,fill:At,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(b==="test"){const W=U(e,p,T),rt=W===void 0?z:W?gt:bt;t.push({x:a,y:n,width:m,height:x,fill:rt,type:"value",metadata:e.metadata});return}const S=P(e,p),N=classifySpec(S,T);let w,J;S===void 0?w=z:b==="spec"?w=N==="failLow"?fe:N==="failHigh"?bt:gt:(w=l.forValue(g(S)),(N==="failLow"||N==="failHigh")&&(J=N)),t.push({x:a,y:n,width:m,height:x,fill:w,type:"value",specMark:J,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const S=C(e),N=S!=null?l.forBin(S):z,w=S!=null&&pe(e,D)===!1?!0:void 0;t.push({x:a,y:n,width:m,height:x,fill:N,type:s,binFail:w,metadata:e.metadata});return}if(s==="metadata"){const S=R(e),N=S!==void 0?u?.get(S)??z:z;t.push({x:a,y:n,width:m,height:x,fill:N,type:"metadata",metadata:e.metadata});return}const y=P(e,0),Y=y!==void 0?l.forValue(g(y)):z;t.push({x:a,y:n,width:m,height:x,fill:Y,type:"value",metadata:e.metadata})}function Pe(t,e,a){const n=t.radius*.15,s=yt(e,n,0),i=yt(e,0,n),o=s.x+i.x>=0?-1:1,l=s.y+i.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+i.x,y:d.y+i.y};return a.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+i.x*.35,y:p.y+i.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...K([d,g]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...K([d,p]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,a={},n){const s=a.reticles??[],{plotMode:i="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:d=!1,ringCount:g=4,showRingBoundaries:p=!1,showQuadrantBoundaries:B=!1,showXYIndicator:D=!1,binColorScheme:T="default",valueColorScheme:b="default",reverseValueScheme:$=!1,useDefinedBinColors:u=!0,binColors:f,passBins:h=[1],highlightBin:k,highlightMetadataValue:m,interactiveTransform:x,valueRange:C,metadataValueOrder:R,testDefs:y,activeTest:Y=0,activeMetadataKey:S,fallbackFormat:N="engineering",aggregationMethod:w,lotSize:J,dataAxisFlip:W,isLotStack:rt=!1,logScale:wt,colorbarRangeMode:Wt="spec",passFailDisplay:jt,showPartialDies:zt=!0}=a,kt=jt??"off",lt=n?.hbinDefs,ct=n?.sbinDefs,vt=n?.metadataFields,Kt=vt?.find(c=>c.key===S),qt={x:(W?.x??!1)!==(x?.flipX??!1),y:(W?.y??!1)!==(x?.flipY??!1)},Xt=lt?new Map(lt.map(c=>[c.bin,c])):null,Yt=ct?new Map(ct.map(c=>[c.bin,c])):null,Gt=i==="softBin"?Yt:Xt,Qt=new Set(h),ut=f&&me(f,e)?f:he(e,{passBins:h,binColorScheme:T,hbinDefs:lt,sbinDefs:ct,useDefinedBinColors:u}),Ut=i==="softBin"?ut.soft:ut.hard,$t={forValue:ve(de(b,$)),forBin:c=>Ut.get(c)??z};let{testNumber:A}=resolveTestNumber(Y,y);if(!y?.length&&i==="value"&&!e.some(r=>r.testValues&&A in r.testValues)){const r=e.reduce((M,V)=>{if(!V.testValues)return M;const v=Object.keys(V.testValues).map(Number),L=v.length?be(v):void 0;return L!==void 0&&(M===void 0||L<M)?L:M},void 0);r!==void 0&&(A=r)}let Z;if(Array.isArray(C))Z=C;else if(C){const{testNumber:c}=resolveTestNumber(C.test,y);Z=c===A?C.range:void 0}const I=findTestDef(y,A),Bt=i==="value"&&I!==void 0&&!xt(I),Jt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let _="off";i==="value"&&(Bt?_="test":kt==="spec"&&Jt?_="spec":kt==="test"&&e.some(c=>U(c,A,I)!==void 0)&&(_="test"));const Tt=_==="spec"?"spec":Wt,ft=i==="stackedValues"||i==="stackedBins"||i==="stackedSoftBins";let O,E,tt=!1;if(Z){[O,E]=Z;let c=!0,r=!1;for(const M of e){const V=ft?P(M,0):P(M,A);if(V!==void 0&&(r=!0,!Number.isInteger(V))){c=!1;break}}tt=r&&c}else{const c=!ft&&i==="value"&&Tt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let r=1/0,M=-1/0,V=!0;for(const v of e){const L=ft?P(v,0):P(v,A);L!==void 0&&(L<r&&(r=L),L>M&&(M=L),Number.isInteger(L)||(V=!1))}isFinite(r)||(V=!1),tt=V,c?(O=I.limitLow!==void 0?I.limitLow:isFinite(r)?r:0,E=I.limitHigh!==void 0?I.limitHigh:isFinite(M)?M:1):(O=isFinite(r)?r:0,E=isFinite(M)?M:1)}O===E&&(E=O+1);const It=Bt?!1:wt??I?.logScale??!1,Vt=It&&O>0&&E>0,Zt=Vt;let et;if(Vt){const c=Math.log10(O),r=Math.log10(E)-c;et=M=>M<=0?0:Ot((Math.log10(M)-c)/r)}else et=c=>Ot((c-O)/(E-O));const F=Se(t,W,x);let Ct=null;if(t.notch){const c=_t(t.notch.type),r=yt(F.physicalToScreen,Math.cos(c)*t.radius,Math.sin(c)*t.radius),M=Math.hypot(r.x,r.y);M>0&&(Ct={x:r.x/M,y:r.y/M})}let q=1/0;for(let c=0;c<e.length;c++){const r=e[c];r.width<q&&(q=r.width),r.height<q&&(q=r.height)}const Ft=1,te=q!==1/0?Math.min(Ft,q*.12):Ft,Dt=[],dt=[],ee=F.bakedToScreen.a===1&&F.bakedToScreen.b===0&&F.bakedToScreen.c===0&&F.bakedToScreen.d===1&&F.bakedToScreen.e===0&&F.bakedToScreen.f===0;let j=null;if(!ee){j=new Float64Array(e.length*2);for(let c=0;c<e.length;c++){const r=G(F.bakedToScreen,e[c].physX,e[c].physY);j[c*2]=r.x,j[c*2+1]=r.y}}const ne=ue(F.gridToScreen),ae=i==="hardBin"||i==="softBin"||i==="stackedBins"||i==="stackedSoftBins",ie=i==="softBin"||i==="stackedSoftBins";let nt=ae?new Map:void 0;const ht=i==="value"&&_==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,mt=i==="value"&&_==="test"?{pass:0,fail:0}:void 0,Pt=i==="metadata";let at=Pt?new Map:void 0,it=null;if(Pt&&S){const c=collectMetadataValues(e,S),r=R?.key===S?R.values:void 0,M=r?[...r,...c.filter(V=>!r.includes(V))]:c;it=new Map(M.map((V,v)=>[V,Kt?.values?.find(L=>L.value===V)?.color??ye(v)]))}for(let c=0;c<e.length;c++){const r=e[c],M=j?j[c*2]:r.physX,V=j?j[c*2+1]:r.physY;if(dt.push({x:M,y:V}),nt&&!r.partial&&!r.edgeExcluded){const v=ie?r.sbin:r.hbin;v!=null&&nt.set(v,(nt.get(v)??0)+1)}if(ht&&!r.partial&&!r.edgeExcluded){const v=classifySpec(P(r,A),I);v&&ht[v]++}if(mt&&!r.partial&&!r.edgeExcluded){const v=U(r,A,I);v!==void 0&&mt[v?"pass":"fail"]++}if(at&&!r.partial&&!r.edgeExcluded){const v=ot(r,S);v!==void 0&&at.set(v,(at.get(v)??0)+1)}r.partial&&!zt||De(Dt,r,M,V,i,ne,te,$t,k,et,A,Gt,Qt,I,_,S,it,m)}const Lt=o?generateTextOverlay(e,j,{plotMode:i,colorFns:$t,normalize:et,activeTest:Y,valueRange:[O,E],testDefs:y,fallbackFormat:N,passFailDisplay:_,activeMetadataKey:S,metadataColorMap:it,allIntegerValues:tt}):[],X=Te(t,F.physicalToScreen);p&&X.push(...Ie(t,F.physicalToScreen,g)),B&&X.push(...Ve(t,F.physicalToScreen,t.center.x,t.center.y)),l&&X.push(...Ce(s,F.gridToScreen)),d&&X.push(...Fe(e)),D&&X.push(...Pe(t,F.gridToScreen,Lt));const se=dt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Dt,hoverPoints:dt,texts:Lt,overlays:X,plotMode:i,binColorScheme:T,valueColorScheme:b,reverseValueScheme:$,binColors:ut,metadata:t.metadata??null,dies:e,valueRange:[O,E],allIntegerValues:tt,testDefs:y,activeTest:Y,logScale:Zt,logScaleRequested:It,aggrMethod:w,lotSize:J,axisFlip:qt,dataAxisFlip:{x:W?.x??!1,y:W?.y??!1},gridToScreen:F.gridToScreen,rotation:((t.orientation+(x?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:rt,passFailDisplay:_,colorbarRangeMode:Tt,waferCenter:t.center,waferRadius:t.radius,notchDir:Ct,binCounts:nt,metadataCounts:at,metadataColorMap:it,activeMetadataKey:S,metadataFields:vt,specCounts:ht,passFailCounts:mt,dieBounds:se}}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>(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";
@@ -2,6 +2,8 @@ import type { Die } from '../core/dies.js';
2
2
  import type { DieMetadata, WaferMetadata } from '../core/metadata.js';
3
3
  import type { Wafer } from '../core/wafer.js';
4
4
  import type { Reticle } from '../core/reticle.js';
5
+ import { type DerivedTestDef } from './derivedTests/apply.js';
6
+ export type { DerivedTestDef } from './derivedTests/apply.js';
5
7
  import { type View, type ViewOptions, type PlotMode } from './buildView.js';
6
8
  /**
7
9
  * Test result for a single die position, as output by the prober.
@@ -232,6 +234,24 @@ export interface TestDef {
232
234
  * would be meaningless. Default: `'P'`.
233
235
  */
234
236
  testType?: 'P' | 'F';
237
+ /**
238
+ * Set by `buildWaferMap` on a test computed from `derivedTests`, never by the
239
+ * caller. It lives on `TestDef` rather than only on `DerivedTestDef` because
240
+ * `result.testDefs` is one homogeneous `TestDef[]`: every display surface
241
+ * reads its test names from there, and a derived value must be
242
+ * distinguishable from a measured one without a cast — an engineer reading a
243
+ * Cpk table cannot be left to assume a number came off the tester.
244
+ */
245
+ readonly derived?: true;
246
+ /**
247
+ * The expression a `derived` test was computed from, carried through verbatim
248
+ * so a display surface can answer "where did this number come from" without
249
+ * the caller having to hold the definition alongside the result. Absent on a
250
+ * measured test.
251
+ */
252
+ readonly expression?: string;
253
+ /** Named constants the `expression` resolved, for the same reason. */
254
+ readonly constants?: Record<string, number>;
235
255
  }
236
256
  /**
237
257
  * True when `def` describes a parametric (continuous-value) test — i.e. its
@@ -368,6 +388,34 @@ export interface WaferMapInputBase {
368
388
  * Both spaces range 0–32767 and may overlap — define them separately.
369
389
  */
370
390
  sbinDefs?: BinDef[];
391
+ /**
392
+ * Tests computed from other tests on the same die, rather than measured.
393
+ *
394
+ * Each entry is a `TestDef` plus an `expression`. They are evaluated per probe
395
+ * record at build time — before lot stacking and before retest resolution, so
396
+ * every value is derived from one real touchdown — and then behave as ordinary
397
+ * tests: they appear in `testDefs`, in the value plot modes, the colorbar,
398
+ * tooltips, `analyzeWaferMap`, Insights and the report.
399
+ *
400
+ * ```ts
401
+ * derivedTests: [
402
+ * { testNumber: 900001, name: 'Leakage Shift', unit: 'uA',
403
+ * expression: 'abs(t[1020] - t[1010])', limitHigh: 5 },
404
+ * { testNumber: 900002, name: 'Sweep All Pass', testType: 'F',
405
+ * expression: 'all(testPass[1010..1025])' },
406
+ * ]
407
+ * ```
408
+ *
409
+ * Expressions are parsed to a typed tree and walked — there is no `eval`, no
410
+ * `Function`, and no way to reach a host object — so a spec can be shared as
411
+ * plain JSON between teams. An entry that cannot be compiled is DROPPED with a
412
+ * `derived-test-invalid` warning naming the position; it is never partially
413
+ * applied.
414
+ *
415
+ * A boolean expression is a verdict: declare `testType: 'F'` and the result
416
+ * lands in `die.testPass`, never as a 1/0 in `testValues`.
417
+ */
418
+ derivedTests?: DerivedTestDef[];
371
419
  /**
372
420
  * Named definitions for `die.metadata` keys that should be selectable as the
373
421
  * `'metadata'` plot mode — a generic categorical view driven by whatever
@@ -519,6 +567,15 @@ export interface WaferWarning {
519
567
  * were given as text (or verdicts as something other than true/false), as a
520
568
  * CSV parser produces. They are not converted, so those dies are judged and
521
569
  * plotted wrongly — a bin of "1" is not pass bin 1. The message counts them.
570
+ * - `'derived-test-invalid'` — raised by `buildWaferMap`: a `derivedTests` entry
571
+ * could not be compiled — a parse or type error in its `expression`, a
572
+ * `testNumber` that collides with a measured test, a `testType` that
573
+ * disagrees with what the expression produces, or an accessor that is
574
+ * statically impossible (`t[n]` on a functional test, `specPass[n]` on a test
575
+ * with no limits). The message names the test and the position in the
576
+ * expression. That derived test is DROPPED — never partially applied, since a
577
+ * half-working expression plots wrong numbers rather than no numbers. Also
578
+ * used for the non-fatal unit-mismatch advisory, where the value IS computed.
522
579
  * - `'input-field-removed'` — raised by `buildWaferMap`: the input used a name
523
580
  * removed in an earlier release (`data`, `die`, `stack`, `values`,
524
581
  * `TestDef.index`, `dieConfig.origin`, `waferConfig.flat`,
@@ -529,7 +586,7 @@ export interface WaferWarning {
529
586
  * The union is intentionally open to string so future advisory codes can be
530
587
  * added without a breaking change; switch with a `default` branch.
531
588
  */
532
- 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' | (string & {});
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 & {});
533
590
  /** Human-readable explanation, suitable for direct display. */
534
591
  message: string;
535
592
  /**