@wafertools/wafermap 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/AGENTS.md +13 -13
  2. package/CHANGELOG.md +334 -1
  3. package/README.md +3 -3
  4. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +2 -12
  5. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  6. package/dist/packages/canvas-adapter/dieList.js +7 -7
  7. package/dist/packages/canvas-adapter/drilldown.js +1 -1
  8. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  9. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  10. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +47 -4
  11. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  12. package/dist/packages/canvas-adapter/summaryPanel.d.ts +22 -3
  13. package/dist/packages/canvas-adapter/summaryPanel.js +4 -3
  14. package/dist/packages/canvas-adapter/toCanvas.d.ts +23 -2
  15. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  16. package/dist/packages/canvas-adapter/toolbar.d.ts +54 -1
  17. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  18. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  19. package/dist/packages/canvas-adapter/userGuideHtml.js +100 -27
  20. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  21. package/dist/packages/canvas-adapter/version.js +1 -1
  22. package/dist/packages/core/aggregates.d.ts +6 -0
  23. package/dist/packages/core/aggregates.js +1 -1
  24. package/dist/packages/core/compact.d.ts +205 -0
  25. package/dist/packages/core/compact.js +3 -0
  26. package/dist/packages/core/dieTable.d.ts +137 -0
  27. package/dist/packages/core/dieTable.js +1 -0
  28. package/dist/packages/core/dies.d.ts +22 -0
  29. package/dist/packages/core/dies.js +1 -1
  30. package/dist/packages/core/passBins.d.ts +6 -0
  31. package/dist/packages/core/passBins.js +1 -1
  32. package/dist/packages/core/utils.d.ts +14 -0
  33. package/dist/packages/core/utils.js +1 -1
  34. package/dist/packages/renderer/axisTicks.d.ts +14 -0
  35. package/dist/packages/renderer/axisTicks.js +1 -1
  36. package/dist/packages/renderer/buildView.d.ts +29 -4
  37. package/dist/packages/renderer/buildView.js +1 -1
  38. package/dist/packages/renderer/buildWaferMap.d.ts +29 -11
  39. package/dist/packages/renderer/buildWaferMap.js +1 -1
  40. package/dist/packages/renderer/columnarInput.d.ts +87 -0
  41. package/dist/packages/renderer/columnarInput.js +1 -0
  42. package/dist/packages/renderer/compactView.d.ts +22 -0
  43. package/dist/packages/renderer/compactView.js +1 -0
  44. package/dist/packages/renderer/derivedTests/apply.d.ts +8 -6
  45. package/dist/packages/renderer/derivedTests/apply.js +1 -1
  46. package/dist/packages/renderer/index.d.ts +1 -0
  47. package/dist/packages/renderer/spec.d.ts +3 -7
  48. package/dist/packages/renderer/spec.js +1 -1
  49. package/dist/packages/renderer/xyIndicator.d.ts +27 -0
  50. package/dist/packages/renderer/xyIndicator.js +1 -0
  51. package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
  52. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  53. package/dist/packages/stats/analyzeWaferMap.d.ts +169 -2
  54. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  55. package/dist/packages/stats/binRows.d.ts +38 -0
  56. package/dist/packages/stats/binRows.js +1 -0
  57. package/dist/packages/stats/boxplot.js +1 -1
  58. package/dist/packages/stats/capability.d.ts +7 -7
  59. package/dist/packages/stats/capability.js +1 -1
  60. package/dist/packages/stats/clusterDetection.js +1 -1
  61. package/dist/packages/stats/connectedComponents.js +1 -1
  62. package/dist/packages/stats/correlation.js +1 -1
  63. package/dist/packages/stats/filterFindings.d.ts +30 -0
  64. package/dist/packages/stats/filterFindings.js +1 -1
  65. package/dist/packages/stats/findingText.d.ts +9 -0
  66. package/dist/packages/stats/findingText.js +2 -0
  67. package/dist/packages/stats/histogram.js +1 -1
  68. package/dist/packages/stats/lotDrift.d.ts +16 -0
  69. package/dist/packages/stats/lotDrift.js +1 -0
  70. package/dist/packages/stats/lotPattern.d.ts +33 -0
  71. package/dist/packages/stats/lotPattern.js +1 -0
  72. package/dist/packages/stats/math.d.ts +73 -6
  73. package/dist/packages/stats/math.js +1 -1
  74. package/dist/packages/stats/normalizeInput.js +1 -1
  75. package/dist/packages/stats/patternClassification.d.ts +34 -3
  76. package/dist/packages/stats/patternClassification.js +1 -1
  77. package/dist/packages/stats/presentation.d.ts +42 -0
  78. package/dist/packages/stats/presentation.js +1 -0
  79. package/dist/packages/stats/regions.d.ts +16 -0
  80. package/dist/packages/stats/regions.js +1 -1
  81. package/dist/packages/stats/renderSummaryReport.d.ts +15 -2
  82. package/dist/packages/stats/renderSummaryReport.js +32 -27
  83. package/dist/packages/stats/reportHtml.d.ts +54 -9
  84. package/dist/packages/stats/reportHtml.js +130 -47
  85. package/dist/packages/stats/scatter.js +1 -1
  86. package/dist/packages/stats/sweep.js +1 -1
  87. package/dist/packages/stats/synthesis.d.ts +66 -0
  88. package/dist/packages/stats/synthesis.js +2 -0
  89. package/dist/packages/stats/testPassRate.js +1 -1
  90. package/dist/packages/stats/trend.js +1 -1
  91. package/dist/packages/stats/types.d.ts +6 -1
  92. package/dist/packages/stats/yieldRows.d.ts +31 -0
  93. package/dist/packages/stats/yieldRows.js +1 -0
  94. package/dist/packages/worker/index.js +1 -1
  95. package/dist/packages/worker/wafermap.worker.d.ts +3 -0
  96. package/dist/packages/worker/wafermap.worker.js +1 -1
  97. package/llms.txt +1 -1
  98. package/package.json +1 -1
  99. package/dist/packages/stats/findingsNarrative.d.ts +0 -3
  100. package/dist/packages/stats/findingsNarrative.js +0 -1
