@wafertools/wafermap 0.26.0 → 0.27.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 (88) hide show
  1. package/CHANGELOG.md +367 -0
  2. package/README.md +46 -3
  3. package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
  4. package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +13 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -10
  11. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
  13. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -9
  15. package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
  16. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
  17. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
  18. package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -1
  19. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  20. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  21. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  22. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
  23. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
  24. package/dist/packages/canvas-adapter/charts/trend.d.ts +30 -0
  25. package/dist/packages/canvas-adapter/charts/trend.js +1 -0
  26. package/dist/packages/canvas-adapter/dieList.js +7 -7
  27. package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
  28. package/dist/packages/canvas-adapter/identityHeader.js +1 -0
  29. package/dist/packages/canvas-adapter/index.d.ts +1 -1
  30. package/dist/packages/canvas-adapter/insightsTab.d.ts +27 -3
  31. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  32. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  33. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
  34. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
  35. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +72 -17
  36. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  37. package/dist/packages/canvas-adapter/summaryPanel.d.ts +219 -33
  38. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  39. package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
  40. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  41. package/dist/packages/canvas-adapter/toolbar.d.ts +310 -6
  42. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  43. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  44. package/dist/packages/canvas-adapter/userGuideHtml.js +98 -35
  45. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  46. package/dist/packages/canvas-adapter/version.js +1 -1
  47. package/dist/packages/canvas-adapter/warnings.d.ts +0 -4
  48. package/dist/packages/canvas-adapter/warnings.js +1 -1
  49. package/dist/packages/core/utils.d.ts +11 -0
  50. package/dist/packages/core/utils.js +1 -1
  51. package/dist/packages/renderer/buildView.d.ts +34 -1
  52. package/dist/packages/renderer/buildView.js +1 -1
  53. package/dist/packages/renderer/buildWaferMap.d.ts +104 -5
  54. package/dist/packages/renderer/buildWaferMap.js +1 -1
  55. package/dist/packages/renderer/colorSchemes.js +1 -1
  56. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  57. package/dist/packages/stats/binPareto.d.ts +15 -0
  58. package/dist/packages/stats/binPareto.js +1 -1
  59. package/dist/packages/stats/clusterDetection.js +1 -1
  60. package/dist/packages/stats/connectedComponents.d.ts +14 -0
  61. package/dist/packages/stats/connectedComponents.js +1 -0
  62. package/dist/packages/stats/correlation.d.ts +26 -0
  63. package/dist/packages/stats/correlation.js +1 -1
  64. package/dist/packages/stats/facets.js +1 -1
  65. package/dist/packages/stats/filterFindings.d.ts +18 -0
  66. package/dist/packages/stats/filterFindings.js +1 -1
  67. package/dist/packages/stats/findingsNarrative.js +1 -1
  68. package/dist/packages/stats/histogram.d.ts +19 -1
  69. package/dist/packages/stats/histogram.js +1 -1
  70. package/dist/packages/stats/index.d.ts +6 -0
  71. package/dist/packages/stats/index.js +1 -1
  72. package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
  73. package/dist/packages/stats/mergeTestDefs.js +1 -0
  74. package/dist/packages/stats/metadataColumns.d.ts +20 -4
  75. package/dist/packages/stats/metadataColumns.js +1 -1
  76. package/dist/packages/stats/patternClassification.js +1 -1
  77. package/dist/packages/stats/renderFindingsReport.js +14 -14
  78. package/dist/packages/stats/renderSummaryReport.js +32 -29
  79. package/dist/packages/stats/reportHtml.js +38 -13
  80. package/dist/packages/stats/testPassRate.d.ts +85 -0
  81. package/dist/packages/stats/testPassRate.js +1 -0
  82. package/dist/packages/stats/trend.d.ts +40 -0
  83. package/dist/packages/stats/trend.js +1 -0
  84. package/dist/packages/stats/types.d.ts +117 -6
  85. package/llms.txt +6 -0
  86. package/package.json +7 -5
  87. package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
  88. package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