@@ -1 +1 @@
1
- import{createWafer as G}from"../core/wafer.js";import{isYieldEligibleDie as ge,getDieKey as j,hasPosition as _}from"../core/dies.js";import{applyOrientation as Q,transformDies as ye,clipDiesToWafer as be}from"../core/transforms.js";import{generateDies as we}from"../core/dies.js";import{affineRotation as De,affineMirror as xe,affineCompose as Me,affinePoint as ve}from"../core/transforms.js";import{inferWaferFromXY as Se}from"../core/inference/wafer.js";import{resolveGridPitch as Ce}from"../core/inference/pitch.js";import{assignGridIndices as Oe}from"../core/inference/grid.js";import{generateReticleGrid as ke}from"../core/reticle.js";import{applyDerivedTests as Z}from"./derivedTests/apply.js";import{buildView as ee}from"./buildView.js";import{isStdfBin as $e,isStdfCoord as te,isStdfTestNumber as ne,isStdfSite as Ae,STDF_BIN_MAX as Re,STDF_COORD_MAX as se,STDF_TEST_NUM_MAX as We,STDF_SITE_MAX as Ne}from"../core/stdf.js";import{maxOf as H,minOf as ie,modeOf as Ee}from"../core/utils.js";import{aggregateValues as re,aggregateBinCounts as Te}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function Fe(t){const e=(a,r)=>({ringCount:a,ringCountWarning:{code:"analysis-option-corrected",severity:"warning",message:`ringCount=${String(t)} ${r} (using ${a}).`}});if(t===void 0)return{ringCount:4,ringCountWarning:void 0};if(typeof t!="number"||!Number.isFinite(t))return e(4,"is not a finite number");const n=Math.max(1,Math.round(t));return n===t?{ringCount:t,ringCountWarning:void 0}:e(n,"is not a whole number of at least 1")}function Ie(t){const e=Array.isArray(t)?{}:t,n={hbin:0,sbin:0,testValues:0,testPass:0};let a;const r=(d,v)=>{n[d]++,a??=`${d} ${JSON.stringify(v)}`},s={bins:0,positions:0,values:0,sites:0,orientation:0},u=new Set;let o=0;const w=d=>typeof d!="number"?!0:Number.isNaN(d)?(o++,!1):$e(d)?!0:(s.bins++,!1);function h(d){const v=new Set,x=new Set;for(let M=0;M<d.length;M+=M<50?1:100){const g=d[M];if(!(g===null||typeof g!="object")){if(g.testValues&&typeof g.testValues=="object")for(const m of Object.keys(g.testValues))v.add(m);if(g.testPass&&typeof g.testPass=="object")for(const m of Object.keys(g.testPass))x.add(m)}}const O=[...v],y=[...x],b=M=>ne(Number(M));for(const M of new Set([...O,...y]))b(M)||u.add(M);let C;for(let M=0;M<d.length;M++){const g=d[M];if(g===null||typeof g!="object")continue;const m=g;let F;const A=()=>F??={...m};m.hbin!=null&&typeof m.hbin!="number"&&r("hbin",m.hbin),m.sbin!=null&&typeof m.sbin!="number"&&r("sbin",m.sbin),w(m.hbin)||(A().hbin=void 0),w(m.sbin)||(A().sbin=void 0);const N=typeof m.x=="number"&&!te(m.x),$=typeof m.y=="number"&&!te(m.y);if(N||$){s.positions+=(N?1:0)+($?1:0);const k=A();k.x=void 0,k.y=void 0}typeof m.siteNum=="number"&&!Ae(m.siteNum)&&(s.sites++,A().siteNum=void 0);const I=m.testValues;if(I&&typeof I=="object"){let k;for(let R=0;R<O.length;R++){const T=O[R],E=I[T];if(E!=null){if(!b(T)){k??={...I},delete k[T];continue}typeof E!="number"?r("testValues",E):Number.isFinite(E)||(s.values++,k??={...I},delete k[T])}}k&&(A().testValues=k)}const V=m.testPass;if(V&&typeof V=="object"){let k;for(let R=0;R<y.length;R++){const T=y[R],E=V[T];if(E!=null){if(!b(T)){k??={...V},delete k[T];continue}typeof E!="boolean"&&r("testPass",E)}}k&&(A().testPass=k)}F&&((C??=d.slice())[M]=F)}return C}const i={warnings:[]};if(Array.isArray(t))i.results=h(t);else{Array.isArray(e.results)&&(i.results=h(e.results));const d=e.lotStack?.results;if(Array.isArray(d)){const v=d.map(x=>Array.isArray(x)?h(x):void 0);v.some(Boolean)&&(i.stackResults=v.map((x,O)=>x??d[O]))}}if(Array.isArray(e.testDefs)){const d=e.testDefs,v=d.filter(x=>x==null||typeof x.testNumber!="number"||ne(x.testNumber));for(const x of d)v.includes(x)||u.add(String(x.testNumber));v.length!==d.length&&(i.testDefs=v)}const p=e.waferConfig?.orientation;if(typeof p=="number"){const d=Number.isFinite(p)?(p%360+360)%360:NaN;d===0||d===90||d===180||d===270?i.orientation=d:(s.orientation++,i.orientation=0)}const l=(d,v,x=`${v}s`)=>`${d} ${d===1?v:x}`,f=[];if(n.hbin&&f.push(l(n.hbin,"hard bin")),n.sbin&&f.push(l(n.sbin,"soft bin")),n.testValues&&f.push(l(n.testValues,"test value")),n.testPass&&f.push(l(n.testPass,"pass/fail verdict")),f.length){const d=`buildWaferMap received ${f.join(", ")} of the wrong type (for example ${a}). Bins and test values must be numbers and verdicts true/false \u2014 a bin of "1" is not pass bin 1, so those dies count as fails and yield is wrong, and a test value given as text is not plotted or analysed correctly. Convert them with Number() (and verdicts to booleans) and rebuild.`;console.warn(`[wafermap] ${d}`),i.warnings.push({code:"input-values-not-numbers",message:d,severity:"error"})}const D=[];if(s.bins&&D.push(`${l(s.bins,"bin")} (legal: whole numbers 0\u2013${Re}) \u2014 those dies have no bin`),s.positions&&D.push(`${l(s.positions,"coordinate")} (legal: whole numbers \u2212${se} to ${se}) \u2014 those dies have no position`),u.size&&D.push(`${l(u.size,"test number")} (legal: whole numbers 0\u2013${We}) \u2014 those tests are left out`),s.values&&D.push(`${l(s.values,"test value")} that ${s.values===1?"is":"are"} not finite \u2014 left out`),s.sites&&D.push(`${l(s.sites,"site number")} (legal: 0\u2013${Ne}) \u2014 those dies have no site`),s.orientation&&D.push("a waferConfig.orientation other than 0, 90, 180 or 270 \u2014 the map is built at 0"),D.length||o){const d=[];D.length&&d.push(`buildWaferMap received values outside the STDF V4 ranges, treated as missing: ${D.join("; ")}.`),o&&d.push(`${l(o,"bin was","bins were")} NaN and ${o===1?"is":"are"} treated as no bin.`);const v=d.join(" ");console.warn(`[wafermap] ${v}`),i.warnings.push({code:"input-values-outside-stdf",message:v,severity:"warning"})}return i}function oe(t){const e=(h,i)=>h!==null&&typeof h=="object"&&h[i]!==void 0,n=Array.isArray(t)?{}:t,a=[],r=(h,i,p)=>{h&&a.push(`\`${i}\` (now \`${p}\`)`)};r(e(n,"data"),"data","results"),r(e(n,"die"),"die","dieConfig"),r(e(n,"stack"),"stack","lotStack"),r(e(n.dieConfig,"origin"),"dieConfig.origin","dieConfig.coordinateOrigin"),r(e(n.waferConfig,"flat"),"waferConfig.flat","waferConfig.notch"),r(e(n.reticleConfig,"anchor"),"reticleConfig.anchor","reticleConfig.anchorDie"),r(e(n.lotStack,"aggr"),"lotStack.aggr","lotStack.method"),r(Array.isArray(n.testDefs)&&n.testDefs.some(h=>e(h,"index")&&!e(h,"testNumber")),"TestDef.index","TestDef.testNumber");const s=e(n.lotStack,"results")?n.lotStack.results:[],u=(Array.isArray(t)?[t]:[n.results,...Array.isArray(s)?s:[]]).filter(h=>Array.isArray(h));if(r(u.some(h=>h.slice(0,100).some(i=>e(i,"values"))),"values","testValues"),!a.length)return;const o=a.length>1,w=`buildWaferMap ignored ${a.join(", ")}: ${o?"these names were":"that name was"} removed in an earlier release, so what ${o?"they":"it"} described is not on this map. Rename and rebuild.`;return console.warn(`[wafermap] ${w}`),{code:"input-field-removed",message:w,severity:"error"}}function Pe(t){if(t.results!==void 0||t.lotStack!==void 0)throw new Error("buildWaferMap: pass `layout`, `results` or `lotStack` \u2014 only one.");const e=t.waferConfig?.diameter,n=t.dieConfig?.width,a=t.dieConfig?.height;if(!(e>0&&n>0&&a>0))throw new Error("buildWaferMap: `layout` needs waferConfig.diameter and dieConfig.width and height, all positive.");const r=G({diameter:e,notch:t.waferConfig?.notch,orientation:0}),s=be(we(r,{width:n,height:a}),r,{width:n,height:a}).filter(i=>!i.partial),{flipX:u,flipY:o}=ae(t.dieConfig,{type:"center"}),{layout:w,...h}=t;return{...h,results:s.map(i=>({x:u?-i.x:i.x,y:o?-i.y:i.y})),waferConfig:{...t.waferConfig,center:{x:0,y:0}},dieConfig:{...t.dieConfig,coordinateOrigin:{type:"center"}}}}function _e(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");const e=Ie(t);return Array.isArray(t)?{results:e.results??t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],ringCount:4,ringCountWarning:void 0,removedFieldWarning:oe(t),inputValueWarnings:e.warnings,standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,derivedTests:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:e.results??t.results??[],waferOpts:e.orientation===void 0||t.waferConfig===void 0?t.waferConfig:{...t.waferConfig,orientation:e.orientation},dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:e.stackResults&&t.lotStack?{...t.lotStack,results:e.stackResults}:t.lotStack,passBins:t.passBins??[1],...Fe(t.ringCount),removedFieldWarning:oe(t),inputValueWarnings:e.warnings,standardDiameters:t.standardDiameters,testDefs:(e.testDefs??t.testDefs)?.length?e.testDefs??t.testDefs:void 0,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,derivedTests:t.derivedTests?.length?t.derivedTests:void 0,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Ve(t,e){return e?.coordinateOrigin?e.coordinateOrigin:{type:"center"}}function Be(t,e,n){if(e.type==="custom"&&e.offset)return{offsetX:e.offset.x,offsetY:e.offset.y};if(e.type!=="center"){let a=1/0,r=-1/0,s=1/0,u=-1/0;for(const o of t)o.x<a&&(a=o.x),o.x>r&&(r=o.x),o.y<s&&(s=o.y),o.y>u&&(u=o.y);return{offsetX:Math.round((r+a)/2),offsetY:Math.round((u+s)/2)}}return{offsetX:n.offsetX,offsetY:n.offsetY}}function Ye(t,e,n,a,r,s){if(s)return{colMidX:(Math.round(s.x)-e)*a,colMidY:(Math.round(s.y)-n)*r,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let u=1/0,o=-1/0,w=1/0,h=-1/0;for(const i of t){const p=Math.round(i.x)-e,l=Math.round(i.y)-n;p<u&&(u=p),p>o&&(o=p),l<w&&(w=l),l>h&&(h=l)}return{colMidX:(u+o)/2*a,colMidY:(w+h)/2*r,anchored:!1}}function Xe(t,e,n){let a=1/0,r=-1/0,s=1/0,u=-1/0,o=0,w=0;for(const p of t){const l=Math.round(p.x)-e,f=Math.round(p.y)-n;o+=l,w+=f,l<a&&(a=l),l>r&&(r=l),f<s&&(s=f),f>u&&(u=f)}const h=t.length,i=(p,l,f)=>{const D=(f-l)/2;return D<=0?0:Math.abs(p/h-(l+f)/2)/D};return i(o,a,r)>.11||i(w,s,u)>.11}function ae(t,e){let n=t?.xAxisDirection==="left",a=t?.yAxisDirection==="down";return(e.type==="UL"||e.type==="UR")&&(a=!0),(e.type==="LR"||e.type==="UR")&&(n=!0),{flipX:n,flipY:a}}function je(t,e){const{method:n,targetBin:a}=t,r=t.results.map(s=>s.filter(_));if(n==="mean"||n==="median"||n==="stddev"||n==="min"||n==="max"||n==="count"){const s=new Set((e??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),u=new Set;for(const i of r)for(const p of i)if(p.testValues)for(const l of Object.keys(p.testValues)){const f=Number(l);s.has(f)||u.add(f)}if(u.size===0)return re(r,n);const o=[...u],w=o.map(i=>re(r,n,i)),h=new Map;for(let i=0;i<o.length;i++){const p=o[i];for(const l of w[i]){const f=j(l);h.has(f)||h.set(f,{template:l,testValues:{}});const D=h.get(f),d=l.testValues?.[0];d!==void 0&&(D.testValues[p]=d)}}return[...h.values()].map(({template:i,testValues:p})=>({...i,testValues:Object.keys(p).length>0?p:void 0}))}if(n==="countBin"||n==="percent"){if(a===void 0)return[];const s=Te(r,a,"hard");if(n==="countBin")return s;const u=r.length;return s.map(o=>({...o,testValues:{0:u>0?(o.testValues?.[0]??0)/u*100:0}}))}if(n==="mode"){const s=new Map;for(const o of r)for(const w of o){const h=j(w);s.has(h)||s.set(h,[]),s.get(h).push(w)}const u=[];for(const[o,w]of s){const h=o.split(","),i=Number(h[0]),p=Number(h[1]),l=w.map(D=>D.hbin).filter(D=>D!==void 0),f=Ee(l);f!==null&&u.push({x:i,y:p,hbin:f})}return u}return[]}function de(t){const e=t.filter(_),n=e.length,a=e.filter(s=>s.edgeExcluded).length,r=e.filter(s=>dieHasTestData(s)||s.hbin!==void 0||s.sbin!==void 0).length;return{filledDies:r,totalDies:n,edgeExcludedDies:a,unpositionedDies:t.length-e.length,ratio:n>0?r/n:0}}function fe(t,e,n="exclude"){const a=new Set(e),s=t.filter(f=>!f.partial).filter(f=>f.edgeExcluded).length,u=t.filter(f=>f.partial).length;let o=0,w=0,h=!1;for(const f of t){if(!ge(f))continue;const D=f.hbin??f.sbin;D!==void 0&&(h=!0,a.has(D)?o++:w++)}const i=o+w,p=h&&i>0?o/i*100:null;let l=null;if(n==="denominator-only"){const f=i+s;l=h&&f>0?o/f*100:null}return{passDies:o,failDies:w,edgeExcludedDies:s,partialDies:u,totalDies:i,yieldPercent:p,yieldPercentGross:l}}function ce(t,e,n,a,r,s=0,u=0,o=0,w=0,h=0,i=!1,p=!1){if(!t)return[];const l=t.anchorDie??{x:0,y:0},f=ke(e,{width:t.width,height:t.height,diePitchX:a,diePitchY:r,anchorDie:{x:l.x-s,y:l.y-u},gridOrigin:{x:-o,y:-w}}),D=Me(xe(i,p,e.center.x,e.center.y),De(h,e.center.x,e.center.y));return f.filter(d=>{const v=d.width/2,x=d.height/2,O=[[d.x-v,d.y-x],[d.x+v,d.y-x],[d.x+v,d.y+x],[d.x-v,d.y+x]].map(([g,m])=>ve(D,g,m)),y=ie(O.map(g=>g.x)),b=H(O.map(g=>g.x)),C=ie(O.map(g=>g.y)),M=H(O.map(g=>g.y));return n.some(g=>g.physX>=y&&g.physX<b&&g.physY>=C&&g.physY<M)})}function Le(t,e,n){const r=Math.max(0,e.radius-n)**2;return t.map(s=>{const u=s.physX-e.center.x,o=s.physY-e.center.y;return u*u+o*o>r?{...s,edgeExcluded:!0}:s})}const Ke=.2;function ze(t,e,n){const a=new Set(n);function r(y,b){if(e==="last")return!0;if(e==="first")return!1;const C=L(y.hbin),M=L(b.hbin);if(C===void 0||M===void 0)return!1;const g=a.has(C),m=a.has(M);return e==="best"?m!==g?m:M<C:m!==g?g:M>C}const s=(y,b)=>b.supersedes!==void 0||r(y,b),u=y=>{const b=y.partId===void 0?"":String(y.partId).trim();return b===""?void 0:b},o=t.filter(y=>!_(y)),w=new Map;for(const y of o){const b=u(y);b!==void 0&&w.set(b,(w.get(b)??0)+1)}const i=(o.length===0?0:Math.max(0,...w.values())/o.length)<=Ke,p=new Map,l=[],f=new Map;let D=0,d=0;for(const y of t){const b=u(y);let C;if(_(y)?C=`p:${j(y)}`:i&&b!==void 0?C=`i:${b}`:C=`u:${d++}`,y.supersedes==="partId"&&b!==void 0){const g=f.get(b);g!==void 0&&g!==C&&p.has(g)&&p.delete(g)}const M=p.get(C);M===void 0?(p.set(C,{winner:y,count:1}),l.push(C)):(M.count++,C.startsWith("i:")&&D++,s(M.winner,y)&&(M.winner=y)),b!==void 0&&f.set(b,C)}const v=[],x=[...l.filter(y=>y.startsWith("p:")),...l.filter(y=>!y.startsWith("p:"))];for(const y of x){const b=p.get(y);b!==void 0&&v.push(b.count>1?{...b.winner,retestCount:b.count}:b.winner)}const O=D>0?{code:"retests-by-part-id",severity:"info",message:`${D} record${D===1?"":"s"} with no position shared a part ID with an earlier record and ${D===1?"was":"were"} treated as a retest of it.`}:void 0;return{results:v,warning:O}}export function getDieTestValue(t,e){return t.testValues?.[e]}export function getTestPassStatus(t,e,n){const a=t.testPass?.[e];if(a!==void 0)return a;if(n!==void 0&&!isParametricTest(n)){const r=t.testValues?.[e];if(r===0||r===1)return r===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function L(t){return t===void 0||Number.isNaN(t)?void 0:t}function K(t,e){const n={},a=L(e.hbin),r=L(e.sbin);return a!==void 0&&(n.hbin=a),r!==void 0&&(n.sbin=r),e.retestCount!==void 0&&(n.retestCount=e.retestCount),e.siteNum!==void 0&&(n.siteNum=e.siteNum),e.partId!==void 0&&(n.partId=e.partId),e.metadata!==void 0&&(n.metadata=e.metadata),e.testPass!==void 0&&(n.testPass=e.testPass),e.testValues!==void 0&&(n.testValues=e.testValues),{...t,...n}}function le(t){return t.some(dieHasTestData)?"value":"hardBin"}function ue(t,e){return t.map(({code:n,message:a})=>({code:n,message:a,severity:n==="non-standard-diameter"||n==="diameter-exceeds-die-extent"?"warning":"error",confidence:e.wafer.confidence}))}const Ue="do not fit inside the supplied",Ge="Inferred wafer diameter",He="The probed dies reach only",qe=.75,Je=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const Qe=.02;function he(t,e){return e.reduce((n,a)=>Math.abs(a-t)<Math.abs(n-t)?a:n)}function Ze(t,e){if(!e.length)return!0;const n=he(t,e);return Math.abs(n-t)/n<=Qe}function et(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(e=>Number.isFinite(e)&&e>0)}export function buildWaferMap(t){const e=_e(!Array.isArray(t)&&t.layout?Pe(t):t),n=[];if(e.derivedTests){const c=new Set(e.passBins);if(e.lotStackOpts){const S=e.lotStackOpts.results.map(W=>Z(W,e.testDefs,e.derivedTests,c));e.lotStackOpts={...e.lotStackOpts,results:S.map(W=>W.results)},e.testDefs=S[0]?.testDefs??e.testDefs,n.push(...S[0]?.warnings??[])}else{const S=Z(e.results,e.testDefs,e.derivedTests,c);e.results=S.results,e.testDefs=S.testDefs,n.push(...S.warnings)}}const a=e.lotStackOpts?je(e.lotStackOpts,e.testDefs):e.results;for(let c=0;c<Math.min(5,a.length);c++){const S=a[c];if(typeof S.x=="string"||typeof S.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 c of a)if(c.x==null!=(c.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(c.x)}, y=${String(c.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const{results:r,warning:s}=ze(a,e.retestPolicy,e.passBins),u={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}},o=[];if(e.explicitDies){let c=e.explicitDies;const S=r.filter(_),W=r.filter(X=>!_(X));if(S.length>0){const X=new Map(S.map(P=>[j(P),P]));c=c.map(P=>{const J=X.get(j(P));return J?K(P,J):P})}const Y=e.waferOpts?.diameter??300,B=G({diameter:Y,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});B.orientation!==0&&(c=Q(c,B));const q=ce(e.reticleOpts,B,c,1,1,0,0,0,0,B.orientation),pe=e.reticleOpts!==void 0,z=ee(B,c,{reticles:q,showReticle:pe,plotMode:le(r),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),me=W.map((X,P)=>K({id:`unpositioned_${P}`,width:c[0]?.width??1,height:c[0]?.height??1},X)),U=[...c,...me];return{wafer:B,dies:U,view:z,reticleConfig:e.reticleOpts,units:"mm",inference:u,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.inputValueWarnings,...s?[s]:[],...ue(o,u),...n,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:z.plotMode,metadata:z.metadata,isLotStack:!1,dataCoverage:de(U),passBins:e.passBins,ringCount:e.ringCount,yield:fe(U,e.passBins,e.edgeDieYieldMode),reticles:q,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const w=r.filter(_),h=r.filter(c=>!_(c)),i=w.map(c=>({x:c.x,y:c.y})),p=Ce(i,e.dieOpts,e.waferOpts?.diameter);u.diePitch={confidence:p.confidence,units:p.units};const{pitchX:l,pitchY:f}=p,D=p.units,d=Ve(w,e.dieOpts),v=Oe(i);u.grid={confidence:v.confidence};const{offsetX:x,offsetY:O}=Be(i,d,v),{flipX:y,flipY:b}=ae(e.dieOpts,d),{colMidX:C,colMidY:M,anchored:g}=Ye(i,x,O,l,f,e.waferOpts?.center);let m=e.waferOpts?.diameter;const F=i.map(c=>({x:(Math.round(c.x)-x)*l-C,y:(Math.round(c.y)-O)*f-M})),A=H(F.map(({x:c,y:S})=>Math.hypot(Math.abs(c)+l/2,Math.abs(S)+f/2)));if(m===void 0)if(i.length>0)if(D==="mm"){const c=Se(F,{minRadius:A});m=c.diameter,u.wafer={confidence:c.confidence,method:c.method};const S=et(e.standardDiameters);if(!Ze(m,S)){const W=he(m,S);o.push({code:"non-standard-diameter",message:`${Ge} ${m.toFixed(1)} mm is not a standard wafer size (nearest is ${W} mm). The diameter was derived from the die extent, which assumes the probed grid reaches the wafer edge \u2014 a non-standard result usually means it does not, because the outer dies were never probed. Every die is then placed against a wafer that is too small, which moves dies between rings and changes ring and edge findings. Supply waferConfig.diameter.`})}}else m=A*2,u.wafer={confidence:p.confidence*.8,method:"extent"};else m=D==="mm"?300:30,u.wafer={confidence:0,method:"default"};if(!g&&i.length>0&&Xe(i,x,O)&&(u.wafer.method="inferred-partial",o.push({code:"partial-coverage",message:"Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly."})),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&A>0&&i.length>=Je){const c=A/(m/2);c<qe&&o.push({code:"diameter-exceeds-die-extent",message:`${He} ${(c*100).toFixed(0)}% of the ${m} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`})}if(e.waferOpts?.diameter!==void 0){const c=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(c&&A>m/2+1e-9){const S=F.filter(({x:W,y:Y})=>Math.hypot(Math.abs(W)+l/2,Math.abs(Y)+f/2)>m/2+1e-9).length;o.push({code:"geometry-conflict",message:`${S} of ${F.length} probed die positions ${Ue} ${m} mm wafer at the supplied die pitch of ${l} \xD7 ${f} 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 ${(A*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`})}}const N=G({diameter:m,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let $=w.map(c=>{const S=Math.round(c.x)-x,W=Math.round(c.y)-O,Y={id:`${S}_${W}`,x:S,y:W,physX:S*l-C,physY:W*f-M,width:l,height:f,insideWafer:!0,partial:!1};return K(Y,c)});(x!==0||O!==0)&&($=$.map(c=>({...c,x:c.x+x,y:c.y+O,id:`${c.x+x}_${c.y+O}`}))),$=Q($,N),(y||b)&&($=ye($,{flipX:y,flipY:b},N.center));const I=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&($=Le($,N,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>N.radius&&I.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${N.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const V=ce(e.reticleOpts,N,$,l,f,x,O,C,M,N.orientation,y,b),k=e.reticleOpts!==void 0,R=ee(N,$,{reticles:V,showReticle:k,plotMode:le(r),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,dataAxisFlip:{x:y,y:b},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),T=h.map((c,S)=>K({id:`unpositioned_${S}`,width:l,height:f},c)),E=[...$,...T];return{wafer:N,dies:E,view:R,reticleConfig:e.reticleOpts,units:D,inference:u,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.inputValueWarnings,...s?[s]:[],...ue(o,u),...I,...n,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:R.plotMode,metadata:R.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:de(E),passBins:e.passBins,ringCount:e.ringCount,yield:fe(E,e.passBins,e.edgeDieYieldMode),reticles:V,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:R.aggrMethod,lotSize:R.lotSize}}
1
+ import{createWafer as z}from"../core/wafer.js";import{isYieldEligibleDie as ye,getDieKey as X,hasPosition as F}from"../core/dies.js";import{applyOrientation as ee,transformDies as be,clipDiesToWafer as we}from"../core/transforms.js";import{generateDies as xe}from"../core/dies.js";import{dieHasAnyTestData as De,dieLink as G,finaliseDie as j,linkDie as ve,recordedVerdict as Me,recordedVerdictsReader as Se,tableFromRows as Ce,testValue as q,testsPresent as ke,VERDICT_FAIL as Ae,VERDICT_NONE as Oe,VERDICT_PASS as Ne}from"../core/dieTable.js";import{isDieColumns as $e,linkColumns as Re,linkRows as te,relink as Te}from"./columnarInput.js";import{affineRotation as Ee,affineMirror as We,affineCompose as Fe,affinePoint as Ie}from"../core/transforms.js";import{inferWaferFromXY as _e}from"../core/inference/wafer.js";import{resolveGridPitch as Pe}from"../core/inference/pitch.js";import{assignGridIndices as Ve}from"../core/inference/grid.js";import{generateReticleGrid as Be}from"../core/reticle.js";import{applyDerivedTests as Le}from"./derivedTests/apply.js";import{isStdfBin as Ye,isStdfCoord as ne,isStdfTestNumber as ie,isStdfSite as Xe,STDF_BIN_MAX as je,STDF_COORD_MAX as se,STDF_TEST_NUM_MAX as Ke,STDF_SITE_MAX as He}from"../core/stdf.js";import{hasAnyKey as Ue,maxOf as J,minOf as re,modeOf as ze}from"../core/utils.js";import{aggregateValues as oe,aggregateBinCounts as Ge,stackHoldsMeasurements as qe}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}export function isBuiltMap(t){return"wafer"in t&&"dies"in t&&"dataAxisFlip"in t}function Je(t){const e=(i,a)=>({ringCount:i,ringCountWarning:{code:"analysis-option-corrected",severity:"warning",message:`ringCount=${String(t)} ${a} (using ${i}).`}});if(t===void 0)return{ringCount:4,ringCountWarning:void 0};if(typeof t!="number"||!Number.isFinite(t))return e(4,"is not a finite number");const n=Math.max(1,Math.round(t));return n===t?{ringCount:t,ringCountWarning:void 0}:e(n,"is not a whole number of at least 1")}function Qe(t,e){const n=Array.isArray(t)?{}:t,i={hbin:0,sbin:0,siteNum:0,testValues:0,testPass:0};let a;const o=(l,b)=>{i[l]++,a??=`${l} ${JSON.stringify(b)}`},s={bins:0,positions:0,values:0,sites:0,orientation:0},r=new Set;let m=0;if(e){i.testValues+=e.wrongValues,i.testPass+=e.wrongVerdicts,e.example!==void 0&&(a??=e.example),s.values+=e.nonFinite;for(const l of e.badTestNumbers)r.add(l)}const h=l=>l==null?!0:typeof l!="number"?!1:Number.isNaN(l)?(m++,!1):Ye(l)?!0:(s.bins++,!1);function d(l){const b=new Set,w=new Set;for(let O=0;O<l.length;O+=O<50?1:100){const k=l[O];if(!(k===null||typeof k!="object")){if(k.testValues&&typeof k.testValues=="object")for(const M of Object.keys(k.testValues))b.add(M);if(k.testPass&&typeof k.testPass=="object")for(const M of Object.keys(k.testPass))w.add(M)}}const g=[...b],y=[...w],A=O=>ie(Number(O));for(const O of new Set([...g,...y]))A(O)||r.add(O);const R=g.map(A),v=y.map(A),N=g.map(Number),B=y.map(Number);let I;for(let O=0;O<l.length;O++){const k=l[O];if(k===null||typeof k!="object")continue;const M=k;let Y;const _=()=>Y??={...M};M.hbin!=null&&typeof M.hbin!="number"&&o("hbin",M.hbin),M.sbin!=null&&typeof M.sbin!="number"&&o("sbin",M.sbin),h(M.hbin)||(_().hbin=void 0),h(M.sbin)||(_().sbin=void 0);const L=typeof M.x=="number"&&!ne(M.x),f=typeof M.y=="number"&&!ne(M.y);if(L||f){s.positions+=(L?1:0)+(f?1:0);const S=_();S.x=void 0,S.y=void 0}M.siteNum!=null&&typeof M.siteNum!="number"?(o("siteNum",M.siteNum),_().siteNum=void 0):typeof M.siteNum=="number"&&!Xe(M.siteNum)&&(s.sites++,_().siteNum=void 0);const C=M.testValues;if(C&&typeof C=="object"){let S;for(let $=0;$<g.length;$++){const E=g[$],W=C[N[$]];if(W!=null){if(!R[$]){S??={...C},delete S[E];continue}typeof W!="number"?(o("testValues",W),S??={...C},delete S[E]):Number.isFinite(W)||(s.values++,S??={...C},delete S[E])}}S&&(_().testValues=S)}const T=M.testPass;if(T&&typeof T=="object"){let S;for(let $=0;$<y.length;$++){const E=y[$],W=T[B[$]];if(W!=null){if(!v[$]){S??={...T},delete S[E];continue}typeof W!="boolean"&&(o("testPass",W),S??={...T},delete S[E])}}S&&(_().testPass=S)}Y&&((I??=l.slice())[O]=Y)}return I}const c={warnings:[]};if(Array.isArray(t))c.results=d(t);else{Array.isArray(n.results)&&(c.results=d(n.results)),Array.isArray(n.dies)&&(c.dies=d(n.dies));const l=n.lotStack?.results;if(Array.isArray(l)){const b=l.map(w=>Array.isArray(w)?d(w):void 0);b.some(Boolean)&&(c.stackResults=b.map((w,g)=>w??l[g]))}}if(Array.isArray(n.testDefs)){const l=n.testDefs,b=l.filter(w=>w==null||typeof w.testNumber!="number"||ie(w.testNumber));for(const w of l)b.includes(w)||r.add(String(w.testNumber));b.length!==l.length&&(c.testDefs=b)}const p=n.waferConfig?.orientation;if(typeof p=="number"){const l=Number.isFinite(p)?(p%360+360)%360:NaN;l===0||l===90||l===180||l===270?c.orientation=l:(s.orientation++,c.orientation=0)}const u=(l,b,w=`${b}s`)=>`${l} ${l===1?b:w}`,x=[];if(i.hbin&&x.push(u(i.hbin,"hard bin")),i.sbin&&x.push(u(i.sbin,"soft bin")),i.siteNum&&x.push(u(i.siteNum,"site number")),i.testValues&&x.push(u(i.testValues,"test value")),i.testPass&&x.push(u(i.testPass,"pass/fail verdict")),x.length){const l=`buildWaferMap received ${x.join(", ")} of the wrong type (for example ${a}), treated as missing. Bins, sites and test values must be numbers and verdicts true/false \u2014 a bin of "1" is not pass bin 1 \u2014 so those dies have no bin, no site, no value or no verdict, and yield and every chart leave them out. Convert them with Number() (and verdicts to booleans) and rebuild.`;console.warn(`[wafermap] ${l}`),c.warnings.push({code:"input-values-not-numbers",message:l,severity:"error"})}const D=[];if(s.bins&&D.push(`${u(s.bins,"bin")} (legal: whole numbers 0\u2013${je}) \u2014 those dies have no bin`),s.positions&&D.push(`${u(s.positions,"coordinate")} (legal: whole numbers \u2212${se} to ${se}) \u2014 those dies have no position`),r.size&&D.push(`${u(r.size,"test number")} (legal: whole numbers 0\u2013${Ke}) \u2014 those tests are left out`),s.values&&D.push(`${u(s.values,"test value")} that ${s.values===1?"is":"are"} not finite \u2014 left out`),s.sites&&D.push(`${u(s.sites,"site number")} (legal: 0\u2013${He}) \u2014 those dies have no site`),s.orientation&&D.push("a waferConfig.orientation other than 0, 90, 180 or 270 \u2014 the map is built at 0"),D.length||m){const l=[];D.length&&l.push(`buildWaferMap received values outside the STDF V4 ranges, treated as missing: ${D.join("; ")}.`),m&&l.push(`${u(m,"bin was","bins were")} NaN and ${m===1?"is":"are"} treated as no bin.`);const b=l.join(" ");console.warn(`[wafermap] ${b}`),c.warnings.push({code:"input-values-outside-stdf",message:b,severity:"warning"})}return c}function ae(t){const e=(h,d)=>h!==null&&typeof h=="object"&&h[d]!==void 0,n=Array.isArray(t)?{}:t,i=[],a=(h,d,c)=>{h&&i.push(`\`${d}\` (now \`${c}\`)`)};a(e(n,"data"),"data","results"),a(e(n,"die"),"die","dieConfig"),a(e(n,"stack"),"stack","lotStack"),a(e(n.dieConfig,"origin"),"dieConfig.origin","dieConfig.coordinateOrigin"),a(e(n.waferConfig,"flat"),"waferConfig.flat","waferConfig.notch"),a(e(n.reticleConfig,"anchor"),"reticleConfig.anchor","reticleConfig.anchorDie"),a(e(n.lotStack,"aggr"),"lotStack.aggr","lotStack.method"),a(Array.isArray(n.testDefs)&&n.testDefs.some(h=>e(h,"index")&&!e(h,"testNumber")),"TestDef.index","TestDef.testNumber");const o=e(n.lotStack,"results")?n.lotStack.results:[],s=(Array.isArray(t)?[t]:[n.results,...Array.isArray(o)?o:[]]).filter(h=>Array.isArray(h));if(a(s.some(h=>h.slice(0,100).some(d=>e(d,"values"))),"values","testValues"),!i.length)return;const r=i.length>1,m=`buildWaferMap ignored ${i.join(", ")}: ${r?"these names were":"that name was"} removed in an earlier release, so what ${r?"they":"it"} described is not on this map. Rename and rebuild.`;return console.warn(`[wafermap] ${m}`),{code:"input-field-removed",message:m,severity:"error"}}function Ze(t){if(t.results!==void 0||t.lotStack!==void 0)throw new Error("buildWaferMap: pass `layout`, `results` or `lotStack` \u2014 only one.");const e=t.waferConfig?.diameter,n=t.dieConfig?.width,i=t.dieConfig?.height;if(!(e>0&&n>0&&i>0))throw new Error("buildWaferMap: `layout` needs waferConfig.diameter and dieConfig.width and height, all positive.");const a=z({diameter:e,notch:t.waferConfig?.notch,orientation:0}),o=we(xe(a,{width:n,height:i}),a,{width:n,height:i}).filter(d=>!d.partial),{flipX:s,flipY:r}=de(t.dieConfig,{type:"center"}),{layout:m,...h}=t;return{...h,results:o.map(d=>({x:s?-d.x:d.x,y:r?-d.y:d.y})),waferConfig:{...t.waferConfig,center:{x:0,y:0}},dieConfig:{...t.dieConfig,coordinateOrigin:{type:"center"}}}}function et(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.");const e={wrongVerdicts:0,wrongValues:0,nonFinite:0,badTestNumbers:[]},n=!Array.isArray(t)&&$e(t.results)?{...t,results:Re(t.results,e).records}:t,i=Qe(n,e);return Array.isArray(n)?{results:Q(i.results??n),waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],ringCount:4,ringCountWarning:void 0,removedFieldWarning:ae(n),inputValueWarnings:i.warnings,standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,derivedTests:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:Q(i.results??n.results??[]),waferOpts:i.orientation===void 0||n.waferConfig===void 0?n.waferConfig:{...n.waferConfig,orientation:i.orientation},dieOpts:n.dieConfig,explicitDies:i.dies??n.dies,reticleOpts:n.reticleConfig,lotStackOpts:n.lotStack?{...n.lotStack,results:(i.stackResults??n.lotStack.results).map(Q)}:void 0,passBins:n.passBins??[1],...Je(n.ringCount),removedFieldWarning:ae(n),inputValueWarnings:i.warnings,standardDiameters:n.standardDiameters,testDefs:(i.testDefs??n.testDefs)?.length?i.testDefs??n.testDefs:void 0,hbinDefs:n.hbinDefs,sbinDefs:n.sbinDefs,metadataFields:n.metadataFields,derivedTests:n.derivedTests?.length?n.derivedTests:void 0,retestPolicy:n.retestPolicy??"last",edgeDieYieldMode:n.edgeDieYieldMode??"exclude"}}function Q(t){return t.length>0&&G(t[0])!==void 0?t:te(t).records}function tt(t,e){return e?.coordinateOrigin?e.coordinateOrigin:{type:"center"}}function nt(t,e,n){if(e.type==="custom"&&e.offset)return{offsetX:e.offset.x,offsetY:e.offset.y};if(e.type!=="center"){let i=1/0,a=-1/0,o=1/0,s=-1/0;for(const r of t)r.x<i&&(i=r.x),r.x>a&&(a=r.x),r.y<o&&(o=r.y),r.y>s&&(s=r.y);return{offsetX:Math.round((a+i)/2),offsetY:Math.round((s+o)/2)}}return{offsetX:n.offsetX,offsetY:n.offsetY}}function it(t,e,n,i,a,o){if(o)return{colMidX:(Math.round(o.x)-e)*i,colMidY:(Math.round(o.y)-n)*a,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let s=1/0,r=-1/0,m=1/0,h=-1/0;for(const d of t){const c=Math.round(d.x)-e,p=Math.round(d.y)-n;c<s&&(s=c),c>r&&(r=c),p<m&&(m=p),p>h&&(h=p)}return{colMidX:(s+r)/2*i,colMidY:(m+h)/2*a,anchored:!1}}function st(t,e,n){let i=1/0,a=-1/0,o=1/0,s=-1/0,r=0,m=0;for(const c of t){const p=Math.round(c.x)-e,u=Math.round(c.y)-n;r+=p,m+=u,p<i&&(i=p),p>a&&(a=p),u<o&&(o=u),u>s&&(s=u)}const h=t.length,d=(c,p,u)=>{const x=(u-p)/2;return x<=0?0:Math.abs(c/h-(p+u)/2)/x};return d(r,i,a)>.11||d(m,o,s)>.11}function de(t,e){let n=t?.xAxisDirection==="left",i=t?.yAxisDirection==="down";return(e.type==="UL"||e.type==="UR")&&(i=!0),(e.type==="LR"||e.type==="UR")&&(n=!0),{flipX:n,flipY:i}}function rt(t){const e={...t};return delete e.limitLow,delete e.limitHigh,delete e.limitLowInclusive,delete e.limitHighInclusive,delete e.specLow,delete e.specHigh,e}function ot(t,e){const{method:n,targetBin:i}=t,a=t.results.map(o=>o.filter(F));if(n==="mean"||n==="median"||n==="stddev"||n==="min"||n==="max"||n==="count"){const o=new Set((e??[]).filter(d=>!isParametricTest(d)).map(d=>d.testNumber)),s=new Set;for(const d of a)for(const c of ke(d,"values"))o.has(c)||s.add(c);if(s.size===0)return oe(a,n);const r=[...s],m=r.map(d=>oe(a,n,d)),h=new Map;for(let d=0;d<r.length;d++){const c=r[d];for(const p of m[d]){const u=X(p);h.has(u)||h.set(u,{template:p,testValues:{}});const x=h.get(u),D=p.testValues?.[0];D!==void 0&&(x.testValues[c]=D)}}return[...h.values()].map(({template:d,testValues:c})=>({...d,testValues:Ue(c)?c:void 0}))}if(n==="countBin"||n==="percent"){if(i===void 0)return[];const o=Ge(a,i,"hard");if(n==="countBin")return o;const s=a.length;return o.map(r=>({...r,testValues:{0:s>0?(r.testValues?.[0]??0)/s*100:0}}))}if(n==="mode"){const o=new Map;for(const r of a)for(const m of r){const h=X(m);o.has(h)||o.set(h,[]),o.get(h).push(m)}const s=[];for(const[r,m]of o){const h=r.split(","),d=Number(h[0]),c=Number(h[1]),p=m.map(x=>x.hbin).filter(x=>x!==void 0),u=ze(p);u!==null&&s.push({x:d,y:c,hbin:u})}return s}return[]}function fe(t){const e=t.filter(F),n=e.length,i=e.filter(o=>o.edgeExcluded).length,a=e.filter(o=>o.hbin!==void 0||o.sbin!==void 0||dieHasTestData(o)).length;return{filledDies:a,totalDies:n,edgeExcludedDies:i,unpositionedDies:t.length-e.length,ratio:n>0?a/n:0}}function ue(t,e,n="exclude"){const i=new Set(e),o=t.filter(u=>!u.partial).filter(u=>u.edgeExcluded).length,s=t.filter(u=>u.partial).length;let r=0,m=0,h=!1;for(const u of t){if(!ye(u))continue;const x=u.hbin??u.sbin;x!==void 0&&(h=!0,i.has(x)?r++:m++)}const d=r+m,c=h&&d>0?r/d*100:null;let p=null;if(n==="denominator-only"){const u=d+o;p=h&&u>0?r/u*100:null}return{passDies:r,failDies:m,edgeExcludedDies:o,partialDies:s,totalDies:d,yieldPercent:c,yieldPercentGross:p}}function le(t,e,n,i,a,o=0,s=0,r=0,m=0,h=0,d=!1,c=!1){if(!t)return[];const p=t.anchorDie??{x:0,y:0},u=Be(e,{width:t.width,height:t.height,diePitchX:i,diePitchY:a,anchorDie:{x:p.x-o,y:p.y-s},gridOrigin:{x:-r,y:-m}}),x=Fe(We(d,c,e.center.x,e.center.y),Ee(h,e.center.x,e.center.y));return u.filter(D=>{const l=D.width/2,b=D.height/2,w=[[D.x-l,D.y-b],[D.x+l,D.y-b],[D.x+l,D.y+b],[D.x-l,D.y+b]].map(([v,N])=>Ie(x,v,N)),g=re(w.map(v=>v.x)),y=J(w.map(v=>v.x)),A=re(w.map(v=>v.y)),R=J(w.map(v=>v.y));return n.some(v=>v.physX>=g&&v.physX<y&&v.physY>=A&&v.physY<R)})}function at(t,e,n){const a=Math.max(0,e.radius-n)**2;return t.map(o=>{const s=o.physX-e.center.x,r=o.physY-e.center.y;return s*s+r*r>a?{...o,edgeExcluded:!0}:o})}const dt=.2;function ft(t,e,n){const i=new Set(n);function a(g,y){if(e==="last")return!0;if(e==="first")return!1;const A=K(g.hbin),R=K(y.hbin);if(A===void 0||R===void 0)return!1;const v=i.has(A),N=i.has(R);return e==="best"?N!==v?N:R<A:N!==v?v:R>A}const o=(g,y)=>y.supersedes!==void 0||a(g,y),s=g=>{const y=g.partId===void 0?"":String(g.partId).trim();return y===""?void 0:y},r=t.filter(g=>!F(g)),m=new Map;for(const g of r){const y=s(g);y!==void 0&&m.set(y,(m.get(y)??0)+1)}const d=(r.length===0?0:Math.max(0,...m.values())/r.length)<=dt,c=new Map,p=[],u=new Map;let x=0,D=0;for(const g of t){const y=s(g);let A;if(F(g)?A=`p:${X(g)}`:d&&y!==void 0?A=`i:${y}`:A=`u:${D++}`,g.supersedes==="partId"&&y!==void 0){const v=u.get(y);v!==void 0&&v!==A&&c.has(v)&&c.delete(v)}const R=c.get(A);R===void 0?(c.set(A,{winner:g,count:1}),p.push(A)):(R.count++,A.startsWith("i:")&&x++,o(R.winner,g)&&(R.winner=g)),y!==void 0&&u.set(y,A)}const l=[],b=[...p.filter(g=>g.startsWith("p:")),...p.filter(g=>!g.startsWith("p:"))];for(const g of b){const y=c.get(g);y!==void 0&&l.push(y.count>1?{...y.winner,retestCount:y.count}:y.winner)}const w=x>0?{code:"retests-by-part-id",severity:"info",message:`${x} record${x===1?"":"s"} with no position shared a part ID with an earlier record and ${x===1?"was":"were"} treated as a retest of it.`}:void 0;return{results:l,warning:w}}export function getDieTestValue(t,e){return q(t,e)}export function getTestPassStatus(t,e,n){const i=Me(t,e);return i!==void 0?i:n!==void 0&&!isParametricTest(n)?ce(q(t,e)):void 0}function ce(t){return t===0||t===1?t===1:void 0}export function testPassStatusReader(t){const e=t.map(a=>a.testNumber),n=Se(e),i=t.map(a=>!isParametricTest(a));return function(o,s){n(o,s);for(let r=0;r<s.length;r++){if(s[r]!==Oe||!i[r])continue;const m=ce(q(o,e[r]));m!==void 0&&(s[r]=m?Ne:Ae)}return s}}export function dieHasTestData(t){return De(t)}function K(t){return t===void 0||Number.isNaN(t)?void 0:t}function H(t,e){const n={},i=K(e.hbin),a=K(e.sbin);i!==void 0&&(n.hbin=i),a!==void 0&&(n.sbin=a),e.retestCount!==void 0&&(n.retestCount=e.retestCount),e.siteNum!==void 0&&(n.siteNum=e.siteNum),e.partId!==void 0&&(n.partId=e.partId),e.metadata!==void 0&&(n.metadata=e.metadata);const o=G(e);return ve({...t,...n},o.table,o.row)}function he(t){return t.some(dieHasTestData)?"value":"hardBin"}function pe(t,e){return t.map(({code:n,message:i})=>({code:n,message:i,severity:n==="non-standard-diameter"||n==="diameter-exceeds-die-extent"?"warning":"error",confidence:e.wafer.confidence}))}const ut="do not fit inside the supplied",lt="Inferred wafer diameter",ct="The probed dies reach only",ht=.75,pt=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const mt=.02;function me(t,e){return e.reduce((n,i)=>Math.abs(i-t)<Math.abs(n-t)?i:n)}function gt(t,e){if(!e.length)return!0;const n=me(t,e);return Math.abs(n-t)/n<=mt}function yt(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(e=>Number.isFinite(e)&&e>0)}export function buildWaferMap(t){const e=et(!Array.isArray(t)&&t.layout?Ze(t):t),n=[];if(e.derivedTests){const f=new Set(e.passBins),C=$=>{const E=$.length>0?G($[0]):void 0,W=Le($,E?.table??Ce([]),e.testDefs,e.derivedTests,f);return E&&Te($,W.table),W},S=(e.lotStackOpts?e.lotStackOpts.results:[e.results]).map(C);e.testDefs=S[0]?.testDefs??e.testDefs,n.push(...S[0]?.warnings??[])}e.lotStackOpts&&!qe(e.lotStackOpts.method)&&(e.testDefs=e.testDefs?.map(rt));const i=e.lotStackOpts?te(ot(e.lotStackOpts,e.testDefs)).records:e.results;for(let f=0;f<Math.min(5,i.length);f++){const C=i[f];if(typeof C.x=="string"||typeof C.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const f of i)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{results:a,warning:o}=ft(i,e.retestPolicy,e.passBins),s={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}},r=[];if(e.explicitDies){let f=e.explicitDies.filter(F);const C=e.explicitDies.filter(V=>!F(V)),T=a.filter(F),S=a.filter(V=>!F(V));if(T.length>0){const V=new Map(T.map(P=>[X(P),P]));f=f.map(P=>{const Z=V.get(X(P));return Z?H(P,Z):P})}const $=e.waferOpts?.diameter??300,E=z({diameter:$,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});E.orientation!==0&&(f=ee(f,E)),f=f.map(j);const W=le(e.reticleOpts,E,f,1,1,0,0,0,0,E.orientation),ge=S.map((V,P)=>j(H({id:`unpositioned_${P}`,width:f[0]?.width??1,height:f[0]?.height??1},V))),U=[...f,...C,...ge];return{wafer:E,dies:U,dataAxisFlip:{x:!1,y:!1},reticleConfig:e.reticleOpts,units:"mm",inference:s,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.inputValueWarnings,...o?[o]:[],...pe(r,s),...n,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:he(a),metadata:E.metadata??null,isLotStack:!1,dataCoverage:fe(U),passBins:e.passBins,ringCount:e.ringCount,yield:ue(U,e.passBins,e.edgeDieYieldMode),reticles:W,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const m=a.filter(F),h=a.filter(f=>!F(f)),d=m.map(f=>({x:f.x,y:f.y})),c=Pe(d,e.dieOpts,e.waferOpts?.diameter);s.diePitch={confidence:c.confidence,units:c.units};const{pitchX:p,pitchY:u}=c,x=c.units,D=tt(m,e.dieOpts),l=Ve(d);s.grid={confidence:l.confidence};const{offsetX:b,offsetY:w}=nt(d,D,l),{flipX:g,flipY:y}=de(e.dieOpts,D),{colMidX:A,colMidY:R,anchored:v}=it(d,b,w,p,u,e.waferOpts?.center);let N=e.waferOpts?.diameter;const B=d.map(f=>({x:(Math.round(f.x)-b)*p-A,y:(Math.round(f.y)-w)*u-R})),I=J(B.map(({x:f,y:C})=>Math.hypot(Math.abs(f)+p/2,Math.abs(C)+u/2)));if(N===void 0)if(d.length>0)if(x==="mm"){const f=_e(B,{minRadius:I});N=f.diameter,s.wafer={confidence:f.confidence,method:f.method};const C=yt(e.standardDiameters);if(!gt(N,C)){const T=me(N,C);r.push({code:"non-standard-diameter",message:`${lt} ${N.toFixed(1)} mm is not a standard wafer size (nearest is ${T} 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 N=I*2,s.wafer={confidence:c.confidence*.8,method:"extent"};else N=x==="mm"?300:30,s.wafer={confidence:0,method:"default"};if(!v&&d.length>0&&st(d,b,w)&&(s.wafer.method="inferred-partial",r.push({code:"partial-coverage",message:"Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly."})),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&I>0&&d.length>=pt){const f=I/(N/2);f<ht&&r.push({code:"diameter-exceeds-die-extent",message:`${ct} ${(f*100).toFixed(0)}% of the ${N} 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 f=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(f&&I>N/2+1e-9){const C=B.filter(({x:T,y:S})=>Math.hypot(Math.abs(T)+p/2,Math.abs(S)+u/2)>N/2+1e-9).length;r.push({code:"geometry-conflict",message:`${C} of ${B.length} probed die positions ${ut} ${N} mm wafer at the supplied die pitch of ${p} \xD7 ${u} 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 ${(I*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`})}}const O=z({diameter:N,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let k=m.map(f=>{const C=Math.round(f.x)-b,T=Math.round(f.y)-w,S={id:`${C}_${T}`,x:C,y:T,physX:C*p-A,physY:T*u-R,width:p,height:u,insideWafer:!0,partial:!1};return H(S,f)});(b!==0||w!==0)&&(k=k.map(f=>({...f,x:f.x+b,y:f.y+w,id:`${f.x+b}_${f.y+w}`}))),k=ee(k,O),(g||y)&&(k=be(k,{flipX:g,flipY:y},O.center));const M=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(k=at(k,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&M.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"})),k=k.map(j);const Y=le(e.reticleOpts,O,k,p,u,b,w,A,R,O.orientation,g,y),_=h.map((f,C)=>j(H({id:`unpositioned_${C}`,width:p,height:u},f))),L=[...k,..._];return{wafer:O,dies:L,dataAxisFlip:{x:g,y},reticleConfig:e.reticleOpts,units:x,inference:s,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.inputValueWarnings,...o?[o]:[],...pe(r,s),...M,...n,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:he(a),metadata:O.metadata??null,isLotStack:e.lotStackOpts!==void 0,dataCoverage:fe(L),passBins:e.passBins,ringCount:e.ringCount,yield:ue(L,e.passBins,e.edgeDieYieldMode),reticles:Y,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length}}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The front door: both forms of `results` (rows or columns) become the same
3
+ * internal form here, record shells linked to a value table, before anything
4
+ * else in `buildWaferMap` runs.
5
+ *
6
+ * A shell is a `DieResult` without `testValues`/`testPass`: position, bins,
7
+ * site, part ID, metadata, `supersedes`. It is linked to its row of the table
8
+ * (`linkDie`), so retest resolution, derived tests and the lot stack read its
9
+ * values through `core/dieTable.ts` like any die, and the dies built from the
10
+ * shells are linked to the same rows.
11
+ */
12
+ import type { DieMetadata } from '../core/metadata.js';
13
+ import type { DieTable } from '../core/dieTable.js';
14
+ import type { DieResult } from './buildWaferMap.js';
15
+ /**
16
+ * One wafer's results as columns: one entry per **record**, retests included
17
+ * (wmap resolves retests exactly as for rows). An alternative to `DieResult[]`
18
+ * for a host that already holds columns (a parser, Arrow or Parquet): it builds
19
+ * the same map without an object per die. If you have rows, pass rows.
20
+ *
21
+ * Per-record columns must all have `count` entries. A missing entry is `NaN`,
22
+ * or STDF V4's own missing value in an integer column: −32768 for `x`/`y`,
23
+ * 65535 for `hbin`, `sbin` and `siteNum`. Any other value outside STDF's ranges
24
+ * is treated as missing and reported, as for rows. A die is either fully
25
+ * positioned or fully unpositioned: `x` and `y` must be missing together.
26
+ *
27
+ * Test values and verdicts are **sparse**: per test number, the record indices
28
+ * that have one, and their values. A record a test did not run on is simply
29
+ * not listed, so a missing value can never be mistaken for a reading, and
30
+ * nothing can shift. Indices must be whole numbers in `[0, count)`, each at
31
+ * most once per test, and `indices` and `values` must be the same length:
32
+ * `buildWaferMap` throws otherwise, because a misaligned column would draw a
33
+ * plausible, wrong map. Values are stored as supplied (`Float32Array` stays
34
+ * 32-bit); a non-finite value is treated as missing and reported. Verdicts are
35
+ * `true`/`1` (pass) or `false`/`0` (fail).
36
+ */
37
+ export interface DieColumns {
38
+ /** Number of records. */
39
+ count: number;
40
+ x?: ArrayLike<number>;
41
+ y?: ArrayLike<number>;
42
+ hbin?: ArrayLike<number>;
43
+ sbin?: ArrayLike<number>;
44
+ siteNum?: ArrayLike<number>;
45
+ partId?: ArrayLike<number | string | undefined>;
46
+ supersedes?: ArrayLike<'partId' | 'position' | undefined>;
47
+ metadata?: ArrayLike<DieMetadata | undefined>;
48
+ /** Per test number: the records that have a value, and the values. */
49
+ testValues?: Record<number, {
50
+ indices: ArrayLike<number>;
51
+ values: ArrayLike<number>;
52
+ }>;
53
+ /** Per test number: the records that have a recorded verdict, and the verdicts. */
54
+ testPass?: Record<number, {
55
+ indices: ArrayLike<number>;
56
+ values: ArrayLike<boolean | number>;
57
+ }>;
58
+ }
59
+ /** Findings from the columns' test data, reported with the rest of the input checks. */
60
+ export interface ColumnFindings {
61
+ /** Verdicts that were neither true/false nor 1/0. */
62
+ wrongVerdicts: number;
63
+ /** Values that were not numbers. */
64
+ wrongValues: number;
65
+ example?: string;
66
+ /** Non-finite values. */
67
+ nonFinite: number;
68
+ /** Test numbers STDF cannot store, as given. */
69
+ badTestNumbers: string[];
70
+ }
71
+ /** A wafer as record shells, each linked to its row of `table`. */
72
+ export interface LinkedRecords {
73
+ records: DieResult[];
74
+ table: DieTable;
75
+ }
76
+ export declare function isDieColumns(v: unknown): v is DieColumns;
77
+ /** Rows as shells linked to a table built from their values. The rows are not changed. */
78
+ export declare function linkRows(rows: readonly DieResult[]): LinkedRecords;
79
+ /** Points every record at `table` (same rows): after derived tests extend it. */
80
+ export declare function relink(records: readonly DieResult[], table: DieTable): void;
81
+ /**
82
+ * Columns as shells linked to a table. Structural faults (lengths, indices)
83
+ * throw; value faults are left for the input checks (positions, bins, sites:
84
+ * copied onto the shells as given) or counted in `findings` (test data).
85
+ */
86
+ export declare function linkColumns(cols: DieColumns, findings: ColumnFindings): LinkedRecords;
87
+ //# sourceMappingURL=columnarInput.d.ts.map
@@ -0,0 +1 @@
1
+ import{dieLink as E,linkDie as w,tableFromRows as T,HAS_VALUES as M,HAS_VERDICTS as k,VERDICT_FAIL as V,VERDICT_NONE as x,VERDICT_PASS as _}from"../core/dieTable.js";import{isStdfTestNumber as v}from"../core/stdf.js";export function isDieColumns(t){return t!==null&&typeof t=="object"&&!Array.isArray(t)&&typeof t.count=="number"}export function linkRows(t){const f=T(t);return{records:t.map((m,c)=>{const{testValues:b,testPass:y,...l}=m;return w(l,f,c)}),table:f}}export function relink(t,f){for(const s of t)w(s,f,E(s).row)}const A=-32768,h=65535;export function linkColumns(t,f){const s=t.count;if(!Number.isInteger(s)||s<0)throw new TypeError(`buildWaferMap: columns \`count\` must be a whole number of records, got ${String(s)}.`);const m=["x","y","hbin","sbin","siteNum","partId","supersedes","metadata"];for(const e of m){const n=t[e];if(n!==void 0&&n.length!==s)throw new TypeError(`buildWaferMap: column \`${e}\` has ${n.length} entries for ${s} records. Every per-record column needs one entry per record.`)}const c=new Uint8Array(s),b=new Map,y=new Map,l=new Uint8Array(s),N=(e,n)=>{const a=t[e];if(a!==void 0)for(const r of Object.keys(a)){const{indices:o,values:u}=a[r]??{};if(o===void 0||u===void 0||o.length!==u.length)throw new TypeError(`buildWaferMap: ${e}[${r}] needs \`indices\` and \`values\` of the same length (got ${o?.length??"none"} and ${u?.length??"none"}).`);l.fill(0);for(let i=0;i<o.length;i++){const d=o[i];if(!Number.isInteger(d)||d<0||d>=s)throw new TypeError(`buildWaferMap: ${e}[${r}].indices[${i}] is ${String(d)}, not a record index in [0, ${s}).`);if(l[d])throw new TypeError(`buildWaferMap: ${e}[${r}] lists record ${d} twice.`);l[d]=1}if(!v(Number(r))){f.badTestNumbers.push(r);continue}n(Number(r),o,u)}};N("testValues",(e,n,a)=>{const r=a instanceof Float32Array?new Float32Array(s).fill(NaN):new Float64Array(s).fill(NaN);for(let o=0;o<n.length;o++){const u=n[o],i=a[o];if(c[u]|=M,typeof i!="number"){f.wrongValues++,f.example??=`testValues ${JSON.stringify(i)}`;continue}if(!Number.isFinite(i)){f.nonFinite++;continue}r[u]=i}b.set(e,r)}),N("testPass",(e,n,a)=>{const r=new Int8Array(s).fill(x);for(let o=0;o<n.length;o++){const u=n[o],i=a[o];c[u]|=k,i===!0||i===1?r[u]=_:i===!1||i===0?r[u]=V:(f.wrongVerdicts++,f.example??=`testPass ${JSON.stringify(i)}`)}y.set(e,r)});const $={rows:s,values:new Map([...b].sort((e,n)=>e[0]-n[0])),verdicts:new Map([...y].sort((e,n)=>e[0]-n[0])),fields:c},p=(e,n,a)=>{if(e===void 0)return;const r=e[n];return r===a||Number.isNaN(r)?void 0:r},I=new Array(s);for(let e=0;e<s;e++){const n={},a=p(t.x,e,A),r=p(t.y,e,A);a!==void 0&&(n.x=a),r!==void 0&&(n.y=r);const o=p(t.hbin,e,h),u=p(t.sbin,e,h);o!==void 0&&(n.hbin=o),u!==void 0&&(n.sbin=u);const i=p(t.siteNum,e,h);i!==void 0&&(n.siteNum=i);const d=t.partId?.[e];d!==void 0&&(n.partId=d);const g=t.supersedes?.[e];g!==void 0&&(n.supersedes=g);const S=t.metadata?.[e];S!==void 0&&(n.metadata=S),I[e]=w(n,$,e)}return{records:I,table:$}}
@@ -0,0 +1,22 @@
1
+ import { type CompactMap } from '../core/compact.js';
2
+ import type { View } from './buildView.js';
3
+ /**
4
+ * Lay a built view's dies out on a compact grid.
5
+ *
6
+ * Only positions move. Every die keeps its colour, rectangle size, label and place in
7
+ * `view.dies`, so legends, tallies, hit-testing and statistics, all of which read the same
8
+ * dies, are unchanged. Compact cells are one die pitch apart, so the grid has the uniform
9
+ * pitch hit-testing assumes; their placement goes through the linear part of
10
+ * `view.gridToScreen`, so rotation and flips act on the compact grid exactly as they do on
11
+ * the physical one, about the centre of the grid.
12
+ *
13
+ * The wafer outline, ring and quadrant boundaries and reticle grid describe the physical
14
+ * wafer, which these positions no longer follow, so they are replaced by one outline per
15
+ * group of dies. The notch marker is kept: `notchDir` is untouched and the wafer circle it
16
+ * is measured from is replaced by the grid's own edge. The +X/+Y indicator names the
17
+ * die-grid axes, not the wafer, so it stays, drawn in a margin beside the grid.
18
+ */
19
+ export declare function compactView(view: View, map: CompactMap, options?: {
20
+ showXYIndicator?: boolean;
21
+ }): View;
22
+ //# sourceMappingURL=compactView.d.ts.map
@@ -0,0 +1 @@
1
+ import{compactDies as A}from"../core/compact.js";import{maxOf as W,minOf as D}from"../core/utils.js";import{buildXYIndicator as E}from"./xyIndicator.js";export function compactView(o,s,H={}){const{placements:u}=A(o.dies,s),l=s.columns.length,h=s.rows.length;if(u.length===0||l===0||h===0)return o;const y=o.dies[0].width,d=o.dies[0].height,Y=(l-1)/2,g=(h-1)/2,{a:b,b:w,c:$,d:k}=o.gridToScreen,B=(n,t)=>{const r=(n-Y)*y,x=(t-g)*d;return{x:b*r+$*x,y:w*r+k*x}},f=new Map,O=o.hoverPoints.map((n,t)=>{const r=B(u[t].column,u[t].row);return f.set(`${n.x},${n.y}`,r),r}),P=o.rectangles.map(n=>{const t=f.get(`${n.x},${n.y}`);return t?{...n,x:t.x,y:t.y}:n}),p=[];for(const n of o.texts){if(n.role==="indicator")continue;const t=f.get(`${n.x},${n.y}`);p.push(t?{...n,x:t.x,y:t.y}:n)}const c=(n,t)=>B(n-.5,t-.5),C=s.groups.map(n=>({kind:"compact-group",points:[[c(n.columns[0],n.rows[0]),c(n.columns[1],n.rows[0]),c(n.columns[1],n.rows[1]),c(n.columns[0],n.rows[1])]],closed:!0,lineColor:"#888888",lineWidth:1})),I=[c(0,0),c(l,0),c(l,h),c(0,h)],R=I.map(n=>n.x),S=I.map(n=>n.y),a=(W(R)-D(R))/2,e=(W(S)-D(S))/2,M=Math.abs(b)*y+Math.abs($)*d,X=Math.abs(w)*y+Math.abs(k)*d,m={minX:0,maxX:0,minY:0,maxY:0};if(H.showXYIndicator){const n=Math.max(M,X),t=2*n,{overlays:r,signX:x,signY:j}=E(o.gridToScreen,.15*Math.max(a,e),(q,z)=>({x:q*(a+t),y:z*(e+t)}),p);C.push(...r),m[x<0?"minX":"maxX"]=t+n/2,m[j<0?"minY":"maxY"]=t+n/2}const T={minX:-a-m.minX+M/2,maxX:a+m.maxX-M/2,minY:-e-m.minY+X/2,maxY:e+m.maxY-X/2},i=o.notchDir,V=i?Math.min(Math.abs(i.x)>1e-9?a/Math.abs(i.x):1/0,Math.abs(i.y)>1e-9?e/Math.abs(i.y):1/0):Math.max(a,e);return{...o,rectangles:P,hoverPoints:O,texts:p,overlays:C,hasReticle:!1,waferCenter:{x:0,y:0},waferRadius:V,dieBounds:T,compact:{columns:s.columns,rows:s.rows,columnBreaks:s.columnBreaks,rowBreaks:s.rowBreaks,centreColumn:Y,centreRow:g}}}
@@ -21,6 +21,7 @@
21
21
  * with no further change.
22
22
  */
23
23
  import type { DieResult, TestDef, WaferWarning } from '../buildWaferMap.js';
24
+ import type { DieTable } from '../../core/dieTable.js';
24
25
  /**
25
26
  * A test whose values are computed from other tests on the same die, rather than
26
27
  * measured by the tester.
@@ -71,15 +72,16 @@ export interface DerivedTestDef extends TestDef {
71
72
  readonly derived?: true;
72
73
  }
73
74
  export interface ApplyResult {
74
- results: DieResult[];
75
+ /** The wafer's table with the derived columns added (the input table when nothing was derived). */
76
+ table: DieTable;
75
77
  testDefs: TestDef[] | undefined;
76
78
  warnings: WaferWarning[];
77
79
  }
78
80
  /**
79
- * Apply `derivedTests` to `results`, returning new records that carry the
80
- * derived values and a `testDefs` array extended with the derived defs.
81
- *
82
- * Input records are not mutated — the caller's array is its own.
81
+ * Apply `derivedTests` to a wafer's records: `records[i]` is row `i` of
82
+ * `table` (its bins give `diePass()`). Returns a copy of the table with a
83
+ * column per derived test, and `testDefs` extended with the derived defs.
84
+ * Neither the records nor the table passed in are changed.
83
85
  */