@@ -299,6 +299,21 @@ export interface WaferMapInputBase {
299
299
  * Set to an empty array to suppress yield calculation.
300
300
  */
301
301
  passBins?: number[];
302
+ /**
303
+ * Wafer diameters (mm) treated as standard when sanity-checking an *inferred*
304
+ * diameter — see {@link STANDARD_WAFER_DIAMETERS_MM} for the default
305
+ * (100/125/150/200/300) and the measurements behind it.
306
+ *
307
+ * **Replaces** the default rather than adding to it, so spread it to extend:
308
+ * `standardDiameters: [...STANDARD_WAFER_DIAMETERS_MM, 76.2]` for a line that
309
+ * also runs 3-inch. Pass `[]` to disable the check entirely, for genuinely
310
+ * non-standard substrates (panels, reclaim, odd R&D shapes) — that is the
311
+ * intended opt-out, rather than suppressing every geometry advisory.
312
+ *
313
+ * Only consulted when the diameter was inferred; a diameter you supply is
314
+ * never second-guessed.
315
+ */
316
+ standardDiameters?: number[];
302
317
  /**
303
318
  * How to handle multiple `DieResult` entries for the same die position (retests).
304
319
  *
@@ -420,13 +435,34 @@ export interface WaferWarning {
420
435
  * you asserted — it reports this so you can correct one of them. Only raised
421
436
  * when the pitch was supplied: pitch is a free scaling parameter, so with an
422
437
  * inferred pitch "the dies don't fit" is not a statement about your data.
423
- * - `'inferred-pitch'` — `waferConfig.diameter` was supplied without a die pitch,
424
- * so the pitch was derived as `diameter ÷ grid span`, assuming the grid spans
425
- * the full wafer. That assumption fails whenever edge dies are absent, which
426
- * silently scales every die position. Supply `dieConfig.width`/`height`.
438
+ * - `'non-standard-diameter'` — the wafer diameter was INFERRED from the die
439
+ * extent (a pitch was supplied, a diameter was not) and came out off the
440
+ * standard wafer-size ladder (SEMI M1: 100/150/200/300 mm and the smaller
441
+ * legacy sizes). Silicon only comes in those sizes, so a result like 210 mm
442
+ * is evidence the inference went wrong — almost always because the outer
443
+ * dies were never probed, so the die extent understates the wafer. Every die
444
+ * is then placed against a wafer that is too small, which moves dies between
445
+ * rings and changes ring and edge findings. Supply `waferConfig.diameter`.
446
+ *
447
+ * There is deliberately no matching advisory for an inferred *pitch*: that
448
+ * is derived to fit the supplied diameter, so it is always self-consistent
449
+ * and there is nothing to check it against.
450
+ * - `'diameter-exceeds-die-extent'` — a SUPPLIED `waferConfig.diameter` that the
451
+ * probed dies fill less than 75% of. The mirror of `geometry-conflict`, which
452
+ * asks whether the dies *fit*; this asks whether they *fill*. An over-large
453
+ * wafer is not harmless: ring bands are equal-radius, so it crushes dies into
454
+ * the inner rings and empties the outer ones — at a 10× diameter every die
455
+ * lands in ring 1 — and ring/quadrant/edge findings then describe the assumed
456
+ * wafer, not the probed area. A genuinely partial map looks identical and is
457
+ * equally worth reporting, so the message names both causes.
427
458
  * - `'test-count-capped'` — raised by `analyzeWaferMap`: more tests were found in
428
459
  * the die data than the analysis cap allows, so test-value analysis was skipped
429
460
  * entirely and NO test findings were produced. Pass `testNumbers` to scope it.
461
+ * - `'analysis-option-corrected'` — raised by `analyzeWaferMap`: a numeric option
462
+ * was outside the range that can produce a meaningful analysis and was clamped.
463
+ * The message names each correction. Only reachable from untyped callers now
464
+ * that the statistical thresholds are internal, but a wrong `ringCount` or
465
+ * `sectorCount` still gets here from TypeScript.
430
466
  * - `'edge-exclusion-exceeds-radius'` — `waferConfig.edgeExclusion` is larger than
431
467
  * the resolved wafer radius (most likely when the diameter was itself inferred
432
468
  * from sparse/partial data). The excluded band is clamped to the whole wafer
@@ -435,7 +471,7 @@ export interface WaferWarning {
435
471
  * The union is intentionally open to string so future advisory codes can be
436
472
  * added without a breaking change; switch with a `default` branch.
437
473
  */
438
- code: 'partial-coverage' | 'geometry-conflict' | 'inferred-pitch' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | (string & {});
474
+ code: 'partial-coverage' | 'geometry-conflict' | 'non-standard-diameter' | 'diameter-exceeds-die-extent' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | 'analysis-option-corrected' | (string & {});
439
475
  /** Human-readable explanation, suitable for direct display. */
440
476
  message: string;
441
477
  /**
@@ -453,7 +489,14 @@ export interface WaferWarning {
453
489
  confidence?: number;
454
490
  }
455
491
  export interface WaferMapResult {
492
+ /** Resolved wafer geometry — diameter, radius, centre, notch, orientation. */
456
493
  wafer: Wafer;
494
+ /**
495
+ * Every die, carrying its original grid coordinates (`x`/`y`), physical
496
+ * position, bin/test data and eligibility flags. Dies with no reported
497
+ * position are included here — guard with `hasPosition` before using
498
+ * coordinates.
499
+ */
457
500
  dies: Die[];
458
501
  /**
459
502
  * The initial plot mode selected by `buildWaferMap` — `'value'` when test values are
@@ -468,6 +511,17 @@ export interface WaferMapResult {
468
511
  * @internal Renderer-agnostic draw list consumed by `renderWaferMap` and `toCanvas`.
469
512
  * Not part of the public API — access the named fields on `WaferMapResult` instead.
470
513
  */
514
+ /**
515
+ * @internal The initial draw list. Kept on the result because `analyzeWaferMap`
516
+ * discriminates its input on `'view' in input`, and because `renderWaferMap`
517
+ * reads `dataAxisFlip` from it — not because a host should.
518
+ *
519
+ * Read the promoted top-level fields instead (`plotMode`, `metadata`,
520
+ * `isLotStack`, `hbinDefs`, `sbinDefs`, `testDefs`): this one is rebuilt on
521
+ * every option change, so anything you cache from it goes stale immediately.
522
+ * Hosts driving the low-level pipeline should call `buildView` themselves,
523
+ * which is the supported way to hold a {@link View}.
524
+ */
471
525
  view: View;
472
526
  /** Reticle configuration used to generate the overlay and reticle-local groupings. */
473
527
  reticleConfig?: ReticleConfig;
@@ -479,15 +533,26 @@ export interface WaferMapResult {
479
533
  * proportionally correct but not in physical mm.
480
534
  */
481
535
  units: 'mm' | 'normalized';
536
+ /**
537
+ * How much of the geometry had to be guessed, and how confident each guess is.
538
+ * `confidence` runs 0–1, where 1 means the value was supplied rather than
539
+ * inferred. Worth surfacing when a map looks wrong: a low `wafer` or `diePitch`
540
+ * confidence usually means sparse or partial data, and is the first thing to
541
+ * check before suspecting the data itself. See `warnings` for the advisories
542
+ * these confidences drive.
543
+ */
482
544
  inference: {
545
+ /** Wafer diameter/centre. `method` names how it was derived, e.g. `'inferred-from-xy'`. */
483
546
  wafer: {
484
547
  confidence: number;
485
548
  method: string;
486
549
  };
550
+ /** Die pitch. `units` is `'mm'` when a real physical scale was established, `'normalized'` when not. */
487
551
  diePitch: {
488
552
  confidence: number;
489
553
  units: 'mm' | 'normalized';
490
554
  };
555
+ /** The die grid's origin and step. */
491
556
  grid: {
492
557
  confidence: number;
493
558
  };
@@ -574,5 +639,39 @@ export declare function getTestPassStatus(die: Pick<Die, 'testValues' | 'testPas
574
639
  * availability checks.
575
640
  */
576
641
  export declare function dieHasTestData(die: Pick<Die, 'testValues' | 'testPass'>): boolean;
642
+ /**
643
+ * Nominal wafer diameters in mm, used to sanity-check an *inferred* diameter.
644
+ * Override per build with {@link WaferMapInputBase.standardDiameters}.
645
+ *
646
+ * This is what makes an inferred diameter checkable at all. When a die pitch is
647
+ * supplied but no diameter, the wafer is sized from the die extent — right only
648
+ * if the probed grid reaches the wafer edge, and silently wrong whenever the
649
+ * outer dies were never probed. Silicon comes in standard sizes, so landing off
650
+ * the ladder is evidence the assumption failed. (The library already leans on
651
+ * SEMI M1 for notch chord lengths — see `standardNotchLength` in core/wafer.ts.)
652
+ *
653
+ * **The size of this list is a real trade-off, measured, not guessed.** Every
654
+ * entry is one more value a wrong inference can hide behind; every omission
655
+ * wrongly accuses someone actually running that size. Across 63 fixtures
656
+ * (76.2–300 mm, square and rectangular die, 50–94% coverage):
657
+ *
658
+ * | table | caught | missed | false alarms |
659
+ * |---|---|---|---|
660
+ * | 100/150/200/300 | 31 | 8 | 3 |
661
+ * | **100/125/150/200/300 (this default)** | 31 | 8 | 3 |
662
+ * | + 76.2 | 28 | 11 | 0 |
663
+ * | + 25.4/50.8/450 | 26 | 13 | 0 |
664
+ *
665
+ * A miss is silent, which is what every one of these cases did before this check
666
+ * existed — so it costs nothing against the status quo. A false alarm is a new
667
+ * harm: it tells someone whose diameter is correct that it is suspect, with no
668
+ * way to clear it. Hosts running 3-inch or other legacy sizes should add them
669
+ * via `standardDiameters` rather than live with that.
670
+ *
671
+ * 25.4/50.8 (1"/2") and 450 mm are excluded: none are in current production
672
+ * flows, and including 50.8 demonstrably created a miss — a 76.2 mm wafer
673
+ * mis-inferring to 50 mm went undetected.
674
+ */
675
+ export declare const STANDARD_WAFER_DIAMETERS_MM: readonly number[];
577
676
  export declare function buildWaferMap(input: DieResult[] | WaferMapInput, options?: WaferMapOptions): WaferMapResult;
578
677
  //# sourceMappingURL=buildWaferMap.d.ts.map
@@ -1 +1 @@
1
- import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as fe,getDieKey as B,hasPosition as k}from"../core/dies.js";import{applyOrientation as H,transformDies as de}from"../core/transforms.js";import{affineRotation as ce,affineMirror as le,affineCompose as ue,affinePoint as he}from"../core/transforms.js";import{inferWaferFromXY as pe}from"../core/inference/wafer.js";import{resolveGridPitch as me}from"../core/inference/pitch.js";import{assignGridIndices as ye}from"../core/inference/grid.js";import{generateReticleGrid as ge}from"../core/reticle.js";import{buildView as U}from"./buildView.js";import{modeOf as xe}from"../core/utils.js";import{aggregateValues as q,aggregateBinCounts as we}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function be(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],testDefs:t.testDefs,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Me(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function De(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let u=1/0,c=-1/0,a=1/0,l=-1/0;for(const s of t)s.x<u&&(u=s.x),s.x>c&&(c=s.x),s.y<a&&(a=s.y),s.y>l&&(l=s.y);return{offsetX:Math.round((c+u)/2),offsetY:Math.round((l+a)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Oe(t,n,e,u,c,a){if(a)return{colMidX:(Math.round(a.x)-n)*u,colMidY:(Math.round(a.y)-e)*c,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let l=1/0,s=-1/0,h=1/0,p=-1/0;for(const i of t){const d=Math.round(i.x)-n,o=Math.round(i.y)-e;d<l&&(l=d),d>s&&(s=d),o<h&&(h=o),o>p&&(p=o)}return{colMidX:(l+s)/2*u,colMidY:(h+p)/2*c,anchored:!1}}function Se(t,n,e){let u=1/0,c=-1/0,a=1/0,l=-1/0,s=0,h=0;for(const d of t){const o=Math.round(d.x)-n,r=Math.round(d.y)-e;s+=o,h+=r,o<u&&(u=o),o>c&&(c=o),r<a&&(a=r),r>l&&(l=r)}const p=t.length,i=(d,o,r)=>{const m=(r-o)/2;return m<=0?0:Math.abs(d/p-(o+r)/2)/m};return i(s,u,c)>.11||i(h,a,l)>.11}function ke(t,n){let e=t?.xAxisDirection==="left",u=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(u=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:u}}function ve(t,n){const{method:e,targetBin:u}=t,c=t.results.map(a=>a.filter(k));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const a=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),l=new Set;for(const i of c)for(const d of i)if(d.testValues)for(const o of Object.keys(d.testValues)){const r=Number(o);a.has(r)||l.add(r)}if(l.size===0)return q(c,e);const s=[...l],h=s.map(i=>q(c,e,i)),p=new Map;for(let i=0;i<s.length;i++){const d=s[i];for(const o of h[i]){const r=B(o);p.has(r)||p.set(r,{template:o,testValues:{}});const m=p.get(r),y=o.testValues?.[0];y!==void 0&&(m.testValues[d]=y)}}return[...p.values()].map(({template:i,testValues:d})=>({...i,testValues:Object.keys(d).length>0?d:void 0}))}if(e==="countBin"||e==="percent"){if(u===void 0)return[];const a=we(c,u,"hard");if(e==="countBin")return a;const l=c.length;return a.map(s=>({...s,testValues:{0:l>0?(s.testValues?.[0]??0)/l*100:0}}))}if(e==="mode"){const a=new Map;for(const s of c)for(const h of s){const p=B(h);a.has(p)||a.set(p,[]),a.get(p).push(h)}const l=[];for(const[s,h]of a){const p=s.split(","),i=Number(p[0]),d=Number(p[1]),o=h.map(m=>m.hbin).filter(m=>m!==void 0),r=xe(o);r!==null&&l.push({x:i,y:d,hbin:r})}return l}return[]}function J(t){const n=t.filter(k),e=n.length,u=n.filter(a=>a.edgeExcluded).length,c=n.filter(a=>dieHasTestData(a)||a.hbin!==void 0||a.sbin!==void 0).length;return{filledDies:c,totalDies:e,edgeExcludedDies:u,unpositionedDies:t.length-n.length,ratio:e>0?c/e:0}}function Q(t,n,e="exclude"){const u=new Set(n),a=t.filter(r=>!r.partial).filter(r=>r.edgeExcluded).length,l=t.filter(r=>r.partial).length;let s=0,h=0,p=!1;for(const r of t){if(!fe(r))continue;const m=r.hbin??r.sbin;m!==void 0&&(p=!0,u.has(m)?s++:h++)}const i=s+h,d=p&&i>0?s/i*100:null;let o=null;if(e==="denominator-only"){const r=i+a;o=p&&r>0?s/r*100:null}return{passDies:s,failDies:h,edgeExcludedDies:a,partialDies:l,totalDies:i,yieldPercent:d,yieldPercentGross:o}}function Z(t,n,e,u,c,a=0,l=0,s=0,h=0,p=0,i=!1,d=!1){if(!t)return[];const o=t.anchorDie??{x:0,y:0},r=ge(n,{width:t.width,height:t.height,diePitchX:u,diePitchY:c,anchorDie:{x:o.x-a,y:o.y-l},gridOrigin:{x:-s,y:-h}}),m=ue(le(i,d,n.center.x,n.center.y),ce(p,n.center.x,n.center.y));return r.filter(y=>{const v=y.width/2,w=y.height/2,b=[[y.x-v,y.y-w],[y.x+v,y.y-w],[y.x+v,y.y+w],[y.x-v,y.y+w]].map(([g,M])=>he(m,g,M)),C=Math.min(...b.map(g=>g.x)),P=Math.max(...b.map(g=>g.x)),Y=Math.min(...b.map(g=>g.y)),F=Math.max(...b.map(g=>g.y));return e.some(g=>g.physX>=C&&g.physX<P&&g.physY>=Y&&g.physY<F)})}function Re(t,n,e){const c=Math.max(0,n.radius-e)**2;return t.map(a=>{const l=a.physX-n.center.x,s=a.physY-n.center.y;return l*l+s*s>c?{...a,edgeExcluded:!0}:a})}function Ce(t,n,e){const u=t.filter(k),c=t.filter(i=>!k(i)),a=new Map;for(const i of u){let d=a.get(i.x);d||(d=new Map,a.set(i.x,d)),d.set(i.y,(d.get(i.y)??0)+1)}const l=new Set(e);function s(i,d){const o=i.hbin,r=d.hbin;if(o===void 0||r===void 0)return!1;const m=l.has(o),y=l.has(r);return n==="best"?y!==m?y:r<o:y!==m?m:r>o}const h=new Map;for(const i of u){const d=B(i),o=h.get(d);n==="first"&&o||(n==="best"||n==="worst")&&o&&!s(o,i)||h.set(d,i)}return[...Array.from(h.values()).map(i=>{const o=a.get(i.x)?.get(i.y)??1;return o>1?{...i,retestCount:o}:i}),...c]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const u=t.testPass?.[n];if(u!==void 0)return u;if(e!==void 0&&!isParametricTest(e)){const c=t.testValues?.[n];if(c===0||c===1)return c===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function T(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function ee(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function te(t){return(t.warnings??[]).map(n=>({code:Pe(n),message:n,severity:"error",confidence:t.wafer.confidence}))}function Pe(t){return t.includes(ne)?"geometry-conflict":t.includes(ie)?"inferred-pitch":"partial-coverage"}const ne="do not fit inside the supplied",ie="The die pitch was inferred as";export function buildWaferMap(t,n){const e=be(t),{debug:u,...c}=n??{},a=e.lotStackOpts?ve(e.lotStackOpts,e.testDefs):e.results;for(let f=0;f<Math.min(5,a.length);f++){const x=a[f];if(typeof x.x=="string"||typeof x.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const f of a)if(f.x==null!=(f.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(f.x)}, y=${String(f.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const l=Ce(a,e.retestPolicy,e.passBins),s={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let f=e.explicitDies;const x=l.filter(k),R=l.filter(A=>!k(A));if(x.length>0){const A=new Map(x.map(S=>[B(S),S]));f=f.map(S=>{const z=A.get(B(S));return z?T(S,z):S})}const V=e.waferOpts?.diameter??300,E=j({diameter:V,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});E.orientation!==0&&(f=H(f,E));const K=Z(e.reticleOpts,E,f,1,1,0,0,0,0,E.orientation),re=c.showReticle??e.reticleOpts!==void 0,L=U(E,f,{...c,reticles:K,showReticle:re,plotMode:ee(l,c),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ae=R.map((A,S)=>T({id:`unpositioned_${S}`,width:f[0]?.width??1,height:f[0]?.height??1},A)),_=[...f,...ae];return{wafer:E,dies:_,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:s,warnings:te(s),plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:J(_),yield:Q(_,e.passBins,e.edgeDieYieldMode),reticles:K,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const h=l.filter(k),p=l.filter(f=>!k(f)),i=h.map(f=>({x:f.x,y:f.y})),d=me(i,e.dieOpts,e.waferOpts?.diameter);s.diePitch={confidence:d.confidence,units:d.units};const{pitchX:o,pitchY:r}=d,m=d.units,y=Me(h,e.dieOpts),v=ye(i);s.grid={confidence:v.confidence};const{offsetX:w,offsetY:b}=De(i,y,v),{flipX:C,flipY:P}=ke(e.dieOpts,y),{colMidX:Y,colMidY:F,anchored:g}=Oe(i,w,b,o,r,e.waferOpts?.center);let M=e.waferOpts?.diameter;const $=i.map(f=>({x:(Math.round(f.x)-w)*o-Y,y:(Math.round(f.y)-b)*r-F})),X=$.length>0?Math.max(...$.map(({x:f,y:x})=>Math.hypot(Math.abs(f)+o/2,Math.abs(x)+r/2))):0;if(M===void 0)if(i.length>0)if(m==="mm"){const f=pe($,{minRadius:X});M=f.diameter,s.wafer={confidence:f.confidence,method:f.method}}else M=X*2,s.wafer={confidence:d.confidence*.8,method:"extent"};else M=m==="mm"?300:30,s.wafer={confidence:0,method:"default"};if(!g&&i.length>0&&Se(i,w,b)&&(s.wafer.method="inferred-partial",(s.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0){const f=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(f&&X>M/2+1e-9){const x=$.filter(({x:R,y:V})=>Math.hypot(Math.abs(R)+o/2,Math.abs(V)+r/2)>M/2+1e-9).length;(s.warnings??=[]).push(`${x} of ${$.length} probed die positions ${ne} ${M} mm wafer at the supplied die pitch of ${o} \xD7 ${r} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(X*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}else f||(s.warnings??=[]).push(`${ie} ${o.toFixed(3)} \xD7 ${r.toFixed(3)} mm by assuming the die grid fits within the ${M} mm wafer. The assumed aspect ratio may not match the true die shape whenever edge dies are absent from the data (a reticle-complete map, or partial dies filtered out upstream). Supply dieConfig.width and dieConfig.height \u2014 the die pitch, not the diameter, is what fixes placement.`)}const O=j({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata}),Ee={width:o,height:r};let D=h.map(f=>{const x=Math.round(f.x)-w,R=Math.round(f.y)-b,V={id:`${x}_${R}`,x,y:R,physX:x*o-Y,physY:R*r-F,width:o,height:r,insideWafer:!0,partial:!1};return T(V,f)});(w!==0||b!==0)&&(D=D.map(f=>({...f,x:f.x+w,y:f.y+b,id:`${f.x+w}_${f.y+b}`}))),D=H(D,O),(C||P)&&(D=de(D,{flipX:C,flipY:P},O.center));const N=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(D=Re(D,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&N.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${O.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const G=Z(e.reticleOpts,O,D,o,r,w,b,Y,F,O.orientation,C,P),se=c.showReticle??e.reticleOpts!==void 0,I=U(O,D,{...c,reticles:G,showReticle:se,plotMode:ee(l,c),testDefs:e.testDefs,dataAxisFlip:{x:C,y:P},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),oe=p.map((f,x)=>T({id:`unpositioned_${x}`,width:o,height:r},f)),W=[...D,...oe];return{wafer:O,dies:W,view:I,reticleConfig:e.reticleOpts,units:m,inference:s,warnings:[...te(s),...N],plotMode:I.plotMode,metadata:I.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:J(W),yield:Q(W,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:I.aggrMethod,lotSize:I.lotSize}}
1
+ import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as ce,getDieKey as V,hasPosition as E}from"../core/dies.js";import{applyOrientation as q,transformDies as le}from"../core/transforms.js";import{affineRotation as ue,affineMirror as he,affineCompose as pe,affinePoint as me}from"../core/transforms.js";import{inferWaferFromXY as ge}from"../core/inference/wafer.js";import{resolveGridPitch as ye}from"../core/inference/pitch.js";import{assignGridIndices as we}from"../core/inference/grid.js";import{generateReticleGrid as xe}from"../core/reticle.js";import{buildView as H}from"./buildView.js";import{modeOf as be}from"../core/utils.js";import{aggregateValues as U,aggregateBinCounts as De}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function Me(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],standardDiameters:t.standardDiameters,testDefs:t.testDefs,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Oe(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Re(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let u=1/0,c=-1/0,a=1/0,l=-1/0;for(const s of t)s.x<u&&(u=s.x),s.x>c&&(c=s.x),s.y<a&&(a=s.y),s.y>l&&(l=s.y);return{offsetX:Math.round((c+u)/2),offsetY:Math.round((l+a)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Se(t,n,e,u,c,a){if(a)return{colMidX:(Math.round(a.x)-n)*u,colMidY:(Math.round(a.y)-e)*c,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let l=1/0,s=-1/0,h=1/0,p=-1/0;for(const i of t){const f=Math.round(i.x)-n,r=Math.round(i.y)-e;f<l&&(l=f),f>s&&(s=f),r<h&&(h=r),r>p&&(p=r)}return{colMidX:(l+s)/2*u,colMidY:(h+p)/2*c,anchored:!1}}function Ee(t,n,e){let u=1/0,c=-1/0,a=1/0,l=-1/0,s=0,h=0;for(const f of t){const r=Math.round(f.x)-n,d=Math.round(f.y)-e;s+=r,h+=d,r<u&&(u=r),r>c&&(c=r),d<a&&(a=d),d>l&&(l=d)}const p=t.length,i=(f,r,d)=>{const m=(d-r)/2;return m<=0?0:Math.abs(f/p-(r+d)/2)/m};return i(s,u,c)>.11||i(h,a,l)>.11}function ve(t,n){let e=t?.xAxisDirection==="left",u=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(u=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:u}}function ke(t,n){const{method:e,targetBin:u}=t,c=t.results.map(a=>a.filter(E));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const a=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),l=new Set;for(const i of c)for(const f of i)if(f.testValues)for(const r of Object.keys(f.testValues)){const d=Number(r);a.has(d)||l.add(d)}if(l.size===0)return U(c,e);const s=[...l],h=s.map(i=>U(c,e,i)),p=new Map;for(let i=0;i<s.length;i++){const f=s[i];for(const r of h[i]){const d=V(r);p.has(d)||p.set(d,{template:r,testValues:{}});const m=p.get(d),g=r.testValues?.[0];g!==void 0&&(m.testValues[f]=g)}}return[...p.values()].map(({template:i,testValues:f})=>({...i,testValues:Object.keys(f).length>0?f:void 0}))}if(e==="countBin"||e==="percent"){if(u===void 0)return[];const a=De(c,u,"hard");if(e==="countBin")return a;const l=c.length;return a.map(s=>({...s,testValues:{0:l>0?(s.testValues?.[0]??0)/l*100:0}}))}if(e==="mode"){const a=new Map;for(const s of c)for(const h of s){const p=V(h);a.has(p)||a.set(p,[]),a.get(p).push(h)}const l=[];for(const[s,h]of a){const p=s.split(","),i=Number(p[0]),f=Number(p[1]),r=h.map(m=>m.hbin).filter(m=>m!==void 0),d=be(r);d!==null&&l.push({x:i,y:f,hbin:d})}return l}return[]}function J(t){const n=t.filter(E),e=n.length,u=n.filter(a=>a.edgeExcluded).length,c=n.filter(a=>dieHasTestData(a)||a.hbin!==void 0||a.sbin!==void 0).length;return{filledDies:c,totalDies:e,edgeExcludedDies:u,unpositionedDies:t.length-n.length,ratio:e>0?c/e:0}}function Q(t,n,e="exclude"){const u=new Set(n),a=t.filter(d=>!d.partial).filter(d=>d.edgeExcluded).length,l=t.filter(d=>d.partial).length;let s=0,h=0,p=!1;for(const d of t){if(!ce(d))continue;const m=d.hbin??d.sbin;m!==void 0&&(p=!0,u.has(m)?s++:h++)}const i=s+h,f=p&&i>0?s/i*100:null;let r=null;if(e==="denominator-only"){const d=i+a;r=p&&d>0?s/d*100:null}return{passDies:s,failDies:h,edgeExcludedDies:a,partialDies:l,totalDies:i,yieldPercent:f,yieldPercentGross:r}}function Z(t,n,e,u,c,a=0,l=0,s=0,h=0,p=0,i=!1,f=!1){if(!t)return[];const r=t.anchorDie??{x:0,y:0},d=xe(n,{width:t.width,height:t.height,diePitchX:u,diePitchY:c,anchorDie:{x:r.x-a,y:r.y-l},gridOrigin:{x:-s,y:-h}}),m=pe(he(i,f,n.center.x,n.center.y),ue(p,n.center.x,n.center.y));return d.filter(g=>{const v=g.width/2,b=g.height/2,D=[[g.x-v,g.y-b],[g.x+v,g.y-b],[g.x+v,g.y+b],[g.x-v,g.y+b]].map(([w,x])=>me(m,w,x)),k=Math.min(...D.map(w=>w.x)),A=Math.max(...D.map(w=>w.x)),F=Math.min(...D.map(w=>w.y)),P=Math.max(...D.map(w=>w.y));return e.some(w=>w.physX>=k&&w.physX<A&&w.physY>=F&&w.physY<P)})}function Ae(t,n,e){const c=Math.max(0,n.radius-e)**2;return t.map(a=>{const l=a.physX-n.center.x,s=a.physY-n.center.y;return l*l+s*s>c?{...a,edgeExcluded:!0}:a})}function Ce(t,n,e){const u=t.filter(E),c=t.filter(i=>!E(i)),a=new Map;for(const i of u){let f=a.get(i.x);f||(f=new Map,a.set(i.x,f)),f.set(i.y,(f.get(i.y)??0)+1)}const l=new Set(e);function s(i,f){const r=i.hbin,d=f.hbin;if(r===void 0||d===void 0)return!1;const m=l.has(r),g=l.has(d);return n==="best"?g!==m?g:d<r:g!==m?m:d>r}const h=new Map;for(const i of u){const f=V(i),r=h.get(f);n==="first"&&r||(n==="best"||n==="worst")&&r&&!s(r,i)||h.set(f,i)}return[...Array.from(h.values()).map(i=>{const r=a.get(i.x)?.get(i.y)??1;return r>1?{...i,retestCount:r}:i}),...c]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const u=t.testPass?.[n];if(u!==void 0)return u;if(e!==void 0&&!isParametricTest(e)){const c=t.testValues?.[n];if(c===0||c===1)return c===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function L(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function ee(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function te(t){return(t.warnings??[]).map(n=>{const e=Ie(n);return{code:e,message:n,severity:e==="non-standard-diameter"||e==="diameter-exceeds-die-extent"?"warning":"error",confidence:t.wafer.confidence}})}function Ie(t){return t.includes(ne)?"geometry-conflict":t.includes(ie)?"non-standard-diameter":t.includes(se)?"diameter-exceeds-die-extent":"partial-coverage"}const ne="do not fit inside the supplied",ie="Inferred wafer diameter",se="The probed dies reach only",Fe=.75,Pe=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const _e=.02;function oe(t,n){return n.reduce((e,u)=>Math.abs(u-t)<Math.abs(e-t)?u:e)}function Ye(t,n){if(!n.length)return!0;const e=oe(t,n);return Math.abs(e-t)/e<=_e}function $e(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=Me(t),{debug:u,...c}=n??{},a=e.lotStackOpts?ke(e.lotStackOpts,e.testDefs):e.results;for(let o=0;o<Math.min(5,a.length);o++){const y=a[o];if(typeof y.x=="string"||typeof y.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const o of a)if(o.x==null!=(o.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(o.x)}, y=${String(o.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const l=Ce(a,e.retestPolicy,e.passBins),s={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let o=e.explicitDies;const y=l.filter(E),R=l.filter(T=>!E(T));if(y.length>0){const T=new Map(y.map(S=>[V(S),S]));o=o.map(S=>{const G=T.get(V(S));return G?L(S,G):S})}const $=e.waferOpts?.diameter??300,I=j({diameter:$,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});I.orientation!==0&&(o=q(o,I));const z=Z(e.reticleOpts,I,o,1,1,0,0,0,0,I.orientation),de=c.showReticle??e.reticleOpts!==void 0,B=H(I,o,{...c,reticles:z,showReticle:de,plotMode:ee(l,c),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),fe=R.map((T,S)=>L({id:`unpositioned_${S}`,width:o[0]?.width??1,height:o[0]?.height??1},T)),W=[...o,...fe];return{wafer:I,dies:W,view:B,reticleConfig:e.reticleOpts,units:"mm",inference:s,warnings:te(s),plotMode:B.plotMode,metadata:B.metadata,isLotStack:!1,dataCoverage:J(W),yield:Q(W,e.passBins,e.edgeDieYieldMode),reticles:z,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const h=l.filter(E),p=l.filter(o=>!E(o)),i=h.map(o=>({x:o.x,y:o.y})),f=ye(i,e.dieOpts,e.waferOpts?.diameter);s.diePitch={confidence:f.confidence,units:f.units};const{pitchX:r,pitchY:d}=f,m=f.units,g=Oe(h,e.dieOpts),v=we(i);s.grid={confidence:v.confidence};const{offsetX:b,offsetY:D}=Re(i,g,v),{flipX:k,flipY:A}=ve(e.dieOpts,g),{colMidX:F,colMidY:P,anchored:w}=Se(i,b,D,r,d,e.waferOpts?.center);let x=e.waferOpts?.diameter;const _=i.map(o=>({x:(Math.round(o.x)-b)*r-F,y:(Math.round(o.y)-D)*d-P})),C=_.length>0?Math.max(..._.map(({x:o,y})=>Math.hypot(Math.abs(o)+r/2,Math.abs(y)+d/2))):0;if(x===void 0)if(i.length>0)if(m==="mm"){const o=ge(_,{minRadius:C});x=o.diameter,s.wafer={confidence:o.confidence,method:o.method};const y=$e(e.standardDiameters);if(!Ye(x,y)){const R=oe(x,y);(s.warnings??=[]).push(`${ie} ${x.toFixed(1)} mm is not a standard wafer size (nearest is ${R} mm). The diameter was derived from the die extent, which assumes the probed grid reaches the wafer edge \u2014 a non-standard result usually means it does not, because the outer dies were never probed. Every die is then placed against a wafer that is too small, which moves dies between rings and changes ring and edge findings. Supply waferConfig.diameter.`)}}else x=C*2,s.wafer={confidence:f.confidence*.8,method:"extent"};else x=m==="mm"?300:30,s.wafer={confidence:0,method:"default"};if(!w&&i.length>0&&Ee(i,b,D)&&(s.wafer.method="inferred-partial",(s.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&C>0&&i.length>=Pe){const o=C/(x/2);o<Fe&&(s.warnings??=[]).push(`${se} ${(o*100).toFixed(0)}% of the ${x} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`)}if(e.waferOpts?.diameter!==void 0){const o=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(o&&C>x/2+1e-9){const y=_.filter(({x:R,y:$})=>Math.hypot(Math.abs(R)+r/2,Math.abs($)+d/2)>x/2+1e-9).length;(s.warnings??=[]).push(`${y} of ${_.length} probed die positions ${ne} ${x} mm wafer at the supplied die pitch of ${r} \xD7 ${d} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(C*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}}const O=j({diameter:x,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let M=h.map(o=>{const y=Math.round(o.x)-b,R=Math.round(o.y)-D,$={id:`${y}_${R}`,x:y,y:R,physX:y*r-F,physY:R*d-P,width:r,height:d,insideWafer:!0,partial:!1};return L($,o)});(b!==0||D!==0)&&(M=M.map(o=>({...o,x:o.x+b,y:o.y+D,id:`${o.x+b}_${o.y+D}`}))),M=q(M,O),(k||A)&&(M=le(M,{flipX:k,flipY:A},O.center));const X=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(M=Ae(M,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&X.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${O.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const K=Z(e.reticleOpts,O,M,r,d,b,D,F,P,O.orientation,k,A),re=c.showReticle??e.reticleOpts!==void 0,Y=H(O,M,{...c,reticles:K,showReticle:re,plotMode:ee(l,c),testDefs:e.testDefs,dataAxisFlip:{x:k,y:A},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ae=p.map((o,y)=>L({id:`unpositioned_${y}`,width:r,height:d},o)),N=[...M,...ae];return{wafer:O,dies:N,view:Y,reticleConfig:e.reticleOpts,units:m,inference:s,warnings:[...te(s),...X],plotMode:Y.plotMode,metadata:Y.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:J(N),yield:Q(N,e.passBins,e.edgeDieYieldMode),reticles:K,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:Y.aggrMethod,lotSize:Y.lotSize}}
@@ -1 +1 @@
1
- import{HARD_BIN_GREY as t,VIRIDIS as l,lerpKp as a,valueToGreyscale as i,hardBinColor as s,wangHash as u}from"./colorMap.js";const b=new Map;export function registerColorScheme(e,f){b.set(e,f)}export function getColorScheme(e){return b.get(e??"default")??b.get("default")}export function listColorSchemes(){return[...b.entries()].map(([e,f])=>({name:e,label:f.label}))}function c(e,f){const r=e.length-1;return n=>e[u(n^f)%r+1]}const d=[[0,0,255],[0,255,255],[255,255,0],[255,0,0]];registerColorScheme("default",{label:"Default",forBin:s,forValue:e=>a(d,e)}),registerColorScheme("viridis",{label:"Viridis",forBin:e=>a(l,Math.min(e,16)/16),forValue:e=>a(l,1-e)}),registerColorScheme("greyscale",{label:"Greyscale",forBin:c(t,2654435769),forValue:e=>i(1-e)});const B=["#aaaaaa","#51b3ec","#5199ec","#517fec","#5165ec","#5b51ec","#7a51ec","#1891d8","#1871d8","#1851d8","#1832d8","#2518d8","#4b18d8","#116697","#115097","#113997","#112397","#1a1197","#351197","#f39349","#f3a749","#f3bb49","#f3d149","#f3eb49","#e16a0e","#e1820e","#e19b0e","#e1b70e","#e1d70e","#9e4a0a","#9e5c0a","#9e6d0a","#9e810a","#9e970a","#54e8b7","#54e8ca","#54e8de","#54dee8","#54cae8","#1dd396","#1dd3af","#1dd3c7","#1dc7d3","#1dafd3","#149469","#14947b","#14948c","#148c94","#147b94","#bb5ce0","#d35ce0","#e05cd5","#e05cbf","#e05cab","#9b26c9","#b926c9","#c926bc","#c926a1","#c92688","#6d1b8d","#821b8d","#8d1b84","#8d1b71","#8d1b60"],I=[[0,32,77],[54,68,130],[107,107,145],[180,154,108],[253,228,32]];registerColorScheme("accessible",{label:"Accessible (CVD-safe / Cividis)",forBin:c(B,2654435769),forValue:e=>a(I,1-e)});const A=["#888888","#0d0887","#5302a3","#8b0aa5","#b83289","#db5c68","#f48849","#febc2a","#f0f921","#2c0594","#6a00a8","#a62098","#d0456d","#ec7958","#fad44c"],S=[[13,8,135],[126,3,168],[204,71,120],[248,149,64],[240,249,33]];registerColorScheme("plasma",{label:"Plasma",forBin:c(A,2654435769),forValue:e=>a(S,1-e)});const T=["#aaaaaa","#000004","#1b0c41","#4a0c6b","#781c6d","#a52c60","#cf4446","#ed6925","#fb9b06","#f7d13d","#fcffa4","#2e0a47","#8e1e6e","#c8424b","#f5a623"],V=[[0,0,4],[87,16,110],[188,55,84],[249,142,9],[252,255,164]];registerColorScheme("inferno",{label:"Inferno",forBin:c(T,2654435769),forValue:e=>a(V,1-e)});const _=["#aaaaaa","#27ae60","#e74c3c","#f39c12","#2ecc71","#c0392b","#f1c40f","#1e8449","#e67e22","#a9cce3","#922b21","#82e0aa","#d4e6f1","#fdebd0","#d5f5e3"],m=[[46,204,113],[241,196,15],[231,76,60]];registerColorScheme("traffic",{label:"Traffic (Green\u2013Yellow\u2013Red)",forBin:c(_,2654435769),forValue:e=>a(m,e)});const C=["#aaaaaa","#2980b9","#e74c3c","#1abc9c","#f39c12","#8e44ad","#e67e22","#3498db","#c0392b","#16a085","#f1c40f","#154360","#922b21","#d6eaf8","#fdedec"];registerColorScheme("thermal",{label:"Thermal (Blue\u2013Cyan\u2013Yellow\u2013Red)",forBin:c(C,2654435769),forValue:e=>a(d,e)});const o=[[0,0,128],[0,0,255],[0,255,255],[255,255,0],[255,0,0],[128,0,0]];registerColorScheme("jet",{label:"Jet (MATLAB rainbow)",forBin:e=>a(o,Math.min(e,16)/16),forValue:e=>a(o,e)});
1
+ import{HARD_BIN_GREY as t,VIRIDIS as l,lerpKp as a,valueToGreyscale as i,hardBinColor as s,wangHash as u}from"./colorMap.js";const b=new Map;export function registerColorScheme(e,f){b.set(e,f)}export function getColorScheme(e){return b.get(e??"default")??b.get("default")}export function listColorSchemes(){return[...b.entries()].map(([e,f])=>({name:e,label:f.label}))}function c(e,f){const r=e.length-1;return n=>e[u(n^f)%r+1]}const d=[[0,0,255],[0,255,255],[255,255,0],[255,0,0]];registerColorScheme("default",{label:"Default",forBin:s,forValue:e=>a(d,e)}),registerColorScheme("viridis",{label:"Viridis",forBin:e=>a(l,Math.min(e,16)/16),forValue:e=>a(l,1-e)}),registerColorScheme("greyscale",{label:"Greyscale",forBin:c(t,2654435769),forValue:e=>i(1-e)});const B=["#aaaaaa","#51b3ec","#5199ec","#517fec","#5165ec","#5b51ec","#7a51ec","#1891d8","#1871d8","#1851d8","#1832d8","#2518d8","#4b18d8","#116697","#115097","#113997","#112397","#1a1197","#351197","#f39349","#f3a749","#f3bb49","#f3d149","#f3eb49","#e16a0e","#e1820e","#e19b0e","#e1b70e","#e1d70e","#9e4a0a","#9e5c0a","#9e6d0a","#9e810a","#9e970a","#54e8b7","#54e8ca","#54e8de","#54dee8","#54cae8","#1dd396","#1dd3af","#1dd3c7","#1dc7d3","#1dafd3","#149469","#14947b","#14948c","#148c94","#147b94","#bb5ce0","#d35ce0","#e05cd5","#e05cbf","#e05cab","#9b26c9","#b926c9","#c926bc","#c926a1","#c92688","#6d1b8d","#821b8d","#8d1b84","#8d1b71","#8d1b60"],I=[[0,32,77],[54,68,130],[107,107,145],[180,154,108],[253,228,32]];registerColorScheme("accessible",{label:"Accessible (CVD-safe / Cividis)",forBin:c(B,2654435769),forValue:e=>a(I,1-e)});const A=["#888888","#0d0887","#5302a3","#8b0aa5","#b83289","#db5c68","#f48849","#febc2a","#f0f921","#2c0594","#6a00a8","#a62098","#d0456d","#ec7958","#fad44c"],S=[[13,8,135],[126,3,168],[204,71,120],[248,149,64],[240,249,33]];registerColorScheme("plasma",{label:"Plasma",forBin:c(A,2654435769),forValue:e=>a(S,1-e)});const V=["#aaaaaa","#000004","#1b0c41","#4a0c6b","#781c6d","#a52c60","#cf4446","#ed6925","#fb9b06","#f7d13d","#fcffa4","#2e0a47","#8e1e6e","#c8424b","#f5a623"],m=[[0,0,4],[87,16,110],[188,55,84],[249,142,9],[252,255,164]];registerColorScheme("inferno",{label:"Inferno",forBin:c(V,2654435769),forValue:e=>a(m,1-e)});const C=["#aaaaaa","#27ae60","#e74c3c","#f39c12","#2ecc71","#c0392b","#f1c40f","#1e8449","#e67e22","#a9cce3","#922b21","#82e0aa","#d4e6f1","#fdebd0","#d5f5e3"],_=[[46,204,113],[241,196,15],[231,76,60]];registerColorScheme("traffic",{label:"Traffic (Green\u2013Yellow\u2013Red)",forBin:c(C,2654435769),forValue:e=>a(_,e)});const h=["#aaaaaa","#2980b9","#e74c3c","#1abc9c","#f39c12","#8e44ad","#e67e22","#3498db","#c0392b","#16a085","#f1c40f","#154360","#922b21","#d6eaf8","#fdedec"];registerColorScheme("thermal",{label:"Thermal (Blue\u2013Cyan\u2013Yellow\u2013Red)",forBin:c(h,2654435769),forValue:e=>a(d,e)});const o=[[0,0,128],[0,0,255],[0,255,255],[255,255,0],[255,0,0],[128,0,0]];registerColorScheme("jet",{label:"Jet (MATLAB rainbow)",forBin:e=>a(o,Math.min(e,16)/16),forValue:e=>a(o,e)});
@@ -1 +1 @@
1
- import{isYieldEligibleDie as Z,getDieKey as x,isPositionedDie as ue}from"../core/dies.js";import{buildWaferMap as ke,getTestPassStatus as de,isParametricTest as q}from"../renderer/buildWaferMap.js";import{buildQuadrantRegions as ze,buildReticlePositionRegions as Me,buildRingRegions as Re,buildSectorRegions as Be,buildTestSiteRegions as Fe,sectorCompassNames as ge,areQuadrantsAdjacent as Pe,parseRegionKey as j}from"./regions.js";import{buildClusterFindings as Ve}from"./clusterDetection.js";import{classifyPattern as Le}from"./patternClassification.js";import{normalCdf as he}from"./math.js";import{quantile as ee}from"./math.js";import{mean as G,clamp01 as me}from"../core/utils.js";const Ne={ringCount:4,passBins:[1],significanceLevel:.05,minimumEffectSize:.2,minimumRelativeEffect:1,minimumSampleSize:5,includePartial:!1,includeEdgeExcluded:!1,enableYieldAnalysis:!0,enableHardBinAnalysis:!0,enableSoftBinAnalysis:!0,enableTestValueAnalysis:!1,computePerTestStats:!1,enableReticlePositionAnalysis:!0,enableClusterAnalysis:!0,enableAngularAnalysis:!0,sectorCount:8,enablePatternClassification:!0,minimumClusterSize:5};function Ce(s,a){const o={...s};return o.minimumClusterSize=Math.max(5,Math.round(a*.003)),o}function Ke(s){return"wafer"in s&&"dies"in s&&"view"in s?s:ke(s)}function Ee(s,a){return Z(s,a)?s.hbin!==void 0||s.sbin!==void 0||s.testValues!==void 0&&Object.keys(s.testValues).length>0:!1}function Ae(s,a,o){if(!s||a)return;const e=(o??[]).filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(e.length!==0)return n=>{for(const f of e){const r=f.testNumber,i=n.testValues?.[r];if(i!==void 0&&(f.limitLow!==void 0&&i<f.limitLow||f.limitHigh!==void 0&&i>f.limitHigh))return!0}return!1}}function xe(s,a,o){const e=new Set,n=new Set,f=new Set,r=new Map,i=new Map;for(const l of s){if(l.testValues)for(const t of Object.keys(l.testValues))e.add(Number(t));if(l.testPass)for(const t of Object.keys(l.testPass))e.add(Number(t));l.hbin!==void 0&&n.add(l.hbin),l.sbin!==void 0&&f.add(l.sbin),Z(l)&&(l.hbin!==void 0&&r.set(l.hbin,(r.get(l.hbin)??0)+1),l.sbin!==void 0&&i.set(l.sbin,(i.get(l.sbin)??0)+1))}return{totalDies:s.length,analyzedDies:a,excludedDies:s.length-a,yieldPercent:o,testsConsidered:[...e].sort((l,t)=>l-t),hardBinsConsidered:[...n].sort((l,t)=>l-t),softBinsConsidered:[...f].sort((l,t)=>l-t),...r.size?{hardBinCounts:Object.fromEntries(r)}:{},...i.size?{softBinCounts:Object.fromEntries(i)}:{}}}function Te(s,a){if(!a?.length)return;const o=a.filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(!o.length)return;const e=[];for(const n of o){const f=n.testNumber;let r=0,i=0,l=0,t=0;for(const c of s){if(c.partial||c.edgeExcluded)continue;const m=c.testValues?.[f];m!==void 0&&(t++,n.limitLow!==void 0&&m<n.limitLow?i++:n.limitHigh!==void 0&&m>n.limitHigh?l++:r++)}e.push({testNumber:f,label:n.name,passDies:r,failLowDies:i,failHighDies:l,totalDies:t,yieldPercent:t>0?r/t*100:null})}return e.length?e:void 0}export function computeFunctionalYield(s,a){if(!a?.length)return;const o=a.filter(n=>!q(n));if(!o.length)return;const e=[];for(const n of o){const f=n.testNumber;let r=0,i=0;for(const t of s){if(t.partial||t.edgeExcluded)continue;const c=de(t,f,n);c!==void 0&&(c?r++:i++)}const l=r+i;e.push({testNumber:f,label:n.name,passDies:r,failDies:i,totalDies:l,passRatePercent:l>0?r/l*100:null})}return e.length?e:void 0}function qe(s,a,o,e){const n=[];for(const f of a){const r=[];for(const m of s){if(m.partial||m.edgeExcluded)continue;const g=m.testValues?.[f];g!==void 0&&r.push(g)}if(r.length<e)continue;const i=G(r),l=Math.sqrt(re(r,i)),t=r.slice().sort((m,g)=>m-g),c=o?.find(m=>m.testNumber===f)?.name??String(f);n.push({testNumber:f,label:c,count:r.length,min:t[0],max:t[t.length-1],mean:i,stddev:l,median:ee(t,.5),q1:ee(t,.25),q3:ee(t,.75)})}return n.length?n:void 0}function H(s,a,o,e){const n=(s+o)/(a+e),f=n*(1-n)*(1/a+1/e);if(!Number.isFinite(f)||f<=0)return 1;const r=(s/a-o/e)/Math.sqrt(f);return me(2*(1-he(Math.abs(r))))}function te(s){const a=new Map;for(const o of s){const e=`${o.variable.kind}:${o.comparison.family}`,n=a.get(e)??[];n.push(o),a.set(e,n)}for(const o of a.values()){const e=[...o].filter(r=>r.stats.pValue!==void 0).sort((r,i)=>(r.stats.pValue??1)-(i.stats.pValue??1)),n=e.length;let f=1;for(let r=n-1;r>=0;r--){const i=e[r],l=i.stats.pValue??1,t=Math.min(1,Math.min(f,l*n/(r+1)));i.stats.adjustedPValue=t,f=t}}return s}function ne(s,a,o){const e=Math.abs(a),n=o!==void 0?Math.abs(o):0;return s<=.01&&(e>=.3||n>=2.5)?"unusual":s<=.05&&(e>=.2||n>=1.5)?"notable":"info"}function ie(s,a){return s<=.01&&Math.abs(a)>=.5?"unusual":s<=.05&&Math.abs(a)>=.15?"notable":"info"}function J(s){return s==="reticle-position"?"other reticle positions":s==="test-site"?"other test sites":"the rest of the map"}function O(s){return s==="reticle-position"?"Other reticle positions":s==="test-site"?"Other test sites":"Rest of map"}function be(s,a,o){const e=(Math.abs(a)*100).toFixed(1);return`${s} yield is ${e} percentage points ${a>0?"higher":"lower"} than ${J(o)}`}function se(s,a){return a==="quadrant"&&!s.startsWith("Quadrant")?`quadrant ${s}`:s}function pe(s,a,o,e){const n=se(s,e),f=(Math.abs(o)*100).toFixed(1);return`${n} has ${a} occurrence ${f} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function je(s,a,o,e){const n=se(s,e),f=(Math.abs(o)*100).toFixed(1);return`${n} has ${a} pass rate ${f} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function ye(s,a,o,e,n,f){const r=se(s,n),i=J(n),l=o>0?"higher":"lower";if(e!==void 0&&Number.isFinite(e)){const c=(Math.abs(e)*100).toFixed(1);return`${r} mean ${a} is ${c}% ${l} than ${i}`}const t=f?` ${f}`:"";return`${r} mean ${a} is ${Math.abs(o).toPrecision(3)}${t} ${l} than ${i}`}function Se(s,a,o){const e=a?.find(n=>n.bin===s);return e?.name?`${o} ${s} (${e.name})`:`${o} ${s}`}function He(s,a){const o=a?.find(e=>e.testNumber===s);return{label:o?.name??`Test ${s}`,unit:o?.unit}}function oe(s,a){const o=new Map(s.map(n=>[x(n),n])),e=new Map;for(const n of a){const f=[];for(const r of n.dieKeys){const i=o.get(r);i&&f.push(i)}e.set(n.key,f)}return e}function ae(s,a){return te(s),s.filter(o=>{const e=o.stats.adjustedPValue??o.stats.pValue??1,n=Math.abs(o.effect.absoluteDelta??0),f=Math.abs(o.effect.relativeDelta??0);return e<=a.significanceLevel&&(n>=a.minimumEffectSize||f>=a.minimumRelativeEffect)}).map(o=>({...o,severity:ne(o.stats.adjustedPValue??o.stats.pValue??1,o.effect.absoluteDelta??0,o.effect.relativeDelta)}))}function Y(s,a,o,e){const n=new Set(o),f=oe(s,a),r=new Map,i=new Map;for(const[t,c]of f){let m=0,g=0;for(const d of c)d.hbin!==void 0&&(g++,n.has(d.hbin)&&m++);r.set(t,m),i.set(t,g)}const l=[];for(const t of a){const c=i.get(t.key)??0,m=r.get(t.key)??0;let g=0,d=0;for(const[z,b]of i)z!==t.key&&(g+=b,d+=r.get(z)??0);if(c<e.minimumSampleSize||g<e.minimumSampleSize)continue;const h=m/c,p=d/g,y=h-p,S=H(m,c,d,g);l.push({id:`yield:${t.key}`,level:"wafer",severity:"info",variable:{kind:"yield",label:"Yield"},comparison:{family:t.family,left:t.label,right:O(t.family)},effect:{direction:y===0?"different":y>0?"higher":"lower",absoluteDelta:y,relativeDelta:p===0?void 0:y/p,effectSize:y},stats:{method:"two-proportion-z",pValue:S,sampleSizeLeft:c,sampleSizeRight:g},summary:be(t.label,y,t.family),highlight:{kind:"region",regionFamily:t.family,regionKeys:[t.key],dieKeys:[...t.dieKeys]}})}return ae(l,e)}function L(s,a,o,e,n,f){const r=d=>o==="soft"?d.sbin:d.hbin,i=[...new Set(s.map(r).filter(d=>d!==void 0))].sort((d,h)=>d-h),l=oe(s,a),t=new Map,c=new Map;for(const[d,h]of l){const p=new Map;for(const y of h){const S=r(y);S!==void 0&&p.set(S,(p.get(S)??0)+1)}t.set(d,p),c.set(d,h.length)}const m=[],g=n==="hardBin"?"HBin":"SBin";for(const d of a){const h=c.get(d.key)??0,p=t.get(d.key);let y=0;const S=new Map;for(const[z,b]of t)if(z!==d.key){y+=c.get(z)??0;for(const[w,D]of b)S.set(w,(S.get(w)??0)+D)}if(!(h<f.minimumSampleSize||y<f.minimumSampleSize))for(const z of i){const b=p.get(z)??0,w=S.get(z)??0,D=b/h,v=w/y,k=D-v,u=H(b,h,w,y),$=Se(z,e,g);m.push({id:`${n}:${z}:${d.key}`,level:"wafer",severity:"info",variable:{kind:n,bin:z,label:$},comparison:{family:d.family,left:d.label,right:O(d.family)},effect:{direction:k===0?"different":k>0?"higher":"lower",absoluteDelta:k,relativeDelta:v===0?void 0:k/v,effectSize:k},stats:{method:"two-proportion-z",pValue:u,sampleSizeLeft:h,sampleSizeRight:y},summary:pe(d.label,$,k,d.family),highlight:{kind:"bin",bin:z,regionKeys:[d.key],dieKeys:[...d.dieKeys]}})}}return ae(m,f)}function Q(s,a,o,e){const n=(o??[]).filter(c=>!q(c));if(!n.length)return[];const f=n.map(c=>c.testNumber),r=oe(s,a),i=new Map,l=new Map;for(const[c,m]of r){const g=new Uint32Array(n.length),d=new Uint32Array(n.length);for(const h of m)for(let p=0;p<n.length;p++){const y=de(h,f[p],n[p]);y!==void 0&&(d[p]++,y&&g[p]++)}i.set(c,g),l.set(c,d)}const t=[];for(const c of a){const m=i.get(c.key),g=l.get(c.key);for(let d=0;d<n.length;d++){let h=0,p=0;for(const[k,u]of l)k!==c.key&&(p+=u[d],h+=i.get(k)[d]);const y=g[d],S=m[d];if(y<e.minimumSampleSize||p<e.minimumSampleSize)continue;const z=S/y,b=h/p,w=z-b,D=H(S,y,h,p),v=f[d];t.push({id:`functional:${v}:${c.key}`,level:"wafer",severity:"info",variable:{kind:"functionalTest",index:v,label:`${n[d].name} pass rate`},comparison:{family:c.family,left:c.label,right:O(c.family)},effect:{direction:w===0?"different":w>0?"higher":"lower",absoluteDelta:w,relativeDelta:b===0?void 0:w/b,effectSize:w},stats:{method:"two-proportion-z",pValue:D,sampleSizeLeft:y,sampleSizeRight:p},summary:je(c.label,n[d].name,w,c.family),highlight:{kind:"region",regionFamily:c.family,regionKeys:[c.key],dieKeys:[...c.dieKeys]}})}}return ae(t,e)}function re(s,a){return s.length<2?0:s.reduce((o,e)=>o+(e-a)**2,0)/(s.length-1)}function we(s,a,o,e,n,f){const r=Math.sqrt(o/s+f/e),i=a-n;if(!Number.isFinite(r)||r===0)return{pValue:1,effectSize:0,delta:i};const l=i/r,t=Math.sqrt(Math.max(0,(o+f)/2)),c=t===0?i:i/t;return{pValue:me(2*(1-he(Math.abs(l)))),effectSize:c,delta:i}}function Oe(s,a){const o=G(s),e=G(a);return we(s.length,o,re(s,o),a.length,e,re(a,e))}const De=250;function ve(s,a,o){const e=r=>r.filter(i=>q(o?.find(l=>l.testNumber===i)));if(a)return{testNumbers:e(a.slice().sort((r,i)=>r-i))};const n=new Set;let f=!1;e:for(const r of s)for(const i of Object.keys(r.testValues??{})){const l=Number(i);if(!n.has(l)&&(n.add(l),n.size>De)){f=!0;break e}}if(f){const r=`Test-value analysis was skipped: more than ${De} tests were found in the die data, so no test findings were computed. Pass testNumbers: [...] to analyse specific tests.`;return console.warn(`[wafermap] analyzeWaferMap: ${r}`),{testNumbers:[],warning:{code:"test-count-capped",message:r,severity:"warning"}}}return{testNumbers:e([...n].sort((r,i)=>r-i))}}function _(s,a,o,e){const n=ve(s,e.testNumbers,o);if(n.warning)return{findings:[],warning:n.warning};const f=n.testNumbers,r=new Map;for(let h=0;h<a.length;h++)for(const p of a[h].dieKeys)r.set(p,h);const i=a.length,l=[],t=[];for(const h of s){const p=r.get(x(h));p!==void 0&&(l.push(h),t.push(p))}const c=[],m=new Float64Array(i),g=new Float64Array(i),d=new Float64Array(i);for(const h of f){m.fill(0),g.fill(0),d.fill(0);let p=0,y=0,S=0,z;for(let b=0;b<l.length;b++){const D=l[b].testValues?.[h];if(D===void 0)continue;z===void 0&&(z=D);const v=D-z,k=t[b];m[k]+=1,g[k]+=v,d[k]+=v*v,p+=1,y+=v,S+=v*v}if(z!==void 0)for(let b=0;b<i;b++){const w=m[b],D=p-w;if(w<e.minimumSampleSize||D<e.minimumSampleSize)continue;const v=g[b]/w,u=(y-g[b])/D,$=v+z,R=u+z,B=w>1?Math.max(0,d[b]-w*v*v)/(w-1):0,M=D>1?Math.max(0,S-d[b]-D*u*u)/(D-1):0,{pValue:N,effectSize:P,delta:C}=we(w,$,B,D,R,M),V=a[b],{label:W,unit:E}=He(h,o),U=R!==0?C/Math.abs(R):void 0;c.push({id:`test:${h}:${V.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:h,label:W,unit:E},comparison:{family:V.family,left:V.label,right:O(V.family)},effect:{direction:C===0?"different":C>0?"higher":"lower",absoluteDelta:C,relativeDelta:U,effectSize:P},stats:{method:"welch-z-approx",pValue:N,sampleSizeLeft:w,sampleSizeRight:D},summary:ye(V.label,W,C,U,V.family,E),highlight:{kind:"region",regionFamily:V.family,regionKeys:[V.key],dieKeys:[...V.dieKeys]}})}}return te(c),{activeTestNumbers:f,findings:c.filter(h=>{const p=h.stats.adjustedPValue??h.stats.pValue??1,y=Math.abs(h.effect.effectSize??0);return p<=e.significanceLevel&&y>=e.minimumEffectSize}).map(h=>({...h,severity:ie(h.stats.adjustedPValue??h.stats.pValue??1,h.effect.effectSize??0)}))}}function Ie(s,a,o,e){if(!o?.length)return[];const n=o.filter(i=>q(i)&&(i.limitLow!==void 0||i.limitHigh!==void 0));if(!n.length)return[];const f=[],r=new Map(s.map(i=>[x(i),i]));for(const i of n){const l=i.testNumber;for(const t of a){const c=new Map;for(const g of t){const d=[];for(const h of g.dieKeys){const p=r.get(h);p&&d.push(p)}c.set(g.key,d)}const m=[];for(const g of t){const d=c.get(g.key),h=[];for(const[$,R]of c)if($!==g.key)for(const B of R)h.push(B);const p=$=>$.testValues?.[l]!==void 0,y=$=>{const R=$.testValues?.[l];return R===void 0?!1:i.limitLow!==void 0&&R<i.limitLow||i.limitHigh!==void 0&&R>i.limitHigh},S=d.filter(p),z=h.filter(p);if(S.length<e.minimumSampleSize||z.length<e.minimumSampleSize)continue;const b=S.filter(y).length,w=z.filter(y).length,D=b/S.length,v=w/z.length,k=D-v,u=H(b,S.length,w,z.length);m.push({id:`specLimit:${l}:${g.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:l,label:i.name,unit:i.unit},comparison:{family:g.family,left:g.label,right:O(g.family)},effect:{direction:k===0?"different":k>0?"higher":"lower",absoluteDelta:k,relativeDelta:v===0?void 0:k/v,effectSize:k},stats:{method:"two-proportion-z",pValue:u,sampleSizeLeft:S.length,sampleSizeRight:z.length},summary:`${g.label} spec-fail rate for ${i.name} is ${(Math.abs(k)*100).toFixed(1)} pp ${k>0?"higher":"lower"} than the rest of the wafer`,highlight:{kind:"region",regionFamily:g.family,regionKeys:[g.key],dieKeys:[...g.dieKeys]}})}te(m),f.push(...m.filter(g=>{const d=g.stats.adjustedPValue??g.stats.pValue??1,h=Math.abs(g.effect.absoluteDelta??0),p=Math.abs(g.effect.relativeDelta??0);return d<=e.significanceLevel&&(h>=e.minimumEffectSize||p>=e.minimumRelativeEffect)}).map(g=>({...g,severity:ie(g.stats.adjustedPValue??g.stats.pValue??1,g.effect.effectSize??0)})))}}return f}function T(s){return s.highlight.regionKeys??[]}function Ye(s){const a=s.variable.bin??s.variable.index??"";return`${s.comparison.family}\0${s.variable.kind}\0${a}\0${s.effect.direction}`}function I(s,a){const o=T(s)[0]??"",e=j(o);return e.family==="ring"?e.ring??0:e.family==="sector"?ge(a).indexOf(e.sector??""):0}function Qe(s,a,o){if(a==="quadrant"){const r=new Map;for(const c of s){const m=j(T(c)[0]??"").quadrant;m&&r.set(m,c)}const i=[...r.keys()],l=new Set,t=[];for(const c of i){if(l.has(c))continue;const m=[],g=[c];for(l.add(c);g.length;){const d=g.pop();m.push(d);for(const h of i)!l.has(h)&&Pe(d,h)&&(l.add(h),g.push(h))}t.push(m.map(d=>r.get(d)))}return t}const e=[...s].sort((r,i)=>I(r,o)-I(i,o)),n=[];let f=[];for(const r of e){if(f.length===0){f.push(r);continue}const i=I(f[f.length-1],o);I(r,o)-i===1?f.push(r):(n.push(f),f=[r])}if(f.length&&n.push(f),a==="sector"&&n.length>1){const i=ge(o).length,l=I(n[0][0],o),t=I(n[n.length-1][n[n.length-1].length-1],o);if(l===0&&t===i-1){const c=n.pop();n[0]=[...c,...n[0]]}}return n}function _e(s,a){const o=s.map(r=>j(T(r)[0]??"").ring).filter(r=>r!==void 0).sort((r,i)=>r-i),e=o[0],n=o[o.length-1];let f="";return e===n&&e===1&&a>1?f=" (core)":e===n&&e===a&&a>1&&(f=" (edge)"),e===n?`Ring ${e}${f}`:`Rings ${e}\u2013${n}`}function We(s,a){const o=s.map(e=>j(T(e)[0]??"").sector??"");return o.length===1?`Sector ${o[0]}`:`Sectors ${o[0]}\u2013${o[o.length-1]}`}function Ue(s){const a=s.map(o=>j(T(o)[0]??"").quadrant??"");return a.length===1?a[0]:a.length===2?`Quadrants ${a[0]} & ${a[1]}`:`Quadrants ${a.slice(0,-1).join(", ")} & ${a[a.length-1]}`}function le(s){return[...new Set(s)]}function Ge(s,a){const o=s[0],e=o.comparison.family,n=o.variable.kind,f=le(s.flatMap(b=>b.highlight.dieKeys??[])),r=le(s.flatMap(T)),i=new Set(f),l=e==="ring"?_e(s,a.ringCount):e==="sector"?We(s,a.sectorCount):Ue(s),t=n==="softBin"?a.softEligibleDies:n==="test"?a.testDies:a.eligibleDies,c=t.filter(b=>i.has(x(b))),m=t.filter(b=>!i.has(x(b)));let g,d,h,p,y;if(n==="test"){const b=o.variable.index,w=M=>M.testValues?.[b],D=c.map(w).filter(M=>M!==void 0),v=m.map(w).filter(M=>M!==void 0),{pValue:k,effectSize:u,delta:$}=Oe(D,v),R=G(v),B=R!==0?$/Math.abs(R):void 0;g={direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:B,effectSize:u},d={method:"welch-z-approx",pValue:k,sampleSizeLeft:D.length,sampleSizeRight:v.length},h=ie(k,u),p=ye(l,o.variable.label,$,B,e,o.variable.unit),y=`test:${b}`}else if(n==="yield"){const b=new Set(a.passBins),w=c.filter(P=>P.hbin!==void 0),D=m.filter(P=>P.hbin!==void 0),v=w.length,k=D.length,u=w.filter(P=>b.has(P.hbin)).length,$=D.filter(P=>b.has(P.hbin)).length,R=v>0?u/v:0,B=k>0?$/k:0,M=R-B,N=H(u,v,$,k);g={direction:M===0?"different":M>0?"higher":"lower",absoluteDelta:M,relativeDelta:B===0?void 0:M/B,effectSize:M},d={method:"two-proportion-z",pValue:N,sampleSizeLeft:v,sampleSizeRight:k},h=ne(N,M,g.relativeDelta),p=be(l,M,e),y="yield"}else{const b=o.variable.bin,w=M=>n==="softBin"?M.sbin:M.hbin,D=c.filter(M=>w(M)===b).length,v=m.filter(M=>w(M)===b).length,k=D/c.length,u=v/m.length,$=k-u,R=H(D,c.length,v,m.length);g={direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:u===0?void 0:$/u,effectSize:$},d={method:"two-proportion-z",pValue:R,sampleSizeLeft:c.length,sampleSizeRight:m.length},h=ne(R,$,g.relativeDelta);const B=n==="softBin"?a.sbinDefs:a.hbinDefs;p=pe(l,Se(b,B,n==="softBin"?"SBin":"HBin"),$,e),y=`${n}:${b}`}const S=le(r.map(b=>b.replace(`${e}:`,""))).join("-"),z=n==="hardBin"||n==="softBin"?{kind:"bin",bin:o.variable.bin,regionKeys:r,dieKeys:f}:{kind:"region",regionFamily:e,regionKeys:r,dieKeys:f};return{...o,id:`${y}:${e}:${S}`,severity:h,comparison:{family:e,left:l,right:O(e)},effect:g,stats:d,summary:p,highlight:z,relatedIds:s.map(b=>b.id)}}function Je(s,a){const o=new Set(["ring","quadrant","sector"]),e=[],n=new Map;for(const r of s){if(!o.has(r.comparison.family)){e.push(r);continue}const i=Ye(r),l=n.get(i)??[];l.push(r),n.set(i,l)}const f=[];for(const r of n.values()){if(r.length===1){f.push(r[0]);continue}const i=r[0].comparison.family;for(const l of Qe(r,i,a.sectorCount))f.push(l.length===1?l[0]:Ge(l,a))}return[...e,...f]}function Xe(s){const a=new Map,o=new Map;for(const n of s){const f=x(n);n.hbin!==void 0&&n.hbin!==null&&(a.get(n.hbin)??a.set(n.hbin,new Set).get(n.hbin)).add(f),n.sbin!==void 0&&n.sbin!==null&&(o.get(n.sbin)??o.set(n.sbin,new Set).get(n.sbin)).add(f)}const e=new Set;for(const[n,f]of a)for(const[r,i]of o){if(f.size!==i.size)continue;let l=!0;for(const t of f)if(!i.has(t)){l=!1;break}l&&e.add(`${n}:${r}`)}return e}function Ze(s){return`${s.comparison.family}\0${s.comparison.left}`}function et(s,a,o){const e=(i,l)=>{i.absorbedIds=[...i.absorbedIds??[],l.id]},n=new Map;for(const i of s){const l=Ze(i);(n.get(l)??n.set(l,[]).get(l)).push(i)}const f=Xe(a),r=new Set;for(const i of n.values()){for(const t of i)if(!(t.variable.kind!=="hardBin"||t.variable.bin===void 0))for(const c of i){if(c.variable.kind!=="softBin"||c.variable.bin===void 0||r.has(c.id)||!f.has(`${t.variable.bin}:${c.variable.bin}`)||c.effect.direction!==t.effect.direction)continue;e(t,c),r.add(c.id);const m=`${t.variable.label} and ${c.variable.label} (same dies)`;t.summary=t.summary.replace(t.variable.label,m),t.variable.label=m;break}if(o.length!==1)continue;const l=i.find(t=>t.variable.kind==="yield");if(l){for(const t of i)if(!(t===l||r.has(t.id)||!((t.variable.kind==="hardBin"||t.variable.kind==="softBin")&&t.variable.bin===o[0]))){e(l,t),r.add(t.id);for(const m of t.absorbedIds??[])l.absorbedIds=[...l.absorbedIds??[],m]}}}}export function analyzeWaferMap(s,a={}){const o={...Ne,...a},e=Ke(s),n=e.isLotStack,f=e.aggrMethod,r=!n||f==="mode"||f==="countBin"||f==="percent",i=e.dies.filter(u=>Ee(u,o)),l=i.filter(ue),t=Ce(o,i.length),c=e.dies.filter(u=>Z(u,t)),m=c.filter(ue),g=Re(m,e.wafer,t.ringCount),d=ze(m,e.wafer,t.ringCount),h=t.enableReticlePositionAnalysis?Me(m,e.reticleConfig):[],p=t.enableTestSiteAnalysis===!1?[]:Fe(c,t.enableTestSiteAnalysis===!0),y=t.enableAngularAnalysis?Be(m,e.wafer,t.sectorCount):[],S=[];if(t.enableYieldAnalysis&&r&&S.push(...Y(i,g,t.passBins,t),...Y(i,d,t.passBins,t),...Y(i,h,t.passBins,t),...Y(i,p,t.passBins,t),...Y(i,y,t.passBins,t)),t.enableHardBinAnalysis&&r&&S.push(...L(i,g,"hard",e.hbinDefs,"hardBin",t),...L(i,d,"hard",e.hbinDefs,"hardBin",t),...L(i,h,"hard",e.hbinDefs,"hardBin",t),...L(i,p,"hard",e.hbinDefs,"hardBin",t),...L(i,y,"hard",e.hbinDefs,"hardBin",t)),t.enableSoftBinAnalysis){const u=i.filter($=>$.sbin!==void 0);S.push(...L(u,g,"soft",e.sbinDefs,"softBin",t),...L(u,d,"soft",e.sbinDefs,"softBin",t),...L(u,h,"soft",e.sbinDefs,"softBin",t),...L(u,p,"soft",e.sbinDefs,"softBin",t),...L(u,y,"soft",e.sbinDefs,"softBin",t))}const z=[];let b;if(t.enableTestValueAnalysis){const u=_(i,g,e.testDefs,t),$=_(i,d,e.testDefs,t),R=_(i,h,e.testDefs,t),B=_(i,p,e.testDefs,t),M=_(i,y,e.testDefs,t);S.push(...u.findings,...$.findings,...R.findings,...B.findings,...M.findings),u.warning&&z.push(u.warning),b=u.activeTestNumbers,S.push(...Ie(i,[g,d,h,p,y],e.testDefs,t)),S.push(...Q(i,g,e.testDefs,t),...Q(i,d,e.testDefs,t),...Q(i,h,e.testDefs,t),...Q(i,p,e.testDefs,t),...Q(i,y,e.testDefs,t))}if(t.enableClusterAnalysis){const u=Ae(n,r,e.testDefs);(!n||r||u!==void 0)&&S.push(...Ve(l,e.wafer,{...t,isFailingDie:u}))}const w=Je(S,{eligibleDies:i,softEligibleDies:i.filter(u=>u.sbin!==void 0),testDies:i,passBins:t.passBins,ringCount:t.ringCount,sectorCount:t.sectorCount,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs});if(S.length=0,S.push(...w),t.enablePatternClassification&&r){const u=Le(l,e.wafer,{passBins:t.passBins,ringCount:t.ringCount});if(u!==null&&u.pattern!=="random"&&u.pattern!=="none"){let $e=function(F){const K=T(F).map(A=>j(A).ring).filter(A=>A!==void 0);if(K.length===0)return!0;const fe=K.some(A=>A===U),ce=K.some(A=>A===1);return E==="edge-ring"||E==="edge-local"?fe:E==="center"?ce:E==="donut"?!fe&&!ce:!0};const R={center:"Center cluster",donut:"Donut","edge-ring":"Edge-ring","edge-local":"Edge-local",scratch:"Scratch","near-full":"Near-full"}[u.pattern]??u.pattern,B=u.confidence==="high"?"unusual":u.confidence==="medium"?"notable":"info",M=u.features,N=F=>`${(F*100).toFixed(0)}%`,P=u.pattern==="edge-ring"||u.pattern==="edge-local"?`${N(M.edgeRdd)} of edge dies failing`:u.pattern==="center"||u.pattern==="donut"?`centroid at ${N(M.centroidDistNorm)} of wafer radius from centre`:u.pattern==="scratch"?`linear score ${M.linearScore.toFixed(2)}, eccentricity ${M.eccentricity.toFixed(2)}`:`${N(M.globalRdd)} of dies failing`,C=i.filter(F=>{const K=F.hbin??F.sbin;return K!==void 0&&!new Set(t.passBins).has(K)}),V={"edge-ring":["ring","edge-arc"],"edge-local":["edge-arc","sector","quadrant"],center:["ring","cluster"],donut:["ring"],scratch:["cluster","sector","quadrant"],"near-full":["ring"]},W=new Set(V[u.pattern]??[]),E=u.pattern,U=t.ringCount,X=[];for(const F of S){if(!W.has(F.comparison.family)||F.comparison.family==="ring"&&!$e(F))continue;X.push(F.id);const K=F.comparison.family==="ring"||F.comparison.family==="quadrant"||F.comparison.family==="sector";B!=="info"&&K&&(F.severity="info")}S.push({id:`spatial-pattern:${u.pattern}`,level:"wafer",severity:B,variable:{kind:"spatialPattern",label:R},comparison:{family:"spatial-pattern",left:R,right:"Wafer"},effect:{direction:"different",effectSize:M.globalRdd},stats:{method:"geometry",sampleSizeLeft:C.length,sampleSizeRight:i.length},summary:`Spatial pattern: ${R.toLowerCase()} (${u.confidence} confidence) \u2014 ${P}${u.note?` [${u.note}]`:""}`,highlight:{kind:"dies",dieKeys:C.map(F=>x(F))},relatedIds:X.length>0?X:void 0})}}S.sort((u,$)=>{const R=u.stats.adjustedPValue??u.stats.pValue??1,B=$.stats.adjustedPValue??$.stats.pValue??1;return R!==B?R-B:Math.abs($.effect.absoluteDelta??0)-Math.abs(u.effect.absoluteDelta??0)});const D=xe(e.dies,i.length,e.yield.yieldPercent);n&&(D.isLotStack=!0,f&&(D.aggregationMethod=f));const v=Te(e.dies,e.testDefs);v&&(D.testSpecYield=v);const k=computeFunctionalYield(e.dies,e.testDefs);if(k&&(D.functionalYield=k),b===void 0&&t.computePerTestStats){const u=ve(i,t.testNumbers,e.testDefs);u.warning&&z.push(u.warning),b=u.testNumbers}if(b?.length){const u=qe(e.dies,b,e.testDefs,t.minimumSampleSize);u&&(D.perTestStats=u)}return z.length>0&&(D.warnings=z),et(S,i,t.passBins),{level:"wafer",hasNotableFindings:S.some(u=>u.severity==="notable"||u.severity==="unusual"),findings:S,wafer:e.wafer.metadata??void 0,stats:D}}
1
+ import{isYieldEligibleDie as ee,getDieKey as x,isPositionedDie as de}from"../core/dies.js";import{buildWaferMap as Re,getTestPassStatus as ge,isParametricTest as j}from"../renderer/buildWaferMap.js";import{buildQuadrantRegions as Be,buildReticlePositionRegions as Ce,buildRingRegions as Fe,buildSectorRegions as Ne,buildTestSiteRegions as Pe,sectorCompassNames as me,areQuadrantsAdjacent as Ve,parseRegionKey as H}from"./regions.js";import{buildClusterFindings as Le}from"./clusterDetection.js";import{classifyPattern as Ee}from"./patternClassification.js";import{normalCdf as he}from"./math.js";import{quantile as te}from"./math.js";import{mean as X,clamp01 as be}from"../core/utils.js";const Ae={significanceLevel:{min:1e-6,max:1},minimumEffectSize:{min:0,max:1},minimumRelativeEffect:{min:0,max:Number.MAX_SAFE_INTEGER},ringCount:{min:1,max:Number.MAX_SAFE_INTEGER}},pe=[4,8,16,32];function Ke(n){const s={..._,...n},a=[],o=[];for(const e of["significanceLevel","minimumEffectSize","minimumRelativeEffect","ringCount"]){const{min:c,max:l}=Ae[e],r=s[e];if(typeof r!="number"||!Number.isFinite(r)){o.push(`${e}=${String(r)} is not a finite number (using ${_[e]})`),s[e]=_[e];continue}const i=Math.min(l,Math.max(c,r));i!==r&&(o.push(`${e}=${r} is outside ${c}\u2013${l} (using ${i})`),s[e]=i)}if(!Number.isInteger(s.ringCount)){const e=Math.max(1,Math.round(s.ringCount));o.push(`ringCount=${s.ringCount} is not a whole number (using ${e})`),s.ringCount=e}if(pe.includes(s.sectorCount)||(o.push(`sectorCount=${s.sectorCount} is not one of ${pe.join(", ")} (using ${_.sectorCount})`),s.sectorCount=_.sectorCount),o.length){const e=`analyzeWaferMap: ${o.join("; ")}. The analysis ran with the corrected values.`;console.warn(`[wafermap] ${e}`),a.push({code:"analysis-option-corrected",message:e,severity:"warning"})}return{resolved:s,warnings:a}}const _={ringCount:4,passBins:[1],significanceLevel:.05,minimumEffectSize:.2,minimumRelativeEffect:1,minimumSampleSize:5,includePartial:!1,includeEdgeExcluded:!1,enableYieldAnalysis:!0,enableHardBinAnalysis:!0,enableSoftBinAnalysis:!0,enableTestValueAnalysis:!1,computePerTestStats:!1,enableReticlePositionAnalysis:!0,enableClusterAnalysis:!0,enableAngularAnalysis:!0,sectorCount:8,enablePatternClassification:!0,minimumClusterSize:5};function xe(n,s){const a={...n};return a.minimumClusterSize=Math.max(5,Math.round(s*.003)),a}function Te(n){return"wafer"in n&&"dies"in n&&"view"in n?n:Re(n)}function je(n,s){return ee(n,s)?n.hbin!==void 0||n.sbin!==void 0||n.testValues!==void 0&&Object.keys(n.testValues).length>0:!1}function He(n,s,a){if(!n||s)return;const o=(a??[]).filter(e=>j(e)&&(e.limitLow!==void 0||e.limitHigh!==void 0));if(o.length!==0)return e=>{for(const c of o){const l=c.testNumber,r=e.testValues?.[l];if(r!==void 0&&(c.limitLow!==void 0&&r<c.limitLow||c.limitHigh!==void 0&&r>c.limitHigh))return!0}return!1}}function qe(n,s,a){const o=new Set,e=new Set,c=new Set,l=new Map,r=new Map;for(const i of n){if(i.testValues)for(const f of Object.keys(i.testValues))o.add(Number(f));if(i.testPass)for(const f of Object.keys(i.testPass))o.add(Number(f));i.hbin!==void 0&&e.add(i.hbin),i.sbin!==void 0&&c.add(i.sbin),ee(i)&&(i.hbin!==void 0&&l.set(i.hbin,(l.get(i.hbin)??0)+1),i.sbin!==void 0&&r.set(i.sbin,(r.get(i.sbin)??0)+1))}return{totalDies:n.length,analyzedDies:s,excludedDies:n.length-s,yieldPercent:a,testsConsidered:[...o].sort((i,f)=>i-f),hardBinsConsidered:[...e].sort((i,f)=>i-f),softBinsConsidered:[...c].sort((i,f)=>i-f),...l.size?{hardBinCounts:Object.fromEntries(l)}:{},...r.size?{softBinCounts:Object.fromEntries(r)}:{}}}function Oe(n,s){if(!s?.length)return;const a=s.filter(e=>j(e)&&(e.limitLow!==void 0||e.limitHigh!==void 0));if(!a.length)return;const o=[];for(const e of a){const c=e.testNumber;let l=0,r=0,i=0,f=0;for(const t of n){if(t.partial||t.edgeExcluded)continue;const h=t.testValues?.[c];h!==void 0&&(f++,e.limitLow!==void 0&&h<e.limitLow?r++:e.limitHigh!==void 0&&h>e.limitHigh?i++:l++)}o.push({testNumber:c,label:e.name,passDies:l,failLowDies:r,failHighDies:i,totalDies:f,yieldPercent:f>0?l/f*100:null})}return o.length?o:void 0}export function computeFunctionalYield(n,s){if(!s?.length)return;const a=s.filter(e=>!j(e));if(!a.length)return;const o=[];for(const e of a){const c=e.testNumber;let l=0,r=0;for(const f of n){if(f.partial||f.edgeExcluded)continue;const t=ge(f,c,e);t!==void 0&&(t?l++:r++)}const i=l+r;o.push({testNumber:c,label:e.name,passDies:l,failDies:r,totalDies:i,passRatePercent:i>0?l/i*100:null})}return o.length?o:void 0}function Ie(n,s,a,o){const e=[];for(const c of s){const l=[];for(const h of n){if(h.partial||h.edgeExcluded)continue;const g=h.testValues?.[c];g!==void 0&&l.push(g)}if(l.length<o)continue;const r=X(l),i=Math.sqrt(le(l,r)),f=l.slice().sort((h,g)=>h-g),t=a?.find(h=>h.testNumber===c)?.name??String(c);e.push({testNumber:c,label:t,count:l.length,min:f[0],max:f[f.length-1],mean:r,stddev:i,median:te(f,.5),q1:te(f,.25),q3:te(f,.75)})}return e.length?e:void 0}function q(n,s,a,o){const e=(n+a)/(s+o),c=e*(1-e)*(1/s+1/o);if(!Number.isFinite(c)||c<=0)return 1;const l=(n/s-a/o)/Math.sqrt(c);return be(2*(1-he(Math.abs(l))))}function ne(n){const s=new Map;for(const a of n){const o=`${a.variable.kind}:${a.comparison.family}`,e=s.get(o)??[];e.push(a),s.set(o,e)}for(const a of s.values()){const o=[...a].filter(l=>l.stats.pValue!==void 0).sort((l,r)=>(l.stats.pValue??1)-(r.stats.pValue??1)),e=o.length;let c=1;for(let l=e-1;l>=0;l--){const r=o[l],i=r.stats.pValue??1,f=Math.min(1,Math.min(c,i*e/(l+1)));r.stats.adjustedPValue=f,c=f}}return n}function ie(n,s,a){const o=Math.abs(s),e=a!==void 0?Math.abs(a):0;return n<=.01&&(o>=.3||e>=2.5)?"unusual":n<=.05&&(o>=.2||e>=1.5)?"notable":"info"}function se(n,s){return n<=.01&&Math.abs(s)>=.5?"unusual":n<=.05&&Math.abs(s)>=.15?"notable":"info"}function J(n){return n==="reticle-position"?"other reticle positions":n==="test-site"?"other test sites":"the rest of the map"}function O(n){return n==="reticle-position"?"Other reticle positions":n==="test-site"?"Other test sites":"Rest of map"}function ye(n,s,a){const o=(Math.abs(s)*100).toFixed(1);return`${n} yield is ${o} percentage points ${s>0?"higher":"lower"} than ${J(a)}`}function oe(n,s){return s==="quadrant"&&!n.startsWith("Quadrant")?`quadrant ${n}`:n}function Se(n,s,a,o){const e=oe(n,o),c=(Math.abs(a)*100).toFixed(1);return`${e} has ${s} occurrence ${c} percentage points ${a>0?"higher":"lower"} than ${J(o)}`}function _e(n,s,a,o){const e=oe(n,o),c=(Math.abs(a)*100).toFixed(1);return`${e} has ${s} pass rate ${c} percentage points ${a>0?"higher":"lower"} than ${J(o)}`}function we(n,s,a,o,e,c){const l=oe(n,e),r=J(e),i=a>0?"higher":"lower";if(o!==void 0&&Number.isFinite(o)){const t=(Math.abs(o)*100).toFixed(1);return`${l} mean ${s} is ${t}% ${i} than ${r}`}const f=c?` ${c}`:"";return`${l} mean ${s} is ${Math.abs(a).toPrecision(3)}${f} ${i} than ${r}`}function $e(n,s,a){const o=s?.find(e=>e.bin===n);return o?.name?`${a} ${n} (${o.name})`:`${a} ${n}`}function Ye(n,s){const a=s?.find(o=>o.testNumber===n);return{label:a?.name??`Test ${n}`,unit:a?.unit}}function ae(n,s){const a=new Map(n.map(e=>[x(e),e])),o=new Map;for(const e of s){const c=[];for(const l of e.dieKeys){const r=a.get(l);r&&c.push(r)}o.set(e.key,c)}return o}function re(n,s){return ne(n),n.filter(a=>{const o=a.stats.adjustedPValue??a.stats.pValue??1,e=Math.abs(a.effect.absoluteDelta??0),c=Math.abs(a.effect.relativeDelta??0);return o<=s.significanceLevel&&(e>=s.minimumEffectSize||c>=s.minimumRelativeEffect)}).map(a=>({...a,severity:ie(a.stats.adjustedPValue??a.stats.pValue??1,a.effect.absoluteDelta??0,a.effect.relativeDelta)}))}function Y(n,s,a,o){const e=new Set(a),c=ae(n,s),l=new Map,r=new Map;for(const[f,t]of c){let h=0,g=0;for(const d of t)d.hbin!==void 0&&(g++,e.has(d.hbin)&&h++);l.set(f,h),r.set(f,g)}const i=[];for(const f of s){const t=r.get(f.key)??0,h=l.get(f.key)??0;let g=0,d=0;for(const[y,b]of r)y!==f.key&&(g+=b,d+=l.get(y)??0);if(t<o.minimumSampleSize||g<o.minimumSampleSize)continue;const m=h/t,p=d/g,S=m-p,D=q(h,t,d,g);i.push({id:`yield:${f.key}`,level:"wafer",severity:"info",variable:{kind:"yield",label:"Yield"},comparison:{family:f.family,left:f.label,right:O(f.family)},effect:{direction:S===0?"different":S>0?"higher":"lower",absoluteDelta:S,relativeDelta:p===0?void 0:S/p,effectSize:S},stats:{method:"two-proportion-z",pValue:D,sampleSizeLeft:t,sampleSizeRight:g},summary:ye(f.label,S,f.family),highlight:{kind:"region",regionFamily:f.family,regionKeys:[f.key],dieKeys:[...f.dieKeys]}})}return re(i,o)}function V(n,s,a,o,e,c){const l=d=>a==="soft"?d.sbin:d.hbin,r=[...new Set(n.map(l).filter(d=>d!==void 0))].sort((d,m)=>d-m),i=ae(n,s),f=new Map,t=new Map;for(const[d,m]of i){const p=new Map;for(const S of m){const D=l(S);D!==void 0&&p.set(D,(p.get(D)??0)+1)}f.set(d,p),t.set(d,m.length)}const h=[],g=e==="hardBin"?"HBin":"SBin";for(const d of s){const m=t.get(d.key)??0,p=f.get(d.key);let S=0;const D=new Map;for(const[y,b]of f)if(y!==d.key){S+=t.get(y)??0;for(const[$,k]of b)D.set($,(D.get($)??0)+k)}if(!(m<c.minimumSampleSize||S<c.minimumSampleSize))for(const y of r){const b=p.get(y)??0,$=D.get(y)??0,k=b/m,w=$/S,v=k-w,R=q(b,m,$,S),u=$e(y,o,g);h.push({id:`${e}:${y}:${d.key}`,level:"wafer",severity:"info",variable:{kind:e,bin:y,label:u},comparison:{family:d.family,left:d.label,right:O(d.family)},effect:{direction:v===0?"different":v>0?"higher":"lower",absoluteDelta:v,relativeDelta:w===0?void 0:v/w,effectSize:v},stats:{method:"two-proportion-z",pValue:R,sampleSizeLeft:m,sampleSizeRight:S},summary:Se(d.label,u,v,d.family),highlight:{kind:"bin",bin:y,regionKeys:[d.key],dieKeys:[...d.dieKeys]}})}}return re(h,c)}function W(n,s,a,o){const e=(a??[]).filter(t=>!j(t));if(!e.length)return[];const c=e.map(t=>t.testNumber),l=ae(n,s),r=new Map,i=new Map;for(const[t,h]of l){const g=new Uint32Array(e.length),d=new Uint32Array(e.length);for(const m of h)for(let p=0;p<e.length;p++){const S=ge(m,c[p],e[p]);S!==void 0&&(d[p]++,S&&g[p]++)}r.set(t,g),i.set(t,d)}const f=[];for(const t of s){const h=r.get(t.key),g=i.get(t.key);for(let d=0;d<e.length;d++){let m=0,p=0;for(const[v,R]of i)v!==t.key&&(p+=R[d],m+=r.get(v)[d]);const S=g[d],D=h[d];if(S<o.minimumSampleSize||p<o.minimumSampleSize)continue;const y=D/S,b=m/p,$=y-b,k=q(D,S,m,p),w=c[d];f.push({id:`functional:${w}:${t.key}`,level:"wafer",severity:"info",variable:{kind:"functionalTest",index:w,label:`${e[d].name} pass rate`},comparison:{family:t.family,left:t.label,right:O(t.family)},effect:{direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:b===0?void 0:$/b,effectSize:$},stats:{method:"two-proportion-z",pValue:k,sampleSizeLeft:S,sampleSizeRight:p},summary:_e(t.label,e[d].name,$,t.family),highlight:{kind:"region",regionFamily:t.family,regionKeys:[t.key],dieKeys:[...t.dieKeys]}})}}return re(f,o)}function le(n,s){return n.length<2?0:n.reduce((a,o)=>a+(o-s)**2,0)/(n.length-1)}function ve(n,s,a,o,e,c){const l=Math.sqrt(a/n+c/o),r=s-e;if(!Number.isFinite(l)||l===0)return{pValue:1,effectSize:0,delta:r};const i=r/l,f=Math.sqrt(Math.max(0,(a+c)/2)),t=f===0?r:r/f;return{pValue:be(2*(1-he(Math.abs(i)))),effectSize:t,delta:r}}function We(n,s){const a=X(n),o=X(s);return ve(n.length,a,le(n,a),s.length,o,le(s,o))}const De=250;function ke(n,s,a){const o=l=>l.filter(r=>j(a?.find(i=>i.testNumber===r)));if(s)return{testNumbers:o(s.slice().sort((l,r)=>l-r))};const e=new Set;let c=!1;e:for(const l of n)for(const r of Object.keys(l.testValues??{})){const i=Number(r);if(!e.has(i)&&(e.add(i),e.size>De)){c=!0;break e}}if(c){const l=`Test-value analysis was skipped: more than ${De} tests were found in the die data, so no test findings were computed. Pass testNumbers: [...] to analyse specific tests.`;return console.warn(`[wafermap] analyzeWaferMap: ${l}`),{testNumbers:[],warning:{code:"test-count-capped",message:l,severity:"warning"}}}return{testNumbers:o([...e].sort((l,r)=>l-r))}}function Q(n,s,a,o){const e=ke(n,o.testNumbers,a);if(e.warning)return{findings:[],warning:e.warning};const c=e.testNumbers,l=new Map;for(let m=0;m<s.length;m++)for(const p of s[m].dieKeys)l.set(p,m);const r=s.length,i=[],f=[];for(const m of n){const p=l.get(x(m));p!==void 0&&(i.push(m),f.push(p))}const t=[],h=new Float64Array(r),g=new Float64Array(r),d=new Float64Array(r);for(const m of c){h.fill(0),g.fill(0),d.fill(0);let p=0,S=0,D=0,y;for(let b=0;b<i.length;b++){const k=i[b].testValues?.[m];if(k===void 0)continue;y===void 0&&(y=k);const w=k-y,v=f[b];h[v]+=1,g[v]+=w,d[v]+=w*w,p+=1,S+=w,D+=w*w}if(y!==void 0)for(let b=0;b<r;b++){const $=h[b],k=p-$;if($<o.minimumSampleSize||k<o.minimumSampleSize)continue;const w=g[b]/$,R=(S-g[b])/k,u=w+y,M=R+y,B=$>1?Math.max(0,d[b]-$*w*w)/($-1):0,z=k>1?Math.max(0,D-d[b]-k*R*R)/(k-1):0,{pValue:F,effectSize:N,delta:E}=ve($,u,B,k,M,z),P=s[b],{label:U,unit:G}=Ye(m,a),A=M!==0?E/Math.abs(M):void 0;t.push({id:`test:${m}:${P.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:m,label:U,unit:G},comparison:{family:P.family,left:P.label,right:O(P.family)},effect:{direction:E===0?"different":E>0?"higher":"lower",absoluteDelta:E,relativeDelta:A,effectSize:N},stats:{method:"welch-z-approx",pValue:F,sampleSizeLeft:$,sampleSizeRight:k},summary:we(P.label,U,E,A,P.family,G),highlight:{kind:"region",regionFamily:P.family,regionKeys:[P.key],dieKeys:[...P.dieKeys]}})}}return ne(t),{activeTestNumbers:c,findings:t.filter(m=>{const p=m.stats.adjustedPValue??m.stats.pValue??1,S=Math.abs(m.effect.effectSize??0);return p<=o.significanceLevel&&S>=o.minimumEffectSize}).map(m=>({...m,severity:se(m.stats.adjustedPValue??m.stats.pValue??1,m.effect.effectSize??0)}))}}function Qe(n,s,a,o){if(!a?.length)return[];const e=a.filter(r=>j(r)&&(r.limitLow!==void 0||r.limitHigh!==void 0));if(!e.length)return[];const c=[],l=new Map(n.map(r=>[x(r),r]));for(const r of e){const i=r.testNumber;for(const f of s){const t=new Map;for(const g of f){const d=[];for(const m of g.dieKeys){const p=l.get(m);p&&d.push(p)}t.set(g.key,d)}const h=[];for(const g of f){const d=t.get(g.key),m=[];for(const[u,M]of t)if(u!==g.key)for(const B of M)m.push(B);const p=u=>u.testValues?.[i]!==void 0,S=u=>{const M=u.testValues?.[i];return M===void 0?!1:r.limitLow!==void 0&&M<r.limitLow||r.limitHigh!==void 0&&M>r.limitHigh},D=d.filter(p),y=m.filter(p);if(D.length<o.minimumSampleSize||y.length<o.minimumSampleSize)continue;const b=D.filter(S).length,$=y.filter(S).length,k=b/D.length,w=$/y.length,v=k-w,R=q(b,D.length,$,y.length);h.push({id:`specLimit:${i}:${g.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:i,label:r.name,unit:r.unit},comparison:{family:g.family,left:g.label,right:O(g.family)},effect:{direction:v===0?"different":v>0?"higher":"lower",absoluteDelta:v,relativeDelta:w===0?void 0:v/w,effectSize:v},stats:{method:"two-proportion-z",pValue:R,sampleSizeLeft:D.length,sampleSizeRight:y.length},summary:`${g.label} spec-fail rate for ${r.name} is ${(Math.abs(v)*100).toFixed(1)} pp ${v>0?"higher":"lower"} than the rest of the wafer`,highlight:{kind:"region",regionFamily:g.family,regionKeys:[g.key],dieKeys:[...g.dieKeys]}})}ne(h),c.push(...h.filter(g=>{const d=g.stats.adjustedPValue??g.stats.pValue??1,m=Math.abs(g.effect.absoluteDelta??0),p=Math.abs(g.effect.relativeDelta??0);return d<=o.significanceLevel&&(m>=o.minimumEffectSize||p>=o.minimumRelativeEffect)}).map(g=>({...g,severity:se(g.stats.adjustedPValue??g.stats.pValue??1,g.effect.effectSize??0)})))}}return c}function T(n){return n.highlight.regionKeys??[]}function Ue(n){const s=n.variable.bin??n.variable.index??"";return`${n.comparison.family}\0${n.variable.kind}\0${s}\0${n.effect.direction}`}function I(n,s){const a=T(n)[0]??"",o=H(a);return o.family==="ring"?o.ring??0:o.family==="sector"?me(s).indexOf(o.sector??""):0}function Ge(n,s,a){if(s==="quadrant"){const l=new Map;for(const t of n){const h=H(T(t)[0]??"").quadrant;h&&l.set(h,t)}const r=[...l.keys()],i=new Set,f=[];for(const t of r){if(i.has(t))continue;const h=[],g=[t];for(i.add(t);g.length;){const d=g.pop();h.push(d);for(const m of r)!i.has(m)&&Ve(d,m)&&(i.add(m),g.push(m))}f.push(h.map(d=>l.get(d)))}return f}const o=[...n].sort((l,r)=>I(l,a)-I(r,a)),e=[];let c=[];for(const l of o){if(c.length===0){c.push(l);continue}const r=I(c[c.length-1],a);I(l,a)-r===1?c.push(l):(e.push(c),c=[l])}if(c.length&&e.push(c),s==="sector"&&e.length>1){const r=me(a).length,i=I(e[0][0],a),f=I(e[e.length-1][e[e.length-1].length-1],a);if(i===0&&f===r-1){const t=e.pop();e[0]=[...t,...e[0]]}}return e}function Xe(n,s){const a=n.map(l=>H(T(l)[0]??"").ring).filter(l=>l!==void 0).sort((l,r)=>l-r),o=a[0],e=a[a.length-1];let c="";return o===e&&o===1&&s>1?c=" (core)":o===e&&o===s&&s>1&&(c=" (edge)"),o===e?`Ring ${o}${c}`:`Rings ${o}\u2013${e}`}function Je(n,s){const a=n.map(o=>H(T(o)[0]??"").sector??"");return a.length===1?`Sector ${a[0]}`:`Sectors ${a[0]}\u2013${a[a.length-1]}`}function Ze(n){const s=n.map(a=>H(T(a)[0]??"").quadrant??"");return s.length===1?s[0]:s.length===2?`Quadrants ${s[0]} & ${s[1]}`:`Quadrants ${s.slice(0,-1).join(", ")} & ${s[s.length-1]}`}function fe(n){return[...new Set(n)]}function et(n,s){const a=n[0],o=a.comparison.family,e=a.variable.kind,c=fe(n.flatMap(b=>b.highlight.dieKeys??[])),l=fe(n.flatMap(T)),r=new Set(c),i=o==="ring"?Xe(n,s.ringCount):o==="sector"?Je(n,s.sectorCount):Ze(n),f=e==="softBin"?s.softEligibleDies:e==="test"?s.testDies:s.eligibleDies,t=f.filter(b=>r.has(x(b))),h=f.filter(b=>!r.has(x(b)));let g,d,m,p,S;if(e==="test"){const b=a.variable.index,$=z=>z.testValues?.[b],k=t.map($).filter(z=>z!==void 0),w=h.map($).filter(z=>z!==void 0),{pValue:v,effectSize:R,delta:u}=We(k,w),M=X(w),B=M!==0?u/Math.abs(M):void 0;g={direction:u===0?"different":u>0?"higher":"lower",absoluteDelta:u,relativeDelta:B,effectSize:R},d={method:"welch-z-approx",pValue:v,sampleSizeLeft:k.length,sampleSizeRight:w.length},m=se(v,R),p=we(i,a.variable.label,u,B,o,a.variable.unit),S=`test:${b}`}else if(e==="yield"){const b=new Set(s.passBins),$=t.filter(N=>N.hbin!==void 0),k=h.filter(N=>N.hbin!==void 0),w=$.length,v=k.length,R=$.filter(N=>b.has(N.hbin)).length,u=k.filter(N=>b.has(N.hbin)).length,M=w>0?R/w:0,B=v>0?u/v:0,z=M-B,F=q(R,w,u,v);g={direction:z===0?"different":z>0?"higher":"lower",absoluteDelta:z,relativeDelta:B===0?void 0:z/B,effectSize:z},d={method:"two-proportion-z",pValue:F,sampleSizeLeft:w,sampleSizeRight:v},m=ie(F,z,g.relativeDelta),p=ye(i,z,o),S="yield"}else{const b=a.variable.bin,$=z=>e==="softBin"?z.sbin:z.hbin,k=t.filter(z=>$(z)===b).length,w=h.filter(z=>$(z)===b).length,v=k/t.length,R=w/h.length,u=v-R,M=q(k,t.length,w,h.length);g={direction:u===0?"different":u>0?"higher":"lower",absoluteDelta:u,relativeDelta:R===0?void 0:u/R,effectSize:u},d={method:"two-proportion-z",pValue:M,sampleSizeLeft:t.length,sampleSizeRight:h.length},m=ie(M,u,g.relativeDelta);const B=e==="softBin"?s.sbinDefs:s.hbinDefs;p=Se(i,$e(b,B,e==="softBin"?"SBin":"HBin"),u,o),S=`${e}:${b}`}const D=fe(l.map(b=>b.replace(`${o}:`,""))).join("-"),y=e==="hardBin"||e==="softBin"?{kind:"bin",bin:a.variable.bin,regionKeys:l,dieKeys:c}:{kind:"region",regionFamily:o,regionKeys:l,dieKeys:c};return{...a,id:`${S}:${o}:${D}`,severity:m,comparison:{family:o,left:i,right:O(o)},effect:g,stats:d,summary:p,highlight:y,relatedIds:n.map(b=>b.id)}}function tt(n,s){const a=new Set(["ring","quadrant","sector"]),o=[],e=new Map;for(const l of n){if(!a.has(l.comparison.family)){o.push(l);continue}const r=Ue(l),i=e.get(r)??[];i.push(l),e.set(r,i)}const c=[];for(const l of e.values()){if(l.length===1){c.push(l[0]);continue}const r=l[0].comparison.family;for(const i of Ge(l,r,s.sectorCount))c.push(i.length===1?i[0]:et(i,s))}return[...o,...c]}function nt(n){const s=new Map,a=new Map;for(const e of n){const c=x(e);e.hbin!==void 0&&e.hbin!==null&&(s.get(e.hbin)??s.set(e.hbin,new Set).get(e.hbin)).add(c),e.sbin!==void 0&&e.sbin!==null&&(a.get(e.sbin)??a.set(e.sbin,new Set).get(e.sbin)).add(c)}const o=new Set;for(const[e,c]of s)for(const[l,r]of a){if(c.size!==r.size)continue;let i=!0;for(const f of c)if(!r.has(f)){i=!1;break}i&&o.add(`${e}:${l}`)}return o}function it(n){return`${n.comparison.family}\0${n.comparison.left}`}function st(n,s,a){const o=(r,i)=>{r.absorbedIds=[...r.absorbedIds??[],i.id]},e=new Map;for(const r of n){const i=it(r);(e.get(i)??e.set(i,[]).get(i)).push(r)}const c=nt(s),l=new Set;for(const r of e.values()){for(const f of r)if(!(f.variable.kind!=="hardBin"||f.variable.bin===void 0))for(const t of r){if(t.variable.kind!=="softBin"||t.variable.bin===void 0||l.has(t.id)||!c.has(`${f.variable.bin}:${t.variable.bin}`)||t.effect.direction!==f.effect.direction)continue;o(f,t),l.add(t.id);const h=f.variable.label.replace(/^HBin\s+/,""),g=t.variable.label.replace(/^SBin\s+/,""),d=h===g?`HBin and SBin ${h} (same dies)`:`${f.variable.label} and ${t.variable.label} (same dies)`;f.summary=f.summary.replace(f.variable.label,d),f.variable.label=d;break}if(a.length!==1)continue;const i=r.find(f=>f.variable.kind==="yield");if(i){for(const f of r)if(!(f===i||l.has(f.id)||!((f.variable.kind==="hardBin"||f.variable.kind==="softBin")&&f.variable.bin===a[0]))){o(i,f),l.add(f.id);for(const h of f.absorbedIds??[])i.absorbedIds=[...i.absorbedIds??[],h]}}}}export function analyzeWaferMap(n,s={}){const{resolved:a,warnings:o}=Ke(s),e=Te(n),c=e.isLotStack,l=e.aggrMethod,r=!c||l==="mode"||l==="countBin"||l==="percent",i=e.dies.filter(u=>je(u,a)),f=i.filter(de),t=xe(a,i.length),h=e.dies.filter(u=>ee(u,t)),g=h.filter(de),d=Fe(g,e.wafer,t.ringCount),m=Be(g,e.wafer,t.ringCount),p=t.enableReticlePositionAnalysis?Ce(g,e.reticleConfig):[],S=t.enableTestSiteAnalysis===!1?[]:Pe(h,t.enableTestSiteAnalysis===!0),D=t.enableAngularAnalysis?Ne(g,e.wafer,t.sectorCount):[],y=[];if(t.enableYieldAnalysis&&r&&y.push(...Y(i,d,t.passBins,t),...Y(i,m,t.passBins,t),...Y(i,p,t.passBins,t),...Y(i,S,t.passBins,t),...Y(i,D,t.passBins,t)),t.enableHardBinAnalysis&&r&&y.push(...V(i,d,"hard",e.hbinDefs,"hardBin",t),...V(i,m,"hard",e.hbinDefs,"hardBin",t),...V(i,p,"hard",e.hbinDefs,"hardBin",t),...V(i,S,"hard",e.hbinDefs,"hardBin",t),...V(i,D,"hard",e.hbinDefs,"hardBin",t)),t.enableSoftBinAnalysis){const u=i.filter(M=>M.sbin!==void 0);y.push(...V(u,d,"soft",e.sbinDefs,"softBin",t),...V(u,m,"soft",e.sbinDefs,"softBin",t),...V(u,p,"soft",e.sbinDefs,"softBin",t),...V(u,S,"soft",e.sbinDefs,"softBin",t),...V(u,D,"soft",e.sbinDefs,"softBin",t))}const b=[...o];let $;if(t.enableTestValueAnalysis){const u=Q(i,d,e.testDefs,t),M=Q(i,m,e.testDefs,t),B=Q(i,p,e.testDefs,t),z=Q(i,S,e.testDefs,t),F=Q(i,D,e.testDefs,t);y.push(...u.findings,...M.findings,...B.findings,...z.findings,...F.findings),u.warning&&b.push(u.warning),$=u.activeTestNumbers,y.push(...Qe(i,[d,m,p,S,D],e.testDefs,t)),y.push(...W(i,d,e.testDefs,t),...W(i,m,e.testDefs,t),...W(i,p,e.testDefs,t),...W(i,S,e.testDefs,t),...W(i,D,e.testDefs,t))}if(t.enableClusterAnalysis){const u=He(c,r,e.testDefs);(!c||r||u!==void 0)&&y.push(...Le(f,e.wafer,{...t,isFailingDie:u}))}const k=tt(y,{eligibleDies:i,softEligibleDies:i.filter(u=>u.sbin!==void 0),testDies:i,passBins:t.passBins,ringCount:t.ringCount,sectorCount:t.sectorCount,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs});if(y.length=0,y.push(...k),t.enablePatternClassification&&r){const u=Ee(f,e.wafer,{passBins:t.passBins,ringCount:t.ringCount});if(u!==null&&u.pattern!=="random"&&u.pattern!=="none"){let Me=function(C){const L=T(C).map(K=>H(K).ring).filter(K=>K!==void 0);if(L.length===0)return!0;const ce=L.some(K=>K===ze),ue=L.some(K=>K===1);return A==="edge-ring"||A==="edge-local"?ce:A==="center"?ue:A==="donut"?!ce&&!ue:!0};const B={center:"Center cluster",donut:"Donut","edge-ring":"Edge-ring","edge-local":"Edge-local",scratch:"Scratch","near-full":"Near-full"}[u.pattern]??u.pattern,z=u.confidence==="high"?"unusual":u.confidence==="medium"?"notable":"info",F=u.features,N=C=>`${(C*100).toFixed(0)}%`,E=u.pattern==="edge-ring"||u.pattern==="edge-local"?`${N(F.edgeRdd)} of edge dies failing`:u.pattern==="center"||u.pattern==="donut"?`centroid at ${N(F.centroidDistNorm)} of wafer radius from centre`:u.pattern==="scratch"?`linear score ${F.linearScore.toFixed(2)}, eccentricity ${F.eccentricity.toFixed(2)}`:`${N(F.globalRdd)} of dies failing`,P=i.filter(C=>{const L=C.hbin??C.sbin;return L!==void 0&&!new Set(t.passBins).has(L)}),U={"edge-ring":["ring","edge-arc"],"edge-local":["edge-arc","sector","quadrant"],center:["ring","cluster"],donut:["ring"],scratch:["cluster","sector","quadrant"],"near-full":["ring"]},G=new Set(U[u.pattern]??[]),A=u.pattern,ze=t.ringCount,Z=[];for(const C of y){if(!G.has(C.comparison.family)||C.comparison.family==="ring"&&!Me(C))continue;Z.push(C.id);const L=C.comparison.family==="ring"||C.comparison.family==="quadrant"||C.comparison.family==="sector";z!=="info"&&L&&(C.severity="info")}y.push({id:`spatial-pattern:${u.pattern}`,level:"wafer",severity:z,variable:{kind:"spatialPattern",label:B},comparison:{family:"spatial-pattern",left:B,right:"Wafer"},effect:{direction:"different",effectSize:F.globalRdd},stats:{method:"geometry",sampleSizeLeft:P.length,sampleSizeRight:i.length},summary:`Spatial pattern: ${B.toLowerCase()} (${u.confidence} confidence) \u2014 ${E}${u.note?` [${u.note}]`:""}`,highlight:{kind:"dies",dieKeys:P.map(C=>x(C))},relatedIds:Z.length>0?Z:void 0})}}y.sort((u,M)=>{const B=u.stats.adjustedPValue??u.stats.pValue??1,z=M.stats.adjustedPValue??M.stats.pValue??1;return B!==z?B-z:Math.abs(M.effect.absoluteDelta??0)-Math.abs(u.effect.absoluteDelta??0)});const w=qe(e.dies,i.length,e.yield.yieldPercent);c&&(w.isLotStack=!0,l&&(w.aggregationMethod=l));const v=Oe(e.dies,e.testDefs);v&&(w.testSpecYield=v);const R=computeFunctionalYield(e.dies,e.testDefs);if(R&&(w.functionalYield=R),$===void 0&&t.computePerTestStats){const u=ke(i,t.testNumbers,e.testDefs);u.warning&&b.push(u.warning),$=u.testNumbers}if($?.length){const u=Ie(e.dies,$,e.testDefs,t.minimumSampleSize);u&&(w.perTestStats=u)}return b.length>0&&(w.warnings=b),st(y,i,t.passBins),{level:"wafer",hasNotableFindings:y.some(u=>u.severity==="notable"||u.severity==="unusual"),findings:y,wafer:e.wafer.metadata??void 0,stats:w}}
@@ -46,4 +46,19 @@ export declare function buildBinClusterData(groups: {
46
46
  key: string;
47
47
  items: BinItem[];
48
48
  }[], binType: BinType): BinClusterData;
49
+ /**
50
+ * Display order for a bin breakdown: pass bins first (in bin order), then failing
51
+ * bins by descending count, ties broken by bin number.
52
+ *
53
+ * The single source of this ordering. It previously existed as three separate
54
+ * ascending-bin-number sorts — the Summary panel's bin section and the summary
55
+ * report's two bin tables (per-wafer and lot) — none of which agreed with this
56
+ * module's own `buildBinParetoData`, which has always been count-descending. The
57
+ * effect was that the same wafer's bins came out in one order in the Insights bin
58
+ * chart and a different one in the panel and the report.
59
+ *
60
+ * Pass bins are pinned to the top rather than competing on count: they are the
61
+ * reference the failures are read against, not themselves a failure mode.
62
+ */
63
+ export declare function sortBinsForDisplay(entries: Iterable<[bin: number, count: number]>, passBins?: number[]): Array<[bin: number, count: number]>;
49
64
  //# sourceMappingURL=binPareto.d.ts.map
@@ -1 +1 @@
1
- import{isYieldEligibleDie as m}from"../core/dies.js";export function buildBinParetoData(l,a){const s=new Map;let i=0;for(const o of l){const r=a==="hbin"?o.hardBinCounts:o.softBinCounts;if(r){for(const[c,t]of Object.entries(r)){const n=Number(c);i+=t,s.set(n,(s.get(n)??0)+t)}continue}for(const c of o.dies??[]){if(!m(c))continue;const t=a==="hbin"?c.hbin:c.sbin;t!==void 0&&(i++,s.set(t,(s.get(t)??0)+1))}}const f=Array.from(s.entries()).map(([o,r])=>({label:`Bin ${o}`,value:r,percent:i>0?r/i*100:0,itemCount:1,binCode:o}));return f.sort((o,r)=>r.value-o.value),f}export function buildBinClusterData(l,a){const s=l.map(t=>t.key),i=new Map,f=[];function o(t,n,u){let e=i.get(t);e||(e={total:0,counts:[]},i.set(t,e),f.push(t)),e.total+=u,e.counts[n]=(e.counts[n]??0)+u}l.forEach((t,n)=>{for(const u of t.items){const e=a==="hbin"?u.hardBinCounts:u.softBinCounts;if(e){for(const[d,b]of Object.entries(e))o(Number(d),n,b);continue}for(const d of u.dies??[]){if(!m(d))continue;const b=a==="hbin"?d.hbin:d.sbin;b!==void 0&&o(b,n,1)}}});const c=f.slice().sort((t,n)=>i.get(n).total-i.get(t).total).map(t=>{const n=i.get(t);return{binCode:t,label:`Bin ${t}`,total:n.total,counts:s.map((u,e)=>n.counts[e]??0)}});return{groups:s,bins:c}}
1
+ import{isYieldEligibleDie as p}from"../core/dies.js";export function buildBinParetoData(l,u){const s=new Map;let n=0;for(const o of l){const i=u==="hbin"?o.hardBinCounts:o.softBinCounts;if(i){for(const[a,t]of Object.entries(i)){const e=Number(a);n+=t,s.set(e,(s.get(e)??0)+t)}continue}for(const a of o.dies??[]){if(!p(a))continue;const t=u==="hbin"?a.hbin:a.sbin;t!==void 0&&(n++,s.set(t,(s.get(t)??0)+1))}}const c=Array.from(s.entries()).map(([o,i])=>({label:`Bin ${o}`,value:i,percent:n>0?i/n*100:0,itemCount:1,binCode:o}));return c.sort((o,i)=>i.value-o.value),c}export function buildBinClusterData(l,u){const s=l.map(t=>t.key),n=new Map,c=[];function o(t,e,f){let r=n.get(t);r||(r={total:0,counts:[]},n.set(t,r),c.push(t)),r.total+=f,r.counts[e]=(r.counts[e]??0)+f}l.forEach((t,e)=>{for(const f of t.items){const r=u==="hbin"?f.hardBinCounts:f.softBinCounts;if(r){for(const[d,b]of Object.entries(r))o(Number(d),e,b);continue}for(const d of f.dies??[]){if(!p(d))continue;const b=u==="hbin"?d.hbin:d.sbin;b!==void 0&&o(b,e,1)}}});const a=c.slice().sort((t,e)=>n.get(e).total-n.get(t).total).map(t=>{const e=n.get(t);return{binCode:t,label:`Bin ${t}`,total:e.total,counts:s.map((f,r)=>e.counts[r]??0)}});return{groups:s,bins:a}}export function sortBinsForDisplay(l,u=[1]){const s=new Set(u);return[...l].sort((n,c)=>{const o=s.has(n[0]),i=s.has(c[0]);return o!==i?o?-1:1:o&&i?n[0]-c[0]:c[1]-n[1]||n[0]-c[0]})}
@@ -1 +1 @@
1
- import{normalCdf as rt}from"./math.js";import{getDieKey as $}from"../core/dies.js";const ft=["E","ENE","NE","NNE","N","NNW","NW","WNW","W","WSW","SW","SSW","S","SSE","SE","ESE"];function ut(n,o){const i=(Math.atan2(o,n)+2*Math.PI)%(2*Math.PI);return ft[Math.round(i/(2*Math.PI)*16)%16]}function lt(n,o,i,d){const g=d!==void 0?Math.abs(d):0;return n<=.01&&(o>=.25||g>=2||i>=.1)?"unusual":n<=.05&&(o>=.15||g>=1||i>=.03)?"notable":"info"}function ht(n,o,i){if(o===0||i<=0)return n>0?0:1;if(i>=1)return n<=o?0:1;const d=o*i,g=Math.sqrt(o*i*(1-i));if(g===0)return n>d?0:1;const x=(n-.5-d)/g;return 1-rt(x)}export function buildClusterFindings(n,o,i){const{passBins:d,significanceLevel:g,minimumEffectSize:x,minimumRelativeEffect:G,minimumSampleSize:I,minimumClusterSize:H,ringCount:J}=i,Q=new Set(d);if(n.length<I)return[];const U=t=>{const c=t.hbin??t.sbin;return c===void 0||!Q.has(c)},Z=i.isFailingDie??U,E=n.filter(Z),y=E.length/n.length,M=n[0],B=M?M.width:10,K=M?M.height:10,F=Math.max(B,K)*1.5,R=new Map;for(const t of E)R.set($(t),t);const P=new Map;for(const t of n)P.set($(t),t);const A=Math.ceil(F/B),k=Math.ceil(F/K),b=new Set,V=[];for(const t of E){const c=$(t);if(b.has(c))continue;const r=[],u=[t];for(b.add(c);u.length>0;){const s=u.pop();r.push(s);for(let l=-1;l<=1;l++)for(let h=-1;h<=1;h++){if(h===0&&l===0)continue;const f=`${s.x+h},${s.y+l}`;if(b.has(f))continue;const p=R.get(f);p&&(b.add(f),u.push(p))}}V.push(r)}const L=[],v=o.center.x,Y=o.center.y,tt=o.radius;for(const t of V){if(t.length<H)continue;const c=new Set(t.map(e=>$(e))),r=new Set(c);for(const e of t)for(let a=-k;a<=k;a++)for(let N=-A;N<=A;N++){const W=`${e.x+N},${e.y+a}`;if(r.has(W))continue;const X=P.get(W);X&&Math.hypot(X.physX-e.physX,X.physY-e.physY)<=F&&r.add(W)}const u=[...r].map(e=>P.get(e)).filter(Boolean),s=t.length,l=u.length;if(l<I)continue;const h=s/l,f=h-y,p=ht(s,l,y),S=y>0?f/y:void 0,et=f>=x||S!==void 0&&S>=G;if(p>g||!et)continue;const q=t.reduce((e,a)=>e+a.physX,0)/s,j=t.reduce((e,a)=>e+a.physY,0)/s;let m=t[0],O=1/0;for(const e of t){const a=Math.hypot(e.physX-q,e.physY-j);a<O&&(O=a,m=e)}const w=t.map(e=>(Math.atan2(e.physY-Y,e.physX-v)+2*Math.PI)%(2*Math.PI));let z=w[0],C=w[0];for(const e of w)z=Math.min(z,e),C=Math.max(C,e);const nt=(C-z)*180/Math.PI,st=1-1/J,D=t.filter(e=>Math.hypot(e.physX-v,e.physY-Y)/tt>st).length>s/2&&nt<120,T=D?"edge-arc":"cluster",_=ut(q-v,j-Y),ot=D?`Edge arc ~${_}`:`Cluster at (${m.x}, ${m.y})`,it=s/n.length,ct=lt(p,f,it,S),at=[...c];L.push({id:`${T}:${m.x},${m.y}`,level:"wafer",severity:ct,variable:{kind:"yield",label:"Yield"},comparison:{family:T,left:ot,right:"Rest of wafer"},effect:{direction:"lower",absoluteDelta:-f,relativeDelta:S!==void 0?-S:void 0,effectSize:f},stats:{method:"binomial",pValue:p,adjustedPValue:p,sampleSizeLeft:s,sampleSizeRight:n.length-s},summary:D?`Edge arc near ${_}: ${s} contiguous failing dies (${(h*100).toFixed(0)}% vs ${(y*100).toFixed(0)}% background)`:`Failure cluster at (${m.x}, ${m.y}): ${s} contiguous failing dies (${(h*100).toFixed(0)}% vs ${(y*100).toFixed(0)}% background)`,highlight:{kind:"dies",dieKeys:at}})}return L.sort((t,c)=>{const r=t.stats.pValue??1,u=c.stats.pValue??1;return r!==u?r-u:Math.abs(c.effect.effectSize??0)-Math.abs(t.effect.effectSize??0)})}
1
+ import{normalCdf as ct}from"./math.js";import{getDieKey as q}from"../core/dies.js";import{findConnectedComponents as at}from"./connectedComponents.js";const rt=["E","ENE","NE","NNE","N","NNW","NW","WNW","W","WSW","SW","SSW","S","SSE","SE","ESE"];function ft(n,s){const i=(Math.atan2(s,n)+2*Math.PI)%(2*Math.PI);return rt[Math.round(i/(2*Math.PI)*16)%16]}function lt(n,s,i,r){const f=r!==void 0?Math.abs(r):0;return n<=.01&&(s>=.25||f>=2||i>=.1)?"unusual":n<=.05&&(s>=.15||f>=1||i>=.03)?"notable":"info"}function ut(n,s,i){if(s===0||i<=0)return n>0?0:1;if(i>=1)return n<=s?0:1;const r=s*i,f=Math.sqrt(s*i*(1-i));if(f===0)return n>r?0:1;const S=(n-.5-r)/f;return 1-ct(S)}export function buildClusterFindings(n,s,i){const{passBins:r,significanceLevel:f,minimumEffectSize:S,minimumRelativeEffect:O,minimumSampleSize:W,minimumClusterSize:T,ringCount:_}=i,G=new Set(r);if(n.length<W)return[];const H=e=>{const a=e.hbin??e.sbin;return a===void 0||!G.has(a)},J=i.isFailingDie??H,X=n.filter(J),u=X.length/n.length,m=n[0],I=m?m.width:10,w=m?m.height:10,M=Math.max(I,w)*1.5,b=new Map;for(const e of n)b.set(q(e),e);const B=Math.ceil(M/I),R=Math.ceil(M/w),Q=at(X),A=[],$=s.center.x,x=s.center.y,U=s.radius;for(const e of Q){if(e.length<T)continue;const a=new Set(e.map(t=>q(t))),h=new Set(a);for(const t of e)for(let c=-R;c<=R;c++)for(let z=-B;z<=B;z++){const D=`${t.x+z},${t.y+c}`;if(h.has(D))continue;const N=b.get(D);N&&Math.hypot(N.physX-t.physX,N.physY-t.physY)<=M&&h.add(D)}const p=[...h].map(t=>b.get(t)).filter(Boolean),o=e.length,E=p.length;if(E<W)continue;const C=o/E,d=C-u,y=ut(o,E,u),g=u>0?d/u:void 0,Z=d>=S||g!==void 0&&g>=O;if(y>f||!Z)continue;const K=e.reduce((t,c)=>t+c.physX,0)/o,V=e.reduce((t,c)=>t+c.physY,0)/o;let l=e[0],k=1/0;for(const t of e){const c=Math.hypot(t.physX-K,t.physY-V);c<k&&(k=c,l=t)}const F=e.map(t=>(Math.atan2(t.physY-x,t.physX-$)+2*Math.PI)%(2*Math.PI));let P=F[0],Y=F[0];for(const t of F)P=Math.min(P,t),Y=Math.max(Y,t);const tt=(Y-P)*180/Math.PI,et=1-1/_,v=e.filter(t=>Math.hypot(t.physX-$,t.physY-x)/U>et).length>o/2&&tt<120,L=v?"edge-arc":"cluster",j=ft(K-$,V-x),nt=v?`Edge arc ~${j}`:`Cluster at (${l.x}, ${l.y})`,st=o/n.length,it=lt(y,d,st,g),ot=[...a];A.push({id:`${L}:${l.x},${l.y}`,level:"wafer",severity:it,variable:{kind:"yield",label:"Yield"},comparison:{family:L,left:nt,right:"Rest of wafer"},effect:{direction:"lower",absoluteDelta:-d,relativeDelta:g!==void 0?-g:void 0,effectSize:d},stats:{method:"binomial",pValue:y,adjustedPValue:y,sampleSizeLeft:o,sampleSizeRight:n.length-o},summary:v?`Edge arc near ${j}: ${o} contiguous failing dies (${(C*100).toFixed(0)}% vs ${(u*100).toFixed(0)}% background)`:`Failure cluster at (${l.x}, ${l.y}): ${o} contiguous failing dies (${(C*100).toFixed(0)}% vs ${(u*100).toFixed(0)}% background)`,highlight:{kind:"dies",dieKeys:ot}})}return A.sort((e,a)=>{const h=e.stats.pValue??1,p=a.stats.pValue??1;return h!==p?h-p:Math.abs(a.effect.effectSize??0)-Math.abs(e.effect.effectSize??0)})}
@@ -0,0 +1,14 @@
1
+ import type { PositionedDie } from '../core/dies.js';
2
+ /**
3
+ * Group `failing` into 8-connected components on the integer die grid.
4
+ *
5
+ * Adjacency is `|dx| <= 1 && |dy| <= 1` on `die.x`/`die.y` — the ORIGINAL
6
+ * prober coordinates, per this library's rule that those are the only
7
+ * coordinates any consumer sees. Dies not in `failing` are never traversed, so
8
+ * a component is a contiguous run of failures and nothing else.
9
+ *
10
+ * Iterative, not recursive: a large contiguous failure region is exactly the
11
+ * case this is for, and it is also exactly the case that blows a call stack.
12
+ */
13
+ export declare function findConnectedComponents(failing: readonly PositionedDie[]): PositionedDie[][];
14
+ //# sourceMappingURL=connectedComponents.d.ts.map
@@ -0,0 +1 @@
1
+ import{getDieKey as y}from"../core/dies.js";export function findConnectedComponents(s){if(s.length===0)return[];const r=new Map;for(const e of s)r.set(y(e),e);const n=new Set,u=[];for(const e of s){const f=y(e);if(n.has(f))continue;const p=[],c=[e];for(n.add(f);c.length>0;){const d=c.pop();p.push(d);for(let t=-1;t<=1;t++)for(let o=-1;o<=1;o++){if(o===0&&t===0)continue;const i=`${d.x+o},${d.y+t}`;if(n.has(i))continue;const h=r.get(i);h&&(n.add(i),c.push(h))}}u.push(p)}return u}
@@ -10,7 +10,33 @@ export interface CorrelationCell {
10
10
  xIndex: number;
11
11
  yIndex: number;
12
12
  r: number | null;
13
+ /**
14
+ * Dies contributing to this pair — i.e. those carrying a finite value for BOTH
15
+ * tests, which is not the same as the population size when tests have different
16
+ * coverage. An `r` without its `n` is not interpretable: |r| = 0.8 over 6 dies
17
+ * and over 6,000 are very different claims, and the panel's "strong pair" count
18
+ * thresholds on |r| alone.
19
+ */
20
+ n: number;
13
21
  }
22
+ /**
23
+ * Pearson r from running sums. The single implementation of the formula —
24
+ * `buildCorrelationMatrix`'s per-pair accumulators and `pearsonOfPairs` (used by
25
+ * the scatter panel to report r for the pair it is displaying) both go through
26
+ * here, so the matrix cell and the scatter card can never disagree about the
27
+ * same pair. Returns null below 3 points or with zero variance in either axis.
28
+ */
29
+ export declare function pearsonFromSums(c: number, sumX: number, sumY: number, sumXX: number, sumYY: number, sumXY: number): number | null;
30
+ /** Pearson r and n for an explicit list of XY pairs — the scatter panel's own
31
+ * displayed points, after any legend filtering. Shares `pearsonFromSums` with
32
+ * the matrix. */
33
+ export declare function pearsonOfPairs(pairs: ArrayLike<{
34
+ x: number;
35
+ y: number;
36
+ }>): {
37
+ r: number | null;
38
+ n: number;
39
+ };
14
40
  export interface CorrelationMatrix {
15
41
  tests: CorrelationTestInfo[];
16
42
  cells: CorrelationCell[];
@@ -1 +1 @@
1
- import{isParametricTest as A}from"../renderer/buildWaferMap.js";function F(i){return i.filter(c=>c.testNumber!==void 0&&A(c)).map(c=>({testNumber:c.testNumber,label:c.name,unit:c.unit}))}export function buildCorrelationMatrix(i,c){const u=F(c);if(u.length<2)return{tests:u,cells:[]};const l=u.length,y=u.map(t=>t.testNumber),f=l*(l-1)/2,b=new Float64Array(f),x=new Float64Array(f),r=new Float64Array(f),I=new Float64Array(f),p=new Float64Array(f),g=new Float64Array(f);function w(t,e){return t*l-(t*(t+1)>>1)+(e-t-1)}for(const t of i){if(!t.testValues)continue;const e=new Float64Array(l),o=new Uint8Array(l);for(let n=0;n<l;n++){const s=t.testValues[y[n]];s!==void 0&&Number.isFinite(s)&&(e[n]=s,o[n]=1)}for(let n=0;n<l;n++){if(!o[n])continue;const s=e[n];for(let a=n+1;a<l;a++){if(!o[a])continue;const d=e[a],m=w(n,a);b[m]++,x[m]+=s,r[m]+=d,I[m]+=s*s,p[m]+=d*d,g[m]+=s*d}}}function M(t){const e=b[t];if(e<3)return null;const o=x[t]/e,n=r[t]/e,s=g[t]/e-o*n,a=I[t]/e-o*o,d=p[t]/e-n*n,m=Math.sqrt(a*d);return m===0?null:Math.max(-1,Math.min(1,s/m))}const h=[];for(let t=0;t<l;t++)for(let e=0;e<l;e++){if(e===t){h.push({xIndex:e,yIndex:t,r:1});continue}const o=Math.min(e,t),n=Math.max(e,t),s=M(w(o,n));h.push({xIndex:e,yIndex:t,r:s})}return{tests:u,cells:h}}export function filterCorrelationMatrix(i,{threshold:c=.3,minTests:u=6,maxTests:l=20}={}){const y=[];let f=0,b=0,x=null;for(const t of i.cells){if(t.xIndex>=t.yIndex||t.r===null)continue;const e=Math.abs(t.r);y.push({xi:t.xIndex,yi:t.yIndex,absR:e}),(x===null||e>Math.abs(x.r))&&(x={xLabel:i.tests[t.xIndex].label,yLabel:i.tests[t.yIndex].label,r:t.r})}y.sort((t,e)=>e.absR-t.absR);const r=new Set;for(const{xi:t,yi:e,absR:o}of y){const n=o<c;r.size>=l||n&&r.size>=u||(r.add(t),r.size<l&&r.add(e))}if(r.size<Math.min(u,i.tests.length))for(let t=0;t<i.tests.length&&r.size<Math.min(l,u,i.tests.length);t++)r.add(t);const I=(()=>{const t=Array.from(r),e=new Map,o=new Map;for(const{xi:s,yi:a,absR:d}of y)!r.has(s)||!r.has(a)||(e.set(s,(e.get(s)??0)+d),e.set(a,(e.get(a)??0)+d),o.set(s,(o.get(s)??0)+1),o.set(a,(o.get(a)??0)+1));const n=s=>(o.get(s)??0)>0?e.get(s)/o.get(s):0;return t.sort((s,a)=>n(a)-n(s))})(),p=I.map(t=>i.tests[t]),g=new Map(I.map((t,e)=>[t,e])),w=new Set(p.map(t=>t.testNumber)),M=i.cells.filter(t=>w.has(i.tests[t.xIndex].testNumber)&&w.has(i.tests[t.yIndex].testNumber)).map(t=>({xIndex:g.get(t.xIndex),yIndex:g.get(t.yIndex),r:t.r}));let h=0;for(const{xi:t,yi:e,absR:o}of y)r.has(t)&&r.has(e)?o>=.7?f++:o>=.4&&b++:o<c&&h++;return{matrix:{tests:p,cells:M},strongPairs:f,moderatePairs:b,hiddenWeakPairs:h,strongestPair:x}}
1
+ import{isParametricTest as A}from"../renderer/buildWaferMap.js";export function pearsonFromSums(o,l,u,i,y,f){if(o<3)return null;const d=l/o,c=u/o,r=f/o-d*c,m=i/o-d*d,p=y/o-c*c,h=Math.sqrt(m*p);return h===0?null:Math.max(-1,Math.min(1,r/h))}export function pearsonOfPairs(o){let l=0,u=0,i=0,y=0,f=0,d=0;for(let c=0;c<o.length;c++){const{x:r,y:m}=o[c];!Number.isFinite(r)||!Number.isFinite(m)||(l++,u+=r,i+=m,y+=r*r,f+=m*m,d+=r*m)}return{r:pearsonFromSums(l,u,i,y,f,d),n:l}}function N(o){return o.filter(l=>l.testNumber!==void 0&&A(l)).map(l=>({testNumber:l.testNumber,label:l.name,unit:l.unit}))}export function buildCorrelationMatrix(o,l){const u=N(l);if(u.length<2)return{tests:u,cells:[]};const i=u.length,y=u.map(t=>t.testNumber),f=i*(i-1)/2,d=new Float64Array(f),c=new Float64Array(i),r=new Float64Array(f),m=new Float64Array(f),p=new Float64Array(f),h=new Float64Array(f),g=new Float64Array(f);function w(t,n){return t*i-(t*(t+1)>>1)+(n-t-1)}for(const t of o){if(!t.testValues)continue;const n=new Float64Array(i),x=new Uint8Array(i);for(let s=0;s<i;s++){const a=t.testValues[y[s]];a!==void 0&&Number.isFinite(a)&&(n[s]=a,x[s]=1)}for(let s=0;s<i;s++){if(!x[s])continue;c[s]++;const a=n[s];for(let b=s+1;b<i;b++){if(!x[b])continue;const M=n[b],I=w(s,b);d[I]++,r[I]+=a,m[I]+=M,p[I]+=a*a,h[I]+=M*M,g[I]+=a*M}}}const F=t=>pearsonFromSums(d[t],r[t],m[t],p[t],h[t],g[t]),e=[];for(let t=0;t<i;t++)for(let n=0;n<i;n++){if(n===t){e.push({xIndex:n,yIndex:t,r:1,n:c[n]});continue}const x=Math.min(n,t),s=Math.max(n,t),a=w(x,s);e.push({xIndex:n,yIndex:t,r:F(a),n:d[a]})}return{tests:u,cells:e}}export function filterCorrelationMatrix(o,{threshold:l=.3,minTests:u=6,maxTests:i=20}={}){const y=[];let f=0,d=0,c=null;for(const e of o.cells){if(e.xIndex>=e.yIndex||e.r===null)continue;const t=Math.abs(e.r);y.push({xi:e.xIndex,yi:e.yIndex,absR:t}),(c===null||t>Math.abs(c.r))&&(c={xLabel:o.tests[e.xIndex].label,yLabel:o.tests[e.yIndex].label,r:e.r})}y.sort((e,t)=>t.absR-e.absR);const r=new Set;for(const{xi:e,yi:t,absR:n}of y){const x=n<l;r.size>=i||x&&r.size>=u||(r.add(e),r.size<i&&r.add(t))}if(r.size<Math.min(u,o.tests.length))for(let e=0;e<o.tests.length&&r.size<Math.min(i,u,o.tests.length);e++)r.add(e);const m=(()=>{const e=Array.from(r),t=new Map,n=new Map;for(const{xi:s,yi:a,absR:b}of y)!r.has(s)||!r.has(a)||(t.set(s,(t.get(s)??0)+b),t.set(a,(t.get(a)??0)+b),n.set(s,(n.get(s)??0)+1),n.set(a,(n.get(a)??0)+1));const x=s=>(n.get(s)??0)>0?t.get(s)/n.get(s):0;return e.sort((s,a)=>x(a)-x(s))})(),p=m.map(e=>o.tests[e]),h=new Map(m.map((e,t)=>[e,t])),g=new Set(p.map(e=>e.testNumber)),w=o.cells.filter(e=>g.has(o.tests[e.xIndex].testNumber)&&g.has(o.tests[e.yIndex].testNumber)).map(e=>({xIndex:h.get(e.xIndex),yIndex:h.get(e.yIndex),r:e.r,n:e.n}));let F=0;for(const{xi:e,yi:t,absR:n}of y)r.has(e)&&r.has(t)?n>=.7?f++:n>=.4&&d++:n<l&&F++;return{matrix:{tests:p,cells:w},strongPairs:f,moderatePairs:d,hiddenWeakPairs:F,strongestPair:c}}
@@ -1 +1 @@
1
- import{metadataDisplayValue as b}from"../core/metadata.js";import{compareNatural as C}from"../core/utils.js";export const FACET_NONE_VALUE="(none)";export{prettyKey}from"../core/utils.js";export const DEFAULT_FACET_CURATION={split:{label:"Split"},lot:{label:"Lot"},product:{label:"Product"},testProgram:{label:"Program"},temperature:{label:"Temperature"},testDate:{label:"Test date",date:!0},operator:{label:"Operator"},waferId:{label:"Wafer",facet:!1}};function y(e){const t=e.indexOf("T");return t>0?e.slice(0,t):e}export function facetValueOf(e,t,o=DEFAULT_FACET_CURATION){const l=b(e?.[t]);if(l!==void 0)return o[t]?.date?y(l):l}export function commonMetadata(e){if(!e.length)return{};const t=e[0].metadata??{},o={};for(const[l,r]of Object.entries(t))r==null||r===""||e.every(d=>(d.metadata??{})[l]===r)&&(o[l]=r);return o}export function buildFacetTable(e,t={}){const o={...DEFAULT_FACET_CURATION,...t.curation},l=t.facetableOnly??!0,r=new Set;for(const n of e)for(const c of Object.keys(n.metadata??{}))r.add(c);const m=[...Object.keys(o).filter(n=>r.has(n)),...[...r].filter(n=>!(n in o))],p=[];for(const n of m){const c=o[n];if(l&&c&&c.facet===!1)continue;const u=new Map;for(const f of e){const a=facetValueOf(f.metadata,n,o)??FACET_NONE_VALUE,i=u.get(a)??{waferCount:0,dieCount:0};i.waferCount+=1,i.dieCount+=f.dieCount??0,u.set(a,i)}if(u.size===0)continue;const s=Array.from(u,([f,a])=>({value:f,waferCount:a.waferCount,dieCount:a.dieCount}));s.sort((f,a)=>(f.value===FACET_NONE_VALUE?1:0)-(a.value===FACET_NONE_VALUE?1:0)||a.waferCount-f.waferCount||C(f.value,a.value)),p.push({key:n,label:c?.label??n,values:s,splittable:s.length>1})}return p}
1
+ import{metadataDisplayValue as b}from"../core/metadata.js";import{compareNatural as y,prettyKey as C}from"../core/utils.js";export const FACET_NONE_VALUE="(none)";export{prettyKey}from"../core/utils.js";export const DEFAULT_FACET_CURATION={split:{label:"Split"},lot:{label:"Lot"},product:{label:"Product"},testProgram:{label:"Program"},temperature:{label:"Temperature"},testDate:{label:"Test date",date:!0},operator:{label:"Operator"},waferId:{label:"Wafer",facet:!1}};function O(e){const t=e.indexOf("T");return t>0?e.slice(0,t):e}export function facetValueOf(e,t,o=DEFAULT_FACET_CURATION){const l=b(e?.[t]);if(l!==void 0)return o[t]?.date?O(l):l}export function commonMetadata(e){if(!e.length)return{};const t=e[0].metadata??{},o={};for(const[l,r]of Object.entries(t))r==null||r===""||e.every(d=>(d.metadata??{})[l]===r)&&(o[l]=r);return o}export function buildFacetTable(e,t={}){const o={...DEFAULT_FACET_CURATION,...t.curation},l=t.facetableOnly??!0,r=new Set;for(const n of e)for(const c of Object.keys(n.metadata??{}))r.add(c);const m=[...Object.keys(o).filter(n=>r.has(n)),...[...r].filter(n=>!(n in o))],p=[];for(const n of m){const c=o[n];if(l&&c&&c.facet===!1)continue;const u=new Map;for(const f of e){const a=facetValueOf(f.metadata,n,o)??FACET_NONE_VALUE,i=u.get(a)??{waferCount:0,dieCount:0};i.waferCount+=1,i.dieCount+=f.dieCount??0,u.set(a,i)}if(u.size===0)continue;const s=Array.from(u,([f,a])=>({value:f,waferCount:a.waferCount,dieCount:a.dieCount}));s.sort((f,a)=>(f.value===FACET_NONE_VALUE?1:0)-(a.value===FACET_NONE_VALUE?1:0)||a.waferCount-f.waferCount||y(f.value,a.value)),p.push({key:n,label:c?.label??C(n),values:s,splittable:s.length>1})}return p}
@@ -6,4 +6,22 @@ export interface FindingsFilter {
6
6
  level?: StatsLevel | StatsLevel[];
7
7
  }
8
8
  export declare function filterFindings(source: StatsSummary | LotStatsSummary, filter: FindingsFilter): StatsFinding[];
9
+ /**
10
+ * Drop findings another finding has claimed as an exact restatement of itself —
11
+ * a soft-bin twin covering the same dies, or the single pass bin's row against
12
+ * the yield row that says the same thing.
13
+ *
14
+ * The claimer's own label names what it absorbed ("hard bin and soft bin 3 (same
15
+ * dies)"), so listing both prints one fact twice: once merged, once not. The full
16
+ * uncollapsed list stays on `summary.findings` for any host that wants it.
17
+ *
18
+ * Extracted because this rule existed in three places and one of them was wrong:
19
+ * the Summary panel and the findings report both applied it, while the summary
20
+ * report printed every finding — so a wafer with 8 merged twins listed 16 rows,
21
+ * each duplicate contradicting the merge the label had just described.
22
+ */
23
+ export declare function visibleFindings<T extends {
24
+ id: string;
25
+ absorbedIds?: string[];
26
+ }>(findings: T[]): T[];
9
27
  //# sourceMappingURL=filterFindings.d.ts.map
@@ -1 +1 @@
1
- function t(s){return Array.isArray(s)?s:[s]}export function filterFindings(s,n){let e=s.findings;if(n.severity!==void 0){const i=new Set(t(n.severity));e=e.filter(d=>i.has(d.severity))}if(n.kind!==void 0){const i=new Set(t(n.kind));e=e.filter(d=>i.has(d.variable.kind))}if(n.family!==void 0){const i=new Set(t(n.family));e=e.filter(d=>i.has(d.comparison.family))}if(n.level!==void 0){const i=new Set(t(n.level));e=e.filter(d=>i.has(d.level))}return e}
1
+ function s(i){return Array.isArray(i)?i:[i]}export function filterFindings(i,n){let e=i.findings;if(n.severity!==void 0){const t=new Set(s(n.severity));e=e.filter(d=>t.has(d.severity))}if(n.kind!==void 0){const t=new Set(s(n.kind));e=e.filter(d=>t.has(d.variable.kind))}if(n.family!==void 0){const t=new Set(s(n.family));e=e.filter(d=>t.has(d.comparison.family))}if(n.level!==void 0){const t=new Set(s(n.level));e=e.filter(d=>t.has(d.level))}return e}export function visibleFindings(i){const n=new Set(i.flatMap(e=>e.absorbedIds??[]));return i.filter(e=>!n.has(e.id))}