84
- export declare function applyDerivedTests(results: DieResult[], testDefs: TestDef[] | undefined, derivedTests: DerivedTestDef[] | undefined, passBins: ReadonlySet<number>): ApplyResult;
86
+ export declare function applyDerivedTests(records: readonly DieResult[], table: DieTable, testDefs: TestDef[] | undefined, derivedTests: DerivedTestDef[] | undefined, passBins: ReadonlySet<number>): ApplyResult;
85
87
  //# sourceMappingURL=apply.d.ts.map
@@ -1 +1 @@
1
- import{isStdfTestNumber as S,STDF_TEST_NUM_MAX as D}from"../../core/stdf.js";import{diePassStatus as P}from"../../core/dies.js";import{getTestPassStatus as T,isParametricTest as M}from"../buildWaferMap.js";import{classifySpec as V}from"../spec.js";import{parseExpression as k}from"./parser.js";import{evaluate as O}from"./evaluate.js";function p(m){return{code:"derived-test-invalid",message:m,severity:"warning"}}function F(m,l){const w=[],o=n=>{switch(n.k){case"acc":{if(n.kind!=="t")return;const i=l.get(n.test)?.unit;return i?new Set([i]):void 0}case"unary":return n.op==="-"?o(n.arg):void 0;case"binary":{const i=o(n.left),a=o(n.right);if(n.op==="+"||n.op==="-"){if(i&&a){const d=new Set([...i,...a]);return d.size>1&&w.push(`${[...d].join(" and ")} are combined by "${n.op}"`),d}return i??a}return}case"call":{if(n.name==="min"||n.name==="max"||n.name==="mean"||n.name==="sum"){const i=n.args[0];if(i?.k==="vec"&&i.kind==="t"){const a=new Set(i.tests.map(d=>l.get(d)?.unit).filter(d=>!!d));return a.size>1&&w.push(`${[...a].join(" and ")} are combined by "${n.name}"`),a}}return}default:return}};return o(m),w}function j(m,l,w){const o=[],n=new Map((l??[]).map(e=>[e.testNumber,e])),i=e=>`${JSON.stringify(e.name??"")} (test ${e.testNumber})`,a=[],d=new Set;for(const e of m){const t=i(e);if(!S(e.testNumber)){o.push(p(`Derived test ${t} was dropped: \`testNumber\` must be a whole number from 0 to ${D}, as STDF stores it.`));continue}if(w.has(e.testNumber)||n.has(e.testNumber)){o.push(p(`Derived test ${t} was dropped: test number ${e.testNumber} already exists in the data. Measured values are never replaced by a derived test \u2014 give it an unused number.`));continue}if(d.has(e.testNumber)){o.push(p(`Derived test ${t} was dropped: test number ${e.testNumber} is used by an earlier derived test.`));continue}if(typeof e.expression!="string"||e.expression.trim()===""){o.push(p(`Derived test ${t} was dropped: \`expression\` is empty.`));continue}d.add(e.testNumber),a.push(e)}const y=new Map(n);for(const e of a){const{expression:t,constants:u,...c}=e;y.set(e.testNumber,{...c,testType:e.testType??"P"})}const s=new Map,r=new Set;for(const e of a){const t=i(e),u={testDefs:y,constants:e.constants??{}},c=k(e.expression,u);if(!c.ok){o.push(p(`Derived test ${t} was dropped: ${c.message}`)),r.add(e.testNumber);continue}const f=c.type==="boolean",g=!M(e);if(f!==g){o.push(p(f?`Derived test ${t} was dropped: its expression produces a true/false verdict, so it must declare \`testType: 'F'\`.`:`Derived test ${t} was dropped: it declares \`testType: 'F'\` but its expression produces a number. Functional tests carry a verdict, not a measurement.`)),r.add(e.testNumber);continue}if(f&&(e.limitLow!==void 0||e.limitHigh!==void 0)&&o.push(p(`Derived test ${t}: test limits are ignored on a true/false derived test.`)),e.testNumber===void 0)continue;const N=e.testNumber,x=[...new Set(c.reads.map(h=>h.test))].filter(h=>h!==N&&d.has(h));if(c.reads.some(h=>h.test===N)){o.push(p(`Derived test ${t} was dropped: its expression reads itself.`)),r.add(N);continue}for(const h of F(c.ast,y))o.push(p(`Derived test ${t} combines different units \u2014 ${h}. The result is computed as written; set \`unit\` on the derived test to say what it is.`));s.set(N,{raw:e,compiled:{def:{...e,testType:f?"F":"P",derived:!0},ast:c.ast,boolean:f},deps:x})}const b=[],v=new Map,$=(e,t)=>{if(v.get(e)==="done")return!r.has(e);if(v.get(e)==="visiting"){const f=[...t.slice(t.indexOf(e)),e].join(" \u2192 ");for(const g of t.slice(t.indexOf(e)))r.has(g)||(r.add(g),o.push(p(`Derived test ${i(s.get(g).raw)} was dropped: its expression depends on itself through ${f}.`)));return!1}const u=s.get(e);if(u===void 0)return!1;v.set(e,"visiting");let c=!0;for(const f of u.deps)if(!$(f,[...t,e])){if(!r.has(e)){r.add(e);const g=s.get(f)?.raw.name??`test ${f}`;o.push(p(`Derived test ${i(u.raw)} was dropped: it reads ${JSON.stringify(g)} (test ${f}), which was itself dropped.`))}c=!1;break}return v.set(e,"done"),!c||r.has(e)?!1:(b.push(u.compiled),!0)};for(const e of a)$(e.testNumber,[]);return{compiled:b,warnings:o}}export function applyDerivedTests(m,l,w,o){if(w===void 0||w.length===0)return{results:m,testDefs:l,warnings:[]};const n=new Set;for(const s of m){for(const r of Object.keys(s.testValues??{}))n.add(Number(r));for(const r of Object.keys(s.testPass??{}))n.add(Number(r))}const{compiled:i,warnings:a}=j(w,l,n);if(i.length===0)return{results:m,testDefs:l,warnings:a};const d=new Map((l??[]).map(s=>[s.testNumber,s]));for(const s of i)d.set(s.def.testNumber,s.def);return{results:m.map(s=>{let r,b;const v=t=>r?.[t]??s.testValues?.[t],e={value:v,testPass:t=>b?.[t]??T({testValues:r??s.testValues,testPass:b??s.testPass},t,d.get(t)),specPass:t=>{const u=V(v(t),d.get(t));return u===null?void 0:u==="pass"},diePass:()=>P(s,o)};for(const t of i){const u=O(t.ast,e);u!==void 0&&(t.boolean?(b??={...s.testPass},b[t.def.testNumber]=u):(r??={...s.testValues},r[t.def.testNumber]=u))}return r===void 0&&b===void 0?s:{...s,...r!==void 0?{testValues:r}:{},...b!==void 0?{testPass:b}:{}}}),testDefs:[...l??[],...i.map(s=>s.def)],warnings:a}}
1
+ import{isStdfTestNumber as D,STDF_TEST_NUM_MAX as T}from"../../core/stdf.js";import{diePassStatus as M}from"../../core/dies.js";import{getTestPassStatus as V,isParametricTest as _}from"../buildWaferMap.js";import{classifySpec as F}from"../spec.js";import{parseExpression as P}from"./parser.js";import{evaluate as A}from"./evaluate.js";import{extendTable as E,rowView as O,testValue as I,HAS_VALUES as k,HAS_VERDICTS as C,VERDICT_FAIL as L,VERDICT_NONE as S,VERDICT_PASS as R}from"../../core/dieTable.js";function l(N){return{code:"derived-test-invalid",message:N,severity:"warning"}}function j(N,p){const m=[],r=n=>{switch(n.k){case"acc":{if(n.kind!=="t")return;const o=p.get(n.test)?.unit;return o?new Set([o]):void 0}case"unary":return n.op==="-"?r(n.arg):void 0;case"binary":{const o=r(n.left),i=r(n.right);if(n.op==="+"||n.op==="-"){if(o&&i){const a=new Set([...o,...i]);return a.size>1&&m.push(`${[...a].join(" and ")} are combined by "${n.op}"`),a}return o??i}return}case"call":{if(n.name==="min"||n.name==="max"||n.name==="mean"||n.name==="sum"){const o=n.args[0];if(o?.k==="vec"&&o.kind==="t"){const i=new Set(o.tests.map(a=>p.get(a)?.unit).filter(a=>!!a));return i.size>1&&m.push(`${[...i].join(" and ")} are combined by "${n.name}"`),i}}return}default:return}};return r(N),m}function B(N,p,m){const r=[],n=new Map((p??[]).map(e=>[e.testNumber,e])),o=e=>`${JSON.stringify(e.name??"")} (test ${e.testNumber})`,i=[],a=new Set;for(const e of N){const t=o(e);if(!D(e.testNumber)){r.push(l(`Derived test ${t} was dropped: \`testNumber\` must be a whole number from 0 to ${T}, as STDF stores it.`));continue}if(m.has(e.testNumber)||n.has(e.testNumber)){r.push(l(`Derived test ${t} was dropped: test number ${e.testNumber} already exists in the data. Measured values are never replaced by a derived test \u2014 give it an unused number.`));continue}if(a.has(e.testNumber)){r.push(l(`Derived test ${t} was dropped: test number ${e.testNumber} is used by an earlier derived test.`));continue}if(typeof e.expression!="string"||e.expression.trim()===""){r.push(l(`Derived test ${t} was dropped: \`expression\` is empty.`));continue}a.add(e.testNumber),i.push(e)}const v=new Map(n);for(const e of i){const{expression:t,constants:d,...u}=e;v.set(e.testNumber,{...u,testType:e.testType??"P"})}const b=new Map,f=new Set;for(const e of i){const t=o(e),d={testDefs:v,constants:e.constants??{}},u=P(e.expression,d);if(!u.ok){r.push(l(`Derived test ${t} was dropped: ${u.message}`)),f.add(e.testNumber);continue}const s=u.type==="boolean",c=!_(e);if(s!==c){r.push(l(s?`Derived test ${t} was dropped: its expression produces a true/false verdict, so it must declare \`testType: 'F'\`.`:`Derived test ${t} was dropped: it declares \`testType: 'F'\` but its expression produces a number. Functional tests carry a verdict, not a measurement.`)),f.add(e.testNumber);continue}if(s&&(e.limitLow!==void 0||e.limitHigh!==void 0)&&r.push(l(`Derived test ${t}: test limits are ignored on a true/false derived test.`)),e.testNumber===void 0)continue;const $=e.testNumber,x=[...new Set(u.reads.map(h=>h.test))].filter(h=>h!==$&&a.has(h));if(u.reads.some(h=>h.test===$)){r.push(l(`Derived test ${t} was dropped: its expression reads itself.`)),f.add($);continue}for(const h of j(u.ast,v))r.push(l(`Derived test ${t} combines different units \u2014 ${h}. The result is computed as written; set \`unit\` on the derived test to say what it is.`));b.set($,{raw:e,compiled:{def:{...e,testType:s?"F":"P",derived:!0},ast:u.ast,boolean:s},deps:x})}const g=[],w=new Map,y=(e,t)=>{if(w.get(e)==="done")return!f.has(e);if(w.get(e)==="visiting"){const s=[...t.slice(t.indexOf(e)),e].join(" \u2192 ");for(const c of t.slice(t.indexOf(e)))f.has(c)||(f.add(c),r.push(l(`Derived test ${o(b.get(c).raw)} was dropped: its expression depends on itself through ${s}.`)));return!1}const d=b.get(e);if(d===void 0)return!1;w.set(e,"visiting");let u=!0;for(const s of d.deps)if(!y(s,[...t,e])){if(!f.has(e)){f.add(e);const c=b.get(s)?.raw.name??`test ${s}`;r.push(l(`Derived test ${o(d.raw)} was dropped: it reads ${JSON.stringify(c)} (test ${s}), which was itself dropped.`))}u=!1;break}return w.set(e,"done"),!u||f.has(e)?!1:(g.push(d.compiled),!0)};for(const e of i)y(e.testNumber,[]);return{compiled:g,warnings:r}}export function applyDerivedTests(N,p,m,r,n){if(r===void 0||r.length===0)return{table:p,testDefs:m,warnings:[]};const o=new Set;for(const[e,t]of p.values)t.some(d=>!Number.isNaN(d))&&o.add(e);for(const[e,t]of p.verdicts)t.some(d=>d!==S)&&o.add(e);const{compiled:i,warnings:a}=B(r,m,o);if(i.length===0)return{table:p,testDefs:m,warnings:a};const v=new Map((m??[]).map(e=>[e.testNumber,e]));for(const e of i)v.set(e.def.testNumber,e.def);const b=p.rows,f=new Map,g=new Map;for(const e of i)e.boolean?g.set(e.def.testNumber,new Int8Array(b).fill(S)):f.set(e.def.testNumber,new Float64Array(b).fill(NaN));const w=E(p,f,g),y=O(w);for(let e=0;e<b;e++){const t=y(e),d=s=>I(t,s),u={value:d,testPass:s=>V(t,s,v.get(s)),specPass:s=>{const c=F(d(s),v.get(s));return c===null?void 0:c==="pass"},diePass:()=>M(N[e],n)};for(const s of i){const c=A(s.ast,u);c!==void 0&&(s.boolean?(g.get(s.def.testNumber)[e]=c?R:L,w.fields[e]|=C):(f.get(s.def.testNumber)[e]=c,w.fields[e]|=k))}}return{table:w,testDefs:[...m??[],...i.map(e=>e.def)],warnings:a}}
@@ -2,6 +2,7 @@ export { getDieKey } from './buildView.js';
2
2
  export type { PlotMode } from './buildView.js';
3
3
  export { buildWaferMap, getTestPassStatus } from './buildWaferMap.js';
4
4
  export type { DerivedTestDef, DieResult, WaferConfig, DieConfig, ReticleConfig, LotStackConfig, TestDef, BinDef, MetadataFieldDef, WaferMapInputBase, WaferMapInputSingle, WaferMapInputLotStack, WaferMapInputLayout, WaferMapInput, YieldSummary, WaferWarning, WaferMapResult, } from './buildWaferMap.js';
5
+ export type { DieColumns } from './columnarInput.js';
5
6
  export type { WaferMetadata, DieMetadata } from '../core/metadata.js';
6
7
  export { registerValueColorScheme, listValueColorSchemes, resolveValueColorFn, registerBinColorScheme, listBinColorSchemes } from './colorSchemes.js';
7
8
  export type { ValueColorScheme, BinColorScheme } from './colorSchemes.js';
@@ -52,13 +52,9 @@ export interface SpecLimits {
52
52
  export declare function classifySpec(value: number | undefined, limits: SpecLimits | undefined): SpecCategory | null;
53
53
  /** True for a verdict that is outside the limits. */
54
54
  export declare function isOutOfSpec(category: SpecCategory | null): boolean;
55
- /**
56
- * How many values in an ascending, finite array `classifySpec` would judge out of
57
- * spec — the same rule, counted by binary search instead of one call per value,
58
- * for callers that already hold the values sorted. `tests/specLimits.test.mjs`
59
- * holds the two in agreement.
60
- */
61
- export declare function countOutOfSpecSorted(sorted: ArrayLike<number>, limits: SpecLimits): number;
55
+ /** How many of `values` are out of spec: exactly the per-value
56
+ * {@link classifySpec} judgement, counted. Any order. */
57
+ export declare function countOutOfSpec(values: ArrayLike<number>, limits: SpecLimits): number;
62
58
  /** True when `limits` can produce a verdict at all — i.e. at least one limit is
63
59
  * defined. Used to reject `specPass[n]` at parse time rather than silently
64
60
  * yielding no-data for every die. */
@@ -1 +1 @@
1
- export function classifySpec(i,n){return i===void 0||!Number.isFinite(i)||n===void 0||n.limitLow===void 0&&n.limitHigh===void 0?null:n.limitLow!==void 0&&(n.limitLowInclusive===!1?i<=n.limitLow:i<n.limitLow)?"failLow":n.limitHigh!==void 0&&(n.limitHighInclusive===!1?i>=n.limitHigh:i>n.limitHigh)?"failHigh":"pass"}export function isOutOfSpec(i){return i==="failLow"||i==="failHigh"}export function countOutOfSpecSorted(i,n){let u=0,h=0;if(n.limitLow!==void 0){const l=n.limitLow,o=n.limitLowInclusive===!1?e=>e<=l:e=>e<l;let f=0,t=i.length;for(;f<t;){const e=f+t>>1;o(i[e])?f=e+1:t=e}u=f}if(n.limitHigh!==void 0){const l=n.limitHigh,o=n.limitHighInclusive===!1?e=>e<l:e=>e<=l;let f=0,t=i.length;for(;f<t;){const e=f+t>>1;o(i[e])?f=e+1:t=e}h=i.length-Math.max(f,u)}return u+h}export function hasSpecLimits(i){return i!==void 0&&(i.limitLow!==void 0||i.limitHigh!==void 0)}
1
+ export function classifySpec(n,i){return n===void 0||!Number.isFinite(n)||i===void 0||i.limitLow===void 0&&i.limitHigh===void 0?null:i.limitLow!==void 0&&(i.limitLowInclusive===!1?n<=i.limitLow:n<i.limitLow)?"failLow":i.limitHigh!==void 0&&(i.limitHighInclusive===!1?n>=i.limitHigh:n>i.limitHigh)?"failHigh":"pass"}export function isOutOfSpec(n){return n==="failLow"||n==="failHigh"}export function countOutOfSpec(n,i){let f=0;for(let e=0;e<n.length;e++)isOutOfSpec(classifySpec(n[e],i))&&f++;return f}export function hasSpecLimits(n){return n!==void 0&&(n.limitLow!==void 0||n.limitHigh!==void 0)}
@@ -0,0 +1,27 @@
1
+ import { type Affine } from '../core/transforms.js';
2
+ import type { ViewOverlay, ViewText } from './buildView.js';
3
+ interface Point {
4
+ x: number;
5
+ y: number;
6
+ }
7
+ /**
8
+ * The +X/+Y axis arrows, in display space. The ONE place their direction, colours and labels
9
+ * are decided, for the wafer view and the compact view alike; the two differ only in where the
10
+ * arrows sit, which each supplies through `anchorAt`.
11
+ *
12
+ * The arrows name the DIE-GRID axes (+X = increasing `die.x`), so they take `gridToScreen`,
13
+ * including the data-axis flip: a transform that omitted it made the arrows point opposite to
14
+ * the way the die indices run, contradicting the axis tick labels on the same map. As
15
+ * directions rather than positions they go through `affineVector`, which ignores translation.
16
+ *
17
+ * `anchorAt` is given the signs of the corner the arrows point AWAY from, so they extend into
18
+ * the space beside that corner and never clip; the anchor itself is never transformed, so it
19
+ * stays in its corner whatever the rotation or flip.
20
+ */
21
+ export declare function buildXYIndicator(gridToScreen: Affine<'grid', 'screen'>, len: number, anchorAt: (signX: 1 | -1, signY: 1 | -1) => Point, texts: ViewText[]): {
22
+ overlays: ViewOverlay[];
23
+ signX: 1 | -1;
24
+ signY: 1 | -1;
25
+ };
26
+ export {};
27
+ //# sourceMappingURL=xyIndicator.d.ts.map
@@ -0,0 +1 @@
1
+ import{affineVector as l}from"../core/transforms.js";export function buildXYIndicator(r,x,s,a){const o=l(r,x,0),i=l(r,0,x),e=o.x+i.x>=0?-1:1,y=o.y+i.y>=0?-1:1,n=s(e,y),t={x:n.x+o.x,y:n.y+o.y},c={x:n.x+i.x,y:n.y+i.y};return a.push({x:t.x+o.x*.35,y:t.y+o.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:c.x+i.x*.35,y:c.y+i.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),{overlays:[{kind:"xy-indicator",points:[[n,t]],closed:!1,lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",points:[[n,c]],closed:!1,lineColor:"#0044cc",lineWidth:2}],signX:e,signY:y}}
@@ -1,4 +1,32 @@
1
1
  import type { AnalyzeWaferLotInput, AnalyzeWaferMapOptions, LotStatsSummary, StatsSummary } from './types.js';
2
+ export interface WaferOutlier {
3
+ /** Position in the `yields` array passed in. */
4
+ index: number;
5
+ severity: 'notable' | 'unusual';
6
+ /** The test's own statistic: Dixon's Q, or the distance from the median in IQR units. */
7
+ statistic: number;
8
+ }
9
+ /**
10
+ * The one rule for "outlier wafer", over per-wafer yields — the lot findings and
11
+ * the Summary panel's yield bars both read it, so the list and the findings never
12
+ * disagree about a wafer.
13
+ *
14
+ * - 3–7 wafers: Dixon's Q test on the lowest and highest wafer — gap to its
15
+ * nearest neighbour over the range. Beyond the 95% critical value is
16
+ * `notable`, beyond 99% `unusual`.
17
+ * - 8 or more: Tukey's fences — beyond 1.5 × IQR from the quartiles is
18
+ * `notable`, beyond 3 × IQR `unusual`.
19
+ * - Fewer than 3 wafers, or no spread: none — with two wafers there is no
20
+ * telling which is the odd one.
21
+ *
22
+ * Both tests judge a wafer against the wafers' own spread, so either may also
23
+ * flag a trivially small difference in a tight lot; a wafer is an outlier only
24
+ * when it is also at least `OUTLIER_MIN_POINTS` yield points from the median.
25
+ */
26
+ export declare function outlierWafers(yields: readonly number[]): {
27
+ method: 'dixon-q' | 'tukey-fence';
28
+ outliers: WaferOutlier[];
29
+ } | null;
2
30
  export declare function analyzeWaferLot(items: AnalyzeWaferLotInput, options?: AnalyzeWaferMapOptions & {
3
31
  perWaferSummaries?: StatsSummary[];
4
32
  }): LotStatsSummary;
@@ -1 +1 @@
1
- import{analyzeWaferMap as g}from"./analyzeWaferMap.js";import{normalizeInput as h}from"./normalizeInput.js";import{poolCapability as b,poolRegionYield as w,poolPassRates as v}from"./summaryFigures.js";import{mergeTestDefs as I}from"./mergeTestDefs.js";import{median as p}from"../core/utils.js";import{describeWaferPopulation as S,populationStat as k}from"./population.js";const P=1.3,R=2;function x(f,c){const o={info:0,notable:1,unusual:2};return o[f]>=o[c]?f:c}function z(f){const c=new Map;for(const t of f){const i=new Set,l=new Set(t.summary.findings.flatMap(n=>n.absorbedIds??[]));for(const n of t.summary.findings){if(l.has(n.id))continue;const s=[n.variable.kind,n.variable.index??"",n.variable.bin??"",n.comparison.family,n.comparison.left,n.effect.direction].join("|");if(i.has(s))continue;i.add(s);const a=c.get(s)??{finding:n,waferIndices:[],severity:n.severity};a.waferIndices.push(t.waferIndex),a.severity=x(a.severity,n.severity),c.set(s,a)}}const o=[];for(const[t,i]of c){if(i.waferIndices.length<R)continue;const l=i.waferIndices.length/Math.max(1,f.length),n=l>=.6||i.severity==="unusual"?"unusual":"notable",s=i.finding;o.push({id:`lot-repeat:${t}`,level:"lot",severity:n,variable:{...s.variable},comparison:{...s.comparison},effect:{direction:s.effect.direction,absoluteDelta:s.effect.absoluteDelta,relativeDelta:s.effect.relativeDelta,effectSize:s.effect.effectSize},stats:{method:"wafer-finding-frequency",sampleSizeLeft:i.waferIndices.length,sampleSizeRight:f.length-i.waferIndices.length},summary:`${s.summary} \u2014 seen on ${i.waferIndices.length}/${f.length} wafers (${Math.round(l*100)}%)`,highlight:{kind:"wafer",waferIndices:i.waferIndices}})}return o}function $(f,c){const o=k(c,"median"),t=f.map(a=>({waferIndex:a.waferIndex,yieldPercent:a.summary.stats.yieldPercent})).filter(a=>a.yieldPercent!==null);if(t.length<3)return[];const i=t.map(a=>a.yieldPercent),l=p(i),n=p(i.map(a=>Math.abs(a-l)));if(!Number.isFinite(n)||n===0)return[];const s=[];for(const a of t){const u=.6745*(a.yieldPercent-l)/n;if(Math.abs(u)<P)continue;const e=a.yieldPercent-l;s.push({id:`inter-wafer:yield:${a.waferIndex}`,level:"inter-wafer",severity:Math.abs(u)>=2?"unusual":"notable",variable:{kind:"yield",label:"Yield"},comparison:{family:"wafer",left:`Wafer ${a.waferIndex+1}`,right:o.charAt(0).toUpperCase()+o.slice(1)},effect:{direction:e>0?"higher":"lower",absoluteDelta:e,relativeDelta:l===0?void 0:e/l,effectSize:u},stats:{method:"mad-z-score",sampleSizeLeft:1,sampleSizeRight:t.length-1},summary:`Wafer ${a.waferIndex+1} yield is ${Math.abs(e).toFixed(1)} percentage points ${e>0?"higher":"lower"} than the ${o}`,highlight:{kind:"wafer",waferIndices:[a.waferIndex]}})}return s}export function analyzeWaferLot(f,c={}){const o=f.map(h),t=o.map((e,r)=>({waferIndex:r,summary:c.perWaferSummaries?.[r]??g(e,c)})),i=[...z(t),...$(t,S(t.map(e=>e.summary.wafer)))].sort((e,r)=>{const d=e.severity==="unusual"?2:e.severity==="notable"?1:0,y=r.severity==="unusual"?2:r.severity==="notable"?1:0;return d!==y?y-d:e.summary.localeCompare(r.summary,"en")}),l=t.map(e=>e.summary.wafer).filter(e=>!!e),n={},s=[];if(l.length>0){const e=new Set;for(const r of l)for(const d of Object.keys(r))e.add(d);e.delete("wafer"),e.delete("waferId");for(const r of e){const d=l.filter(m=>r in m).map(m=>m[r]);d.every(m=>m===d[0])?n[r]=d[0]:s.push(r)}}const a=t.map(({waferIndex:e,summary:r})=>({waferIndex:e,yieldPercent:r.stats.yieldPercent})),u=t.map(({waferIndex:e,summary:r})=>r.stats.perTestStats?.length?{waferIndex:e,tests:r.stats.perTestStats}:null).filter(e=>e!==null);return{level:"lot",hasNotableFindings:i.some(e=>e.severity!=="info")||t.some(e=>e.summary.hasNotableFindings),findings:i,lot:Object.keys(n).length>0?n:void 0,...s.length>0?{mixedIdentityFields:s}:{},stats:{waferCount:f.length,...M(o,t.map(e=>e.summary))},lotYieldSeries:a,perWafer:t,...u.length>0?{perWaferTestStats:u}:{}}}function M(f,c){const o={...v(c)},t=b(c,I(f).defs);t&&(o.capability=t);const i=w(c,f.map(l=>l.ringCount));return i&&(o.regionYield=i),o}
1
+ import{analyzeWaferMap as H,candidatesOf as U,describeRegional as Q,resolveOptions as se,adjustPValues as ae,twoProportionZ as X,zPValue as W,welchOfSums as Z,rateRelativeEffect as re,passesRateGate as oe,severityForFinding as J,severityForScore as ee,collapseRedundantFindings as ce,findContiguousRuns as le,mergedRegionLabel as fe}from"./analyzeWaferMap.js";import{normalizeInput as de}from"./normalizeInput.js";import{poolCapability as ue,poolRegionYield as me,poolPassRates as ge}from"./summaryFigures.js";import{mergeTestDefs as pe}from"./mergeTestDefs.js";import{findingPatternKey as he}from"./filterFindings.js";import{median as te}from"../core/utils.js";import{robustFence as ne}from"./math.js";import{findLotPattern as ye}from"./lotPattern.js";import{buildDriftFindings as be}from"./lotDrift.js";import{claimForPattern as we,weakestMultiplier as ve}from"./analyzeWaferMap.js";import{PATTERN_LABELS as Se}from"./patternClassification.js";import{describeWaferPopulation as ze,populationStat as Ie}from"./population.js";const T=8,ke={3:[.97,.994],4:[.829,.926],5:[.71,.821],6:[.625,.74],7:[.568,.68]},Me=3;export function outlierWafers(i){const c=i.length;if(c<3)return null;const o=te([...i]),s=e=>Math.abs(e-o)>=Me;if(c>=T){const e=ne(i,1.5,T),f=ne(i,3,T);if(!e||!f)return null;const u=(e.hi-e.lo)/4,a=[];return i.forEach((d,t)=>{if(d>=e.lo&&d<=e.hi||!s(d))return;const r=d<f.lo||d>f.hi?"unusual":"notable";a.push({index:t,severity:r,statistic:(d-o)/u})}),{method:"tukey-fence",outliers:a}}const m=i.map((e,f)=>f).sort((e,f)=>i[e]-i[f]),p=i[m[c-1]]-i[m[0]];if(p===0)return null;const[n,l]=ke[c],g=[],h=(e,f)=>{const u=Math.abs(i[e]-i[f])/p;u>n&&s(i[e])&&g.push({index:e,severity:u>l?"unusual":"notable",statistic:u})};return h(m[0],m[1]),h(m[c-1],m[c-2]),{method:"dixon-q",outliers:g}}const A=2;function _(i,c){const o={info:0,notable:1,unusual:2};return o[i]>=o[c]?i:c}const Pe=new Set(["ring","quadrant","sector"]),Re=new Set(["ring","quadrant","sector","reticle-position","test-site"]),xe=new Set(["yield","hardBin","softBin","functionalTest","test"]);function $e(i){return Re.has(i.comparison.family)&&xe.has(i.variable.kind)}function Be(i,c){const o=i.map(n=>n.facts);if(!o.length||o.some(n=>!n))return{coincident:new Set,yieldSoftBin:void 0,passBins:[],sectorCount:c};const[s,...m]=o,p=m.every(n=>n.passBins.length===s.passBins.length&&n.passBins.every(l=>s.passBins.includes(l)));return{coincident:new Set([...s.coincident].filter(n=>m.every(l=>l.coincident.has(n)))),yieldSoftBin:m.every(n=>n.yieldSoftBin===s.yieldSoftBin)?s.yieldSoftBin:void 0,passBins:p?s.passBins:[],sectorCount:c}}function j(i,c){let o=0,s=0,m=0,p=0,n=0,l=0,g=0,h=0,e=0,f=!0,u=0;for(const{waferIndex:t,c:r}of i){const y=c(t,r),M=r.rate??r.mean,S=Math.sqrt(M.n+y.n);if(r.rate)o+=S*X(r.rate.hits,r.rate.n,y.hits,y.n),m+=r.rate.hits,p+=r.rate.n,n+=y.hits,l+=y.n;else{const P=Z(r.mean,y.n,y.sum,y.sq);o+=S*P.z,g+=r.mean.n*P.effectSize,h+=r.mean.n*P.delta,u+=r.mean.n,r.relativeDelta===void 0?f=!1:e+=r.mean.n*r.relativeDelta}s+=S*S}const a=s>0?o/Math.sqrt(s):0,d=i[0].c;if(d.rate){const t=n/l,r=m/p-t;return{z:a,delta:r,relativeDelta:t===0?void 0:r/t,size:re({hits:m,n:p,restHits:n,restN:l,passRate:d.rate.passRate})}}return{z:a,delta:h/u,relativeDelta:f?e/u:void 0,size:g/u}}function O(i,c,o,s){return i.rate?oe(c,o.delta,o.size,s):c<=s.significanceLevel&&Math.abs(o.size)>=s.minimumEffectSize}function De(i,c,o,s){const m=new Map;for(const{waferIndex:a,byKey:d}of i)for(const t of d.values())(m.get(t.key)??m.set(t.key,[]).get(t.key)).push({waferIndex:a,c:t});const p=new Map(i.map(a=>[a.waferIndex,a.byKey])),n=(a,d)=>d.rate?{hits:d.rate.restHits,n:d.rate.restN,sum:0,sq:0}:{hits:0,n:d.mean.restN,sum:d.mean.restSum,sq:d.mean.restSq},l=[];for(const[a,d]of m){if(d.length<A)continue;const t=j(d,n);if(t.z===0)continue;const r=d[0].c;l.push({key:a,entries:d,combined:t,finding:{id:`lot-region:${a}`,level:"lot",severity:"info",variable:{...r.variable,label:r.label},comparison:{...r.comparison},effect:{direction:t.z>0?"higher":"lower",absoluteDelta:t.delta,relativeDelta:t.relativeDelta,effectSize:r.rate?t.delta:t.size},stats:{method:"stouffer-z",pValue:W(t.z),sampleSizeLeft:0,sampleSizeRight:0},summary:"",highlight:{kind:"wafer",waferIndices:[]}}})}ae(l.map(a=>a.finding));const g=l.filter(a=>O(a.entries[0].c,a.finding.stats.adjustedPValue??1,a.combined,c)),h=(a,d)=>{const t=a.entries[0].c,r=d.entries[0].c;return t.source===r.source&&t.variable.kind===r.variable.kind&&t.variable.bin===r.variable.bin&&t.variable.index===r.variable.index&&t.region.family===r.region.family},e=[],f=new Map;for(const a of g){const d=a.finding.stats.pValue??1,t=g.filter(b=>b!==a&&h(a,b)&&b.finding.effect.direction!==a.finding.effect.direction&&(b.finding.stats.pValue??1)<d);if(t.length){const b=($,v)=>{const z=n($,v);for(const B of t){const I=p.get($)?.get(B.key);I&&(I.rate?(z.hits-=I.rate.hits,z.n-=I.rate.n):(z.n-=I.mean.n,z.sum-=I.mean.sum,z.sq-=I.mean.sq))}return z},k=a.entries.filter($=>b($.waferIndex,$.c).n>=c.minimumSampleSize);if(!k.length)continue;const x=j(k,b),L=d>0?(a.finding.stats.adjustedPValue??d)/d:1;if(Math.sign(x.z)!==Math.sign(a.combined.z)||!O(a.entries[0].c,Math.min(1,W(x.z)*L),x,c))continue}const r=Math.sign(a.combined.z),y=a.entries.filter(b=>Math.sign(b.c.delta)===r),M=i.length,S=a.entries[0].c,P=a.finding.stats.adjustedPValue??d,K=S.rate?J(P,a.combined.delta,a.combined.size):ee(P,a.combined.size),w=_(K,y.length/M>=.6?"unusual":"info"),N={};for(const b of y)N[b.waferIndex]=[...b.c.region.dieKeys];const E={...a.finding,severity:w,stats:{...a.finding.stats,sampleSizeLeft:y.length,sampleSizeRight:M-y.length},summary:`${Q(S,a.combined.delta,a.combined.relativeDelta)} \u2014 ${a.finding.effect.direction} on ${y.length}/${M} wafers, all wafers' data combined`,highlight:{kind:"wafer",waferIndices:y.map(b=>b.waferIndex),dieKeysByWafer:N}};e.push(E),f.set(E,a)}const u=Ee(f,p,i.length,c,s,n);return e.splice(0,e.length,...e.filter(a=>!u.replaced.has(a)),...u.findings),ce(e,o),e}function Ee(i,c,o,s,m,p){const n=new Map;for(const[h,e]of i){const f=e.entries[0].c;if(!Pe.has(f.region.family))continue;const u=[f.source,f.variable.kind,f.variable.bin??"",f.variable.index??"",f.region.family,h.effect.direction].join("|");(n.get(u)??n.set(u,[]).get(u)).push({finding:h,c:f,key:e.key})}const l=[],g=new Set;for(const h of n.values()){if(h.length<2)continue;const e=h[0].c.region.family;for(const f of le(h,e,s.sectorCount,u=>u.c.region.key)){if(f.length<2)continue;const u=Le(f,c,o,s,m,p);if(u){l.push(u);for(const a of f)g.add(a.finding)}}}return{findings:l,replaced:g}}function Le(i,c,o,s,m,p){const n=i[0].c,l=i[0].finding.effect.direction==="higher"?1:-1,g=fe(n.region.family,i.map(w=>w.c.region.key),m),h=[];for(const[w,N]of c){const E=i.map(v=>N.get(v.key));if(E.some(v=>!v))continue;const b=E,k=b[0];let x,L;if(k.rate){const v=b.reduce((R,D)=>R+D.rate.hits,0),z=b.reduce((R,D)=>R+D.rate.n,0),B=k.rate.hits+k.rate.restHits-v,I=k.rate.n+k.rate.restN-z;if(z<s.minimumSampleSize||I<s.minimumSampleSize)continue;x={...k,rate:{hits:v,n:z,restHits:B,restN:I,passRate:k.rate.passRate}},L=X(v,z,B,I)}else{const v=k.mean,z=b.reduce((q,F)=>q+F.mean.n,0),B=b.reduce((q,F)=>q+F.mean.sum,0),I=b.reduce((q,F)=>q+F.mean.sq,0),R=v.n+v.restN-z,D=v.sum+v.restSum-B,C=v.sq+v.restSq-I;if(z<s.minimumSampleSize||R<s.minimumSampleSize)continue;const V={n:z,sum:B,sq:I,restN:R,restSum:D,restSq:C,shift:v.shift},Y=Z(V,R,D,C),G=D/R+v.shift;x={...k,mean:V,relativeDelta:G!==0?Y.delta/Math.abs(G):void 0},L=Y.z}const $=[...new Set(b.flatMap(v=>[...v.region.dieKeys]))];h.push({waferIndex:w,c:x,z:L,dieKeys:$})}if(h.length<A)return;const e=j(h,p);if(e.z===0||Math.sign(e.z)!==l)return;const f=W(e.z),u=ve(i.map(w=>w.finding)),a=Math.min(1,f*u);if(!O(n,a,e,s))return;const d=h.filter(w=>Math.sign(w.z)===l&&W(w.z)<=s.significanceLevel);if(!d.length)return;const t=n.rate?J(a,e.delta,e.size):ee(a,e.size),r=_(t,d.length/o>=.6?"unusual":"info"),y={};for(const w of d)y[w.waferIndex]=w.dieKeys;const M=i.map(w=>w.c.region.key.slice(w.c.region.key.indexOf(":")+1)).join("-"),S={...n,comparison:{...n.comparison,left:g}},P=i[0].finding,K=P.effect.direction;return{...P,id:`lot-region:${i[0].key.split("|").slice(0,-1).join("|")}|${n.region.family}:${M}`,severity:r,comparison:{...P.comparison,left:g},effect:{direction:K,absoluteDelta:e.delta,relativeDelta:e.relativeDelta,effectSize:n.rate?e.delta:e.size},stats:{method:"stouffer-z",pValue:f,adjustedPValue:a,sampleSizeLeft:d.length,sampleSizeRight:o-d.length},summary:`${Q(S,e.delta,e.relativeDelta)} \u2014 ${K} on ${d.length}/${o} wafers, all wafers' data combined`,highlight:{kind:"wafer",waferIndices:d.map(w=>w.waferIndex),dieKeysByWafer:y},relatedIds:i.map(w=>w.finding.id)}}const ie={"Edge-ring":"edge","Edge-local":"edge","Center cluster":"centre or donut",Donut:"centre or donut"};function qe(i,c){const o=new Map,s=n=>{const l=n.variable.kind==="spatialPattern"?ie[n.comparison.left]:void 0;return l?`spatialPattern|${l}`:he(n)};for(const n of i){const l=new Set,g=new Map;for(const e of n.summary.findings)if(e.variable.kind==="spatialPattern")for(const f of e.relatedIds??[])g.set(f,s(e));const h=new Set(n.summary.findings.flatMap(e=>e.absorbedIds??[]));for(const e of n.summary.findings){if(h.has(e.id)||$e(e)||e.variable.kind==="spatialPattern"&&c?.carriers.has(n.waferIndex))continue;const f=s(e);if(l.has(f))continue;l.add(f);const u=o.get(f)??{finding:e,waferIndices:[],severity:e.severity,labels:new Map,dieKeysByWafer:{},explainedBy:new Map};u.waferIndices.push(n.waferIndex);const a=g.get(e.id);if(a&&u.explainedBy.set(a,(u.explainedBy.get(a)??0)+1),e.variable.kind==="spatialPattern"){u.labels.set(e.comparison.left,(u.labels.get(e.comparison.left)??0)+1);const d=e.highlight.dieKeys;d&&(u.dieKeysByWafer[n.waferIndex]=d)}u.severity=_(u.severity,e.severity),o.set(f,u)}}const m=[];for(const[n,l]of o){if(l.waferIndices.length<A)continue;const g=l.waferIndices.length/Math.max(1,i.length),h=g>=.6||l.severity==="unusual"?"unusual":"notable",e=l.finding;m.push({id:`lot-repeat:${n}`,level:"lot",severity:h,variable:{...e.variable},comparison:{...e.comparison},effect:{direction:e.effect.direction,absoluteDelta:e.effect.absoluteDelta,relativeDelta:e.effect.relativeDelta,effectSize:e.effect.effectSize},stats:{method:"wafer-finding-frequency",sampleSizeLeft:l.waferIndices.length,sampleSizeRight:i.length-l.waferIndices.length},summary:`${Ne(e,l.labels)} \u2014 seen on ${l.waferIndices.length}/${i.length} wafers (${Math.round(g*100)}%)`,highlight:{kind:"wafer",waferIndices:l.waferIndices,...l.labels.size?{dieKeysByWafer:l.dieKeysByWafer}:{}}})}const p=new Map(m.map(n=>[n.id,n]));for(const[n,l]of o){const g=p.get(`lot-repeat:${n}`);if(g)for(const[h,e]of l.explainedBy){const f=p.get(`lot-repeat:${h}`);!f||e!==l.waferIndices.length||(f.relatedIds??=[]).push(g.id)}}return m}function Fe(i,c){const{classification:o,carriers:s}=i,m=Se[o.pattern],p=s.size/Math.max(1,c),n={};for(const[l,g]of s)n[l]=g;return{id:"lot-repeat:spatialPattern|lot",level:"lot",severity:p>=.6||o.confidence==="high"?"unusual":"notable",variable:{kind:"spatialPattern",label:m},comparison:{family:"spatial-pattern",left:m,right:"Wafer"},effect:{direction:"different",effectSize:o.features.globalRdd},stats:{method:"wafer-finding-frequency",sampleSizeLeft:s.size,sampleSizeRight:c-s.size},summary:`Spatial pattern: ${m.toLowerCase()} \u2014 seen on ${s.size}/${c} wafers (${Math.round(p*100)}%)`,highlight:{kind:"wafer",waferIndices:[...s.keys()].sort((l,g)=>l-g),dieKeysByWafer:n}}}function Ke(i){const c=/^Rings? (\d+)(?:\s*[–-]\s*(\d+))?/.exec(i.comparison.left);if(!c)return[];const o=+c[1],s=c[2]?+c[2]:o;return Array.from({length:Math.max(0,s-o+1)},(m,p)=>o+p)}function Ne(i,c){if(!c.size)return i.summary;const o=p=>p.toLowerCase();if(c.size===1)return`Spatial pattern: ${o([...c.keys()][0])}`;const s=ie[i.comparison.left],m=[...c].sort((p,n)=>n[1]-p[1]).map(([p,n])=>`${o(p)} on ${n}`);return`Spatial pattern: ${s} (${m.join(", ")})`}function We(i,c){const o=Ie(c,"median"),s=i.map(g=>({waferIndex:g.waferIndex,yieldPercent:g.summary.stats.yieldPercent})).filter(g=>g.yieldPercent!==null),m=s.map(g=>g.yieldPercent),p=outlierWafers(m);if(!p)return[];const n=te(m),l=[];for(const{index:g,severity:h,statistic:e}of p.outliers){const f=s[g],u=f.yieldPercent-n;l.push({id:`inter-wafer:yield:${f.waferIndex}`,level:"inter-wafer",severity:h,variable:{kind:"yield",label:"Yield"},comparison:{family:"wafer",left:`Wafer ${f.waferIndex+1}`,right:o.charAt(0).toUpperCase()+o.slice(1)},effect:{direction:u>0?"higher":"lower",absoluteDelta:u/100,relativeDelta:n===0?void 0:u/n,effectSize:e},stats:{method:p.method,sampleSizeLeft:1,sampleSizeRight:s.length-1},summary:`Wafer ${f.waferIndex+1} yield is ${Math.abs(u).toFixed(1)} percentage points ${u>0?"higher":"lower"} than the ${o}`,highlight:{kind:"wafer",waferIndices:[f.waferIndex]}})}return l}export function analyzeWaferLot(i,c={}){const o=i.map(de),s=o.map((t,r)=>({waferIndex:r,summary:c.perWaferSummaries?.[r]??H(t,c)})),m=s.map(({waferIndex:t,summary:r})=>U(r)??U(H(o[t],c))),p=m.map((t,r)=>({waferIndex:r,byKey:new Map(t.candidates.map(y=>[y.key,y]))})),n=se(c).resolved,l=ye(o,n),g=l&&l.carriers.size>=A?l:null,h=[...De(p,n,Be(m,n.sectorCount),o[0]?.ringCount??n.ringCount),...qe(s,g),...We(s,ze(s.map(t=>t.summary.wafer))),...be(s,n.significanceLevel)];if(g){const t=Fe(g,s.length);t.relatedIds=we([g.classification.pattern],t.severity,h,Ke,n.ringCount),t.relatedIds.length||delete t.relatedIds,h.push(t)}h.sort((t,r)=>{const y=t.severity==="unusual"?2:t.severity==="notable"?1:0,M=r.severity==="unusual"?2:r.severity==="notable"?1:0;return y!==M?M-y:t.summary.localeCompare(r.summary,"en")});const e=s.map(t=>t.summary.wafer).filter(t=>!!t),f={},u=[];if(e.length>0){const t=new Set;for(const r of e)for(const y of Object.keys(r))t.add(y);t.delete("wafer"),t.delete("waferId");for(const r of t){const y=e.filter(S=>r in S).map(S=>S[r]);y.every(S=>S===y[0])?f[r]=y[0]:u.push(r)}}const a=s.map(({waferIndex:t,summary:r})=>({waferIndex:t,yieldPercent:r.stats.yieldPercent})),d=s.map(({waferIndex:t,summary:r})=>r.stats.perTestStats?.length?{waferIndex:t,tests:r.stats.perTestStats}:null).filter(t=>t!==null);return{level:"lot",hasNotableFindings:h.some(t=>t.severity!=="info")||s.some(t=>t.summary.hasNotableFindings),findings:h,lot:Object.keys(f).length>0?f:void 0,...u.length>0?{mixedIdentityFields:u}:{},stats:{waferCount:i.length,...Ae(o,s.map(t=>t.summary))},lotYieldSeries:a,perWafer:s,...d.length>0?{perWaferTestStats:d}:{}}}function Ae(i,c){const o={...ge(c)},s=ue(c,pe(i).defs);s&&(o.capability=s);const m=me(c,i.map(p=>p.ringCount));return m&&(o.regionYield=m),o}