@wafertools/wafermap 0.21.1 → 0.23.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 (50) hide show
  1. package/AGENTS.md +181 -0
  2. package/CHANGELOG.md +261 -1
  3. package/README.md +9 -1
  4. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  6. package/dist/packages/canvas-adapter/dieList.d.ts +41 -0
  7. package/dist/packages/canvas-adapter/dieList.js +2 -0
  8. package/dist/packages/canvas-adapter/icons.js +1 -1
  9. package/dist/packages/canvas-adapter/index.d.ts +4 -0
  10. package/dist/packages/canvas-adapter/index.js +1 -1
  11. package/dist/packages/canvas-adapter/maplessSummary.d.ts +34 -0
  12. package/dist/packages/canvas-adapter/maplessSummary.js +1 -0
  13. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +9 -11
  14. package/dist/packages/canvas-adapter/renderWaferGallery.js +8 -8
  15. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +31 -1
  16. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  17. package/dist/packages/canvas-adapter/summaryPanel.d.ts +13 -1
  18. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  19. package/dist/packages/canvas-adapter/toolbar.d.ts +4 -1
  20. package/dist/packages/canvas-adapter/toolbar.js +1 -1
  21. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  22. package/dist/packages/canvas-adapter/userGuideHtml.js +59 -22
  23. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  24. package/dist/packages/canvas-adapter/version.js +1 -1
  25. package/dist/packages/canvas-adapter/warnings.d.ts +57 -0
  26. package/dist/packages/canvas-adapter/warnings.js +1 -0
  27. package/dist/packages/core/classify.d.ts +6 -2
  28. package/dist/packages/core/dies.d.ts +67 -7
  29. package/dist/packages/core/dies.js +1 -1
  30. package/dist/packages/core/probe.d.ts +5 -0
  31. package/dist/packages/core/probe.js +1 -1
  32. package/dist/packages/core/transforms.d.ts +4 -4
  33. package/dist/packages/core/transforms.js +1 -1
  34. package/dist/packages/renderer/buildView.d.ts +3 -3
  35. package/dist/packages/renderer/buildView.js +1 -1
  36. package/dist/packages/renderer/buildWaferMap.d.ts +40 -9
  37. package/dist/packages/renderer/buildWaferMap.js +1 -1
  38. package/dist/packages/renderer/index.d.ts +1 -1
  39. package/dist/packages/renderer/index.js +1 -1
  40. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  41. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  42. package/dist/packages/stats/clusterDetection.d.ts +2 -2
  43. package/dist/packages/stats/patternClassification.d.ts +2 -2
  44. package/dist/packages/stats/regions.d.ts +6 -6
  45. package/dist/packages/stats/regions.js +1 -1
  46. package/dist/packages/stats/renderFindingsReport.js +15 -15
  47. package/dist/packages/stats/renderSummaryReport.js +17 -17
  48. package/dist/packages/stats/types.d.ts +34 -4
  49. package/llms.txt +39 -0
  50. package/package.json +12 -8
@@ -1 +1 @@
1
- import{getReticleCell as Qt}from"../core/reticle.js";import{affineRotation as ft,affineMirror as Vt,affineCompose as at,affinePoint as X,affineVector as dt,affineSwapsAxes as Ut}from"../core/transforms.js";import{contrastTextColor as Y,SPEC_PASS_FILL as ht,SPEC_FAIL_LOW as Jt,SPEC_FAIL_HIGH as pt}from"./colorMap.js";import{getColorScheme as Zt}from"./colorSchemes.js";import{getDieTestValue as R,getTestPassStatus as G,isParametricTest as mt}from"./buildWaferMap.js";import{fmt as yt,fmtColorbarAxis as gt,fmtAggregationMethod as Rt}from"./fmt.js";import{metadataValueColor as te}from"./colorMap.js";import{compareNatural as ee,clamp01 as Ct}from"../core/utils.js";import{prettyKey as ne}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function findTestDef(t,e){return t?.find(n=>n.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,n)=>e-n)}const ae="#d3d6db",Nt="#e8e9ea",ie="#eceef0",w="#d6d9dd";function it(t,e){const n=e?t.metadata?.[e]:void 0;return n!=null&&(typeof n=="string"||typeof n=="number"||typeof n=="boolean")?String(n):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function se(t,e,n){const{x:i,y:s}=t.center,a=ft(t.orientation,i,s),o=at(Vt(e?.x??!1,e?.y??!1,i,s),ft(t.orientation,i,s)),l=at(Vt(n?.flipX??!1,n?.flipY??!1,i,s),ft(n?.rotation??0,i,s));return{bakedToScreen:l,physicalToScreen:at(l,a),gridToScreen:at(l,o)}}function _(t,e=!1){return{points:[t],closed:e}}function oe(t,e){const{center:n,radius:i,notch:s}=t;let a=n.x+i*Math.cos(e),o=n.y+i*Math.sin(e);if(!s)return{x:a,y:o};const l=Math.sqrt(i**2-(s.length/2)**2),f=s.length/2,y=a-n.x,m=o-n.y;return s.type==="bottom"&&m<-l?(o=n.y-l,a=n.x+Math.max(-f,Math.min(f,y))):s.type==="top"&&m>l?(o=n.y+l,a=n.x+Math.max(-f,Math.min(f,y))):s.type==="left"&&y<-l?(a=n.x-l,o=n.y+Math.max(-f,Math.min(f,m))):s.type==="right"&&y>l&&(a=n.x+l,o=n.y+Math.max(-f,Math.min(f,m))),{x:a,y:o}}function Ht(t,e){return t.map(e).join(" / ")}function re(t,e=256){const n=Array.from({length:e},(i,s)=>t(s/(e-1)));return i=>n[Math.min(e-1,Math.max(0,Math.round(i*(e-1))))]}function le(t,e){const n=Math.max(1,Math.min(t.width,t.height)),i=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(n*.55,i*1.8))))}function ce(t,e,n){if(e?.length){const i=e.flatMap(s=>{const a=s.testNumber;if(!mt(s)){const f=G(t,a,s);return f===void 0?[]:[{key:a,label:s.name,value:f?"Pass":"Fail"}]}const o=R(t,a);if(o===void 0)return[];const l=t.testPass?.[a]===!1?" <i>(recorded fail)</i>":"";return[{key:a,label:s.name,value:`${yt(o,s.unit,n)}${l}`}]});if(i.length)return i}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:yt(s,void 0,n)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,n={}){const{testDefs:i,hbinDefs:s,sbinDefs:a,fallbackFormat:o,aggrMethod:l,lotSize:f,waferMeta:y,activeTest:m,reticleConfig:$}=n,D=s?new Map(s.map(r=>[r.bin,r])):null,v=a?new Map(a.map(r=>[r.bin,r])):null,x=[`Die (${t.x}, ${t.y})`];if($){const r=Qt(t,$);x.push(`Reticle (${r.column}, ${r.row})`)}if(e==="stackedValues"){const r=R(t,0);if(r!==void 0){const c=i?.[0],p=c?.testNumber,h=c?.name??(p!=null?`Test ${p}`:"Value"),M=l?` (${l})`:"";x.push(`${h}${M}: ${yt(r,c?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const r=R(t,0),c=e==="stackedSoftBins"?t.sbin:t.hbin;if(r!==void 0){const p=e==="stackedSoftBins"?v:D,h=c!==void 0?p?.get(c)?.name?`${c} \xB7 ${p.get(c).name}`:`Bin ${c}`:"Bin";let M;if(l==="percent")M=`${r.toFixed(0)}%`;else{const b=f?` (${(r/f*100).toFixed(0)}% of lot)`:"";M=`${r}${b}`}const T=l?` [${Rt(l)}]`:"";x.push(`${h}: ${M}${T}`)}}else{const r=ce(t,i,o);if(e==="value"&&r.length){const{testNumber:c}=resolveTestNumber(m??0,i),p=findTestDef(i,c),h=R(t,c);let M=r.findIndex(A=>A.key===c);M<0&&(M=0);const T=r[M];let b=`<b>${T.label}: ${T.value}</b>`;if(T.key===c&&p){const A=classifySpec(h,p);(A==="failLow"||A==="failHigh")&&(b+=" <i>(out of spec)</i>")}x.push(b);const O=r.length-1;O>0&&x.push(`<i>+${O} more test${O===1?"":"s"}</i>`)}else if(r.length){const c=r.length;x.push(`<i>${c} test result${c===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const c=[];if(t.hbin!==void 0){const p=D?.get(t.hbin)?.name,h=p?`${t.hbin} \xB7 ${p}`:String(t.hbin);c.push(`HBin: ${h}`)}if(t.sbin!==void 0){const p=v?.get(t.sbin)?.name,h=p?`${t.sbin} \xB7 ${p}`:String(t.sbin);c.push(`SBin: ${h}`)}x.push(c.join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&x.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&x.push(`Site: ${t.siteNum}`),t.partId!==void 0&&x.push(`Part ID: ${t.partId}`),t.partial&&x.push("<i>partial die</i>"),t.probeIndex!==void 0&&x.push(`Probe: #${t.probeIndex}`);const k={...y??{},...t.metadata??{}};for(const[r,c]of Object.entries(k))c!=null&&x.push(`${r}: ${String(c)}`);return x.join("<br>")}export function buildMapTitle(t,e="engineering",n){const i=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=a=>{const o=n?.length===1?n[0]:void 0;return o===void 0?`${a} Bin`:o.name?`${a} Bin ${o.bin} \xB7 ${o.name}`:`${a} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:i};case"stackedSoftBins":return{primary:s("Soft"),secondary:i};case"metadata":{const a=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?ne(t.activeMetadataKey):"Metadata";return{primary:a?.label??o,secondary:""}}case"stackedValues":{const a=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=gt(o,a?.name,a?.unit,e),f=l||"Value",y=t.aggrMethod?` \xB7 ${Rt(t.aggrMethod)}`:"";return{primary:`${f}${y}`,secondary:i}}default:{const a=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=gt(o,a?.name,a?.unit,e),f=l||a?.name;if(t.passFailDisplay!=="off"){const y=a?.testNumber??t.activeTest,m=f?`${f} \xB7 #${y}`:`Test ${y}`,$=t.passFailDisplay==="spec"?"Spec pass/fail":a!==void 0&&!mt(a)?"Functional pass/fail":"Tester pass/fail";return{primary:m,secondary:$}}return{primary:f??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,n){const{plotMode:i,colorFns:s,normalize:a,activeTest:o,valueRange:l,testDefs:f,fallbackFormat:y,passFailDisplay:m="off",activeMetadataKey:$,metadataColorMap:D}=n,v=findTestDef(f,o),{tickFmt:x}=gt(l[1],v?.name,v?.unit,y),{testNumber:k}=resolveTestNumber(o,f);return t.flatMap((r,c)=>{let p="",h="#111111";if(i==="value"&&m==="test"){const b=G(r,k,v);if(b===void 0)return[];p=b?"P":"F",h=Y(b?ht:pt)}else if(i==="value"){const b=R(r,k);if(b===void 0)return[];p=Ht([b],x),h=Y(s.forValue(a(b)))}else if(i==="hardBin"||i==="softBin"){const b=i==="softBin"?r.sbin:r.hbin;if(b===void 0)return[];p=String(b),h=Y(s.forBin(b))}else if(i==="metadata"){const b=it(r,$);if(b===void 0)return[];p=b,h=Y(D?.get(p)??w)}else{const b=R(r,0);if(b===void 0)return[];p=Ht([b],x),h=Y(s.forValue(a(b)))}const M=e?e[c*2]:r.physX,T=e?e[c*2+1]:r.physY;return[{x:M,y:T,text:p,fontSize:le(r,p),color:h,align:"center"}]})}function Ot(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function ue(t,e,n=720){const{center:i,radius:s,notch:a}=t,o=k=>X(e,k.x,k.y);if(!(a!==void 0&&t.diameter>150)){const k=[];for(let r=0;r<=n;r++){const c=2*Math.PI*r/n;k.push(o(oe(t,c)))}return[{kind:"wafer-boundary",..._(k,!0),lineColor:"#888888",lineWidth:1}]}const f=1.25,y=a.length,m=Ot(a.type),$=Math.atan2(y,s),D=s-f,v=[];let x=!1;for(let k=0;k<=n;k++){const r=2*Math.PI*k/n;let c=r-m;for(;c>Math.PI;)c-=2*Math.PI;for(;c<-Math.PI;)c+=2*Math.PI;if(Math.abs(c)<=$){if(!x){x=!0;const p={x:i.x+s*Math.cos(m-$),y:i.y+s*Math.sin(m-$)},h={x:i.x+D*Math.cos(m),y:i.y+D*Math.sin(m)},M={x:i.x+s*Math.cos(m+$),y:i.y+s*Math.sin(m+$)};v.push(o(p),o(h),o(M))}continue}v.push(o({x:i.x+s*Math.cos(r),y:i.y+s*Math.sin(r)}))}return[{kind:"wafer-boundary",..._(v,!0),lineColor:"#888888",lineWidth:1}]}function fe(t,e,n,i=360){const s=[],a=Math.max(1,Math.floor(n));for(let o=1;o<a;o++){const l=t.radius*o/a,f=[];for(let y=0;y<=i;y++){const m=2*Math.PI*y/i,$={x:t.center.x+l*Math.cos(m),y:t.center.y+l*Math.sin(m)};f.push(X(e,$.x,$.y))}s.push({kind:"ring-boundary",..._(f,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function de(t,e,n,i){const{x:s,y:a}=t.center,o=t.radius,l=(r,c)=>X(e,r,c),f=n-s,y=Math.sqrt(Math.max(0,o*o-f*f)),m=l(n,a-y),$=l(n,a+y),D=i-a,v=Math.sqrt(Math.max(0,o*o-D*D)),x=l(s-v,i),k=l(s+v,i);return[{kind:"quadrant-boundary",..._([m,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",..._([x,k]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function he(t,e){return t.map(n=>{const i=n.width/2,s=n.height/2;return{kind:"reticle",points:[[[n.x-i,n.y-s],[n.x+i,n.y-s],[n.x+i,n.y+s],[n.x-i,n.y+s]].map(([o,l])=>X(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function pe(t){const e=t.filter(n=>n.probeIndex!==void 0).sort((n,i)=>(n.probeIndex??0)-(i.probeIndex??0));return e.length?[{kind:"probe-path",..._(e.map(n=>({x:n.physX,y:n.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function me(t,e,n,i,s,a,o,l,f,y,m,$,D,v="off",x,k,r){const c=e.width-o,p=e.height-o,h=a?p:c,M=a?c:p,T=B=>s==="softBin"?B.sbin:B.hbin,b=B=>it(B,x);if(e.partial){t.push({x:n,y:i,width:h,height:M,fill:ae,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:n,y:i,width:h,height:M,fill:ie,type:"stacked",metadata:e.metadata});return}if(f!==void 0&&(s==="hardBin"||s==="softBin")&&T(e)!==f){t.push({x:n,y:i,width:h,height:M,fill:Nt,type:"hardBin",metadata:e.metadata});return}if(r!==void 0&&s==="metadata"&&b(e)!==r){t.push({x:n,y:i,width:h,height:M,fill:Nt,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(v==="test"){const U=G(e,m,D),st=U===void 0?w:U?ht:pt;t.push({x:n,y:i,width:h,height:M,fill:st,type:"value",metadata:e.metadata});return}const B=R(e,m),P=classifySpec(B,D);let W,Q;B===void 0?W=w:v==="spec"?W=P==="failLow"?Jt:P==="failHigh"?pt:ht:(W=l.forValue(y(B)),(P==="failLow"||P==="failHigh")&&(Q=P)),t.push({x:n,y:i,width:h,height:M,fill:W,type:"value",specMark:Q,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const B=T(e),P=B!=null?l.forBin(B):w;t.push({x:n,y:i,width:h,height:M,fill:P,type:s,metadata:e.metadata});return}if(s==="metadata"){const B=b(e),P=B!==void 0?k?.get(B)??w:w;t.push({x:n,y:i,width:h,height:M,fill:P,type:"metadata",metadata:e.metadata});return}const O=R(e,0),A=O!==void 0?l.forValue(y(O)):w;t.push({x:n,y:i,width:h,height:M,fill:A,type:"value",metadata:e.metadata})}function ye(t,e,n){const i=t.radius*.15,s=dt(e,i,0),a=dt(e,0,i),o=s.x+a.x>=0?-1:1,l=s.y+a.y>=0?-1:1,f={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},y={x:f.x+s.x,y:f.y+s.y},m={x:f.x+a.x,y:f.y+a.y};return n.push({x:y.x+s.x*.35,y:y.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:m.x+a.x*.35,y:m.y+a.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",..._([f,y]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",..._([f,m]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,n={},i){const s=n.reticles??[],{plotMode:a="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:f=!1,ringCount:y=4,showRingBoundaries:m=!1,showQuadrantBoundaries:$=!1,showXYIndicator:D=!1,dieGap:v=1,colorScheme:x="default",highlightBin:k,highlightMetadataValue:r,interactiveTransform:c,valueRange:p,testDefs:h,activeTest:M=0,activeMetadataKey:T,fallbackFormat:b="engineering",aggregationMethod:O,lotSize:A,dataAxisFlip:B,isLotStack:P=!1,logScale:W,colorbarRangeMode:Q="spec",passFailDisplay:U,showPartialDies:st=!0}=n,xt=U??"off",bt=i?.hbinDefs,Mt=i?.sbinDefs,St=i?.metadataFields,At=St?.find(u=>u.key===T),Et={x:(B?.x??!1)!==(c?.flipX??!1),y:(B?.y??!1)!==(c?.flipY??!1)},_t=bt?new Map(bt.map(u=>[u.bin,u])):null,wt=Mt?new Map(Mt.map(u=>[u.bin,u])):null,ot=a==="softBin"?wt:_t,rt=Zt(x),$t={forValue:re(rt.forValue),forBin:x==="custom"&&ot?u=>ot.get(u)?.color??rt.forBin(u):rt.forBin};let{testNumber:H}=resolveTestNumber(M,h);if(!h?.length&&a==="value"&&!e.some(d=>d.testValues&&H in d.testValues)){const d=e.reduce((g,L)=>{if(!L.testValues)return g;const S=Object.keys(L.testValues).map(Number),K=S.length?Math.min(...S):void 0;return K!==void 0&&(g===void 0||K<g)?K:g},void 0);d!==void 0&&(H=d)}let J;if(Array.isArray(p))J=p;else if(p){const{testNumber:u}=resolveTestNumber(p.test,h);J=u===H?p.range:void 0}const I=findTestDef(h,H),kt=a==="value"&&I!==void 0&&!mt(I),Wt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let C="off";a==="value"&&(kt?C="test":xt==="spec"&&Wt?C="spec":xt==="test"&&e.some(u=>G(u,H,I)!==void 0)&&(C="test"));const Bt=C==="spec"?"spec":Q;let V,N,vt=!1;if(J)[V,N]=J;else{const u=a==="stackedValues"||a==="stackedBins"||a==="stackedSoftBins",d=!u&&a==="value"&&Bt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let g=1/0,L=-1/0,S=!0;for(const K of e){const q=u?R(K,0):R(K,H);q!==void 0&&(q<g&&(g=q),q>L&&(L=q),Number.isInteger(q)||(S=!1))}isFinite(g)||(S=!1),vt=S,d?(V=I.limitLow!==void 0?I.limitLow:isFinite(g)?g:0,N=I.limitHigh!==void 0?I.limitHigh:isFinite(L)?L:1):(V=isFinite(g)?g:0,N=isFinite(L)?L:1)}V===N&&(N=V+1);const Tt=kt?!1:W??I?.logScale??!1,It=Tt&&V>0&&N>0,jt=It;let Z;if(It){const u=Math.log10(V),d=Math.log10(N)-u;Z=g=>g<=0?0:Ct((Math.log10(g)-u)/d)}else Z=u=>Ct((u-V)/(N-V));const F=se(t,B,c);let Lt=null;if(t.notch){const u=Ot(t.notch.type),d=dt(F.physicalToScreen,Math.cos(u)*t.radius,Math.sin(u)*t.radius),g=Math.hypot(d.x,d.y);g>0&&(Lt={x:d.x/g,y:d.y/g})}let j=1/0;for(let u=0;u<e.length;u++){const d=e[u];d.width<j&&(j=d.width),d.height<j&&(j=d.height)}const zt=j!==1/0?Math.min(v,j*.12):v,Dt=[],lt=[],Kt=F.bakedToScreen.a===1&&F.bakedToScreen.b===0&&F.bakedToScreen.c===0&&F.bakedToScreen.d===1&&F.bakedToScreen.e===0&&F.bakedToScreen.f===0;let E=null;if(!Kt){E=new Float64Array(e.length*2);for(let u=0;u<e.length;u++){const d=X(F.bakedToScreen,e[u].physX,e[u].physY);E[u*2]=d.x,E[u*2+1]=d.y}}const qt=Ut(F.gridToScreen),Xt=a==="hardBin"||a==="softBin"||a==="stackedBins"||a==="stackedSoftBins",Yt=a==="softBin"||a==="stackedSoftBins";let tt=Xt?new Map:void 0;const ct=a==="value"&&C==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ut=a==="value"&&C==="test"?{pass:0,fail:0}:void 0,Ft=a==="metadata";let et=Ft?new Map:void 0,nt=null;if(Ft&&T){const u=new Set;for(const g of e){if(g.partial||g.edgeExcluded)continue;const L=it(g,T);L!==void 0&&u.add(L)}const d=[...u].sort(ee);nt=new Map(d.map((g,L)=>[g,At?.values?.find(S=>S.value===g)?.color??te(L)]))}for(let u=0;u<e.length;u++){const d=e[u],g=E?E[u*2]:d.physX,L=E?E[u*2+1]:d.physY;if(lt.push({x:g,y:L}),tt&&!d.partial&&!d.edgeExcluded){const S=Yt?d.sbin:d.hbin;S!=null&&tt.set(S,(tt.get(S)??0)+1)}if(ct&&!d.partial&&!d.edgeExcluded){const S=classifySpec(R(d,H),I);S&&ct[S]++}if(ut&&!d.partial&&!d.edgeExcluded){const S=G(d,H,I);S!==void 0&&ut[S?"pass":"fail"]++}if(et&&!d.partial&&!d.edgeExcluded){const S=it(d,T);S!==void 0&&et.set(S,(et.get(S)??0)+1)}d.partial&&!st||me(Dt,d,g,L,a,qt,zt,$t,k,Z,H,ot,I,C,T,nt,r)}const Pt=o?generateTextOverlay(e,E,{plotMode:a,colorFns:$t,normalize:Z,activeTest:M,valueRange:[V,N],testDefs:h,fallbackFormat:b,passFailDisplay:C,activeMetadataKey:T,metadataColorMap:nt}):[],z=ue(t,F.physicalToScreen);m&&z.push(...fe(t,F.physicalToScreen,y)),$&&z.push(...de(t,F.physicalToScreen,t.center.x,t.center.y)),l&&z.push(...he(s,F.gridToScreen)),f&&z.push(...pe(e)),D&&z.push(...ye(t,F.gridToScreen,Pt));const Gt=lt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Dt,hoverPoints:lt,texts:Pt,overlays:z,plotMode:a,colorScheme:x,metadata:t.metadata??null,dies:e,valueRange:[V,N],allIntegerValues:vt,testDefs:h,activeTest:M,logScale:jt,logScaleRequested:Tt,aggrMethod:O,lotSize:A,axisFlip:Et,dataAxisFlip:{x:B?.x??!1,y:B?.y??!1},gridToScreen:F.gridToScreen,rotation:((t.orientation+(c?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:P,passFailDisplay:C,colorBySpec:C==="spec",colorbarRangeMode:Bt,waferCenter:t.center,waferRadius:t.radius,notchDir:Lt,binCounts:tt,metadataCounts:et,metadataColorMap:nt,activeMetadataKey:T,metadataFields:St,specCounts:ct,passFailCounts:ut,dieBounds:Gt}}export{getDieKey}from"../core/dies.js";
1
+ import{hasPosition as Qt}from"../core/dies.js";import{getReticleCell as Ut}from"../core/reticle.js";import{affineRotation as ft,affineMirror as Vt,affineCompose as at,affinePoint as X,affineVector as dt,affineSwapsAxes as Jt}from"../core/transforms.js";import{contrastTextColor as Y,SPEC_PASS_FILL as ht,SPEC_FAIL_LOW as Zt,SPEC_FAIL_HIGH as pt}from"./colorMap.js";import{getColorScheme as te}from"./colorSchemes.js";import{getDieTestValue as R,getTestPassStatus as G,isParametricTest as mt}from"./buildWaferMap.js";import{fmt as yt,fmtColorbarAxis as gt,fmtAggregationMethod as Rt}from"./fmt.js";import{metadataValueColor as ee}from"./colorMap.js";import{compareNatural as ne,clamp01 as Ct}from"../core/utils.js";import{prettyKey as ae}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function findTestDef(t,e){return t?.find(n=>n.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,n)=>e-n)}const ie="#d3d6db",Nt="#e8e9ea",se="#eceef0",w="#d6d9dd";function it(t,e){const n=e?t.metadata?.[e]:void 0;return n!=null&&(typeof n=="string"||typeof n=="number"||typeof n=="boolean")?String(n):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function oe(t,e,n){const{x:i,y:s}=t.center,a=ft(t.orientation,i,s),o=at(Vt(e?.x??!1,e?.y??!1,i,s),ft(t.orientation,i,s)),l=at(Vt(n?.flipX??!1,n?.flipY??!1,i,s),ft(n?.rotation??0,i,s));return{bakedToScreen:l,physicalToScreen:at(l,a),gridToScreen:at(l,o)}}function _(t,e=!1){return{points:[t],closed:e}}function re(t,e){const{center:n,radius:i,notch:s}=t;let a=n.x+i*Math.cos(e),o=n.y+i*Math.sin(e);if(!s)return{x:a,y:o};const l=Math.sqrt(i**2-(s.length/2)**2),f=s.length/2,y=a-n.x,m=o-n.y;return s.type==="bottom"&&m<-l?(o=n.y-l,a=n.x+Math.max(-f,Math.min(f,y))):s.type==="top"&&m>l?(o=n.y+l,a=n.x+Math.max(-f,Math.min(f,y))):s.type==="left"&&y<-l?(a=n.x-l,o=n.y+Math.max(-f,Math.min(f,m))):s.type==="right"&&y>l&&(a=n.x+l,o=n.y+Math.max(-f,Math.min(f,m))),{x:a,y:o}}function Ht(t,e){return t.map(e).join(" / ")}function le(t,e=256){const n=Array.from({length:e},(i,s)=>t(s/(e-1)));return i=>n[Math.min(e-1,Math.max(0,Math.round(i*(e-1))))]}function ce(t,e){const n=Math.max(1,Math.min(t.width,t.height)),i=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(n*.55,i*1.8))))}function ue(t,e,n){if(e?.length){const i=e.flatMap(s=>{const a=s.testNumber;if(!mt(s)){const f=G(t,a,s);return f===void 0?[]:[{key:a,label:s.name,value:f?"Pass":"Fail"}]}const o=R(t,a);if(o===void 0)return[];const l=t.testPass?.[a]===!1?" <i>(recorded fail)</i>":"";return[{key:a,label:s.name,value:`${yt(o,s.unit,n)}${l}`}]});if(i.length)return i}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:yt(s,void 0,n)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,n={}){const{testDefs:i,hbinDefs:s,sbinDefs:a,fallbackFormat:o,aggrMethod:l,lotSize:f,waferMeta:y,activeTest:m,reticleConfig:$}=n,P=s?new Map(s.map(r=>[r.bin,r])):null,v=a?new Map(a.map(r=>[r.bin,r])):null,x=[`Die (${t.x}, ${t.y})`];if($&&Qt(t)){const r=Ut(t,$);x.push(`Reticle (${r.column}, ${r.row})`)}if(e==="stackedValues"){const r=R(t,0);if(r!==void 0){const c=i?.[0],p=c?.testNumber,h=c?.name??(p!=null?`Test ${p}`:"Value"),M=l?` (${l})`:"";x.push(`${h}${M}: ${yt(r,c?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const r=R(t,0),c=e==="stackedSoftBins"?t.sbin:t.hbin;if(r!==void 0){const p=e==="stackedSoftBins"?v:P,h=c!==void 0?p?.get(c)?.name?`${c} \xB7 ${p.get(c).name}`:`Bin ${c}`:"Bin";let M;if(l==="percent")M=`${r.toFixed(0)}%`;else{const b=f?` (${(r/f*100).toFixed(0)}% of lot)`:"";M=`${r}${b}`}const T=l?` [${Rt(l)}]`:"";x.push(`${h}: ${M}${T}`)}}else{const r=ue(t,i,o);if(e==="value"&&r.length){const{testNumber:c}=resolveTestNumber(m??0,i),p=findTestDef(i,c),h=R(t,c);let M=r.findIndex(A=>A.key===c);M<0&&(M=0);const T=r[M];let b=`<b>${T.label}: ${T.value}</b>`;if(T.key===c&&p){const A=classifySpec(h,p);(A==="failLow"||A==="failHigh")&&(b+=" <i>(out of spec)</i>")}x.push(b);const O=r.length-1;O>0&&x.push(`<i>+${O} more test${O===1?"":"s"}</i>`)}else if(r.length){const c=r.length;x.push(`<i>${c} test result${c===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const c=[];if(t.hbin!==void 0){const p=P?.get(t.hbin)?.name,h=p?`${t.hbin} \xB7 ${p}`:String(t.hbin);c.push(`HBin: ${h}`)}if(t.sbin!==void 0){const p=v?.get(t.sbin)?.name,h=p?`${t.sbin} \xB7 ${p}`:String(t.sbin);c.push(`SBin: ${h}`)}x.push(c.join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&x.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&x.push(`Site: ${t.siteNum}`),t.partId!==void 0&&x.push(`Part ID: ${t.partId}`),t.partial&&x.push("<i>partial die</i>"),t.probeIndex!==void 0&&x.push(`Probe: #${t.probeIndex}`);const k={...y??{},...t.metadata??{}};for(const[r,c]of Object.entries(k))c!=null&&x.push(`${r}: ${String(c)}`);return x.join("<br>")}export function buildMapTitle(t,e="engineering",n){const i=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=a=>{const o=n?.length===1?n[0]:void 0;return o===void 0?`${a} Bin`:o.name?`${a} Bin ${o.bin} \xB7 ${o.name}`:`${a} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:i};case"stackedSoftBins":return{primary:s("Soft"),secondary:i};case"metadata":{const a=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?ae(t.activeMetadataKey):"Metadata";return{primary:a?.label??o,secondary:""}}case"stackedValues":{const a=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=gt(o,a?.name,a?.unit,e),f=l||"Value",y=t.aggrMethod?` \xB7 ${Rt(t.aggrMethod)}`:"";return{primary:`${f}${y}`,secondary:i}}default:{const a=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=gt(o,a?.name,a?.unit,e),f=l||a?.name;if(t.passFailDisplay!=="off"){const y=a?.testNumber??t.activeTest,m=f?`${f} \xB7 #${y}`:`Test ${y}`,$=t.passFailDisplay==="spec"?"Spec pass/fail":a!==void 0&&!mt(a)?"Functional pass/fail":"Tester pass/fail";return{primary:m,secondary:$}}return{primary:f??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,n){const{plotMode:i,colorFns:s,normalize:a,activeTest:o,valueRange:l,testDefs:f,fallbackFormat:y,passFailDisplay:m="off",activeMetadataKey:$,metadataColorMap:P}=n,v=findTestDef(f,o),{tickFmt:x}=gt(l[1],v?.name,v?.unit,y),{testNumber:k}=resolveTestNumber(o,f);return t.flatMap((r,c)=>{let p="",h="#111111";if(i==="value"&&m==="test"){const b=G(r,k,v);if(b===void 0)return[];p=b?"P":"F",h=Y(b?ht:pt)}else if(i==="value"){const b=R(r,k);if(b===void 0)return[];p=Ht([b],x),h=Y(s.forValue(a(b)))}else if(i==="hardBin"||i==="softBin"){const b=i==="softBin"?r.sbin:r.hbin;if(b===void 0)return[];p=String(b),h=Y(s.forBin(b))}else if(i==="metadata"){const b=it(r,$);if(b===void 0)return[];p=b,h=Y(P?.get(p)??w)}else{const b=R(r,0);if(b===void 0)return[];p=Ht([b],x),h=Y(s.forValue(a(b)))}const M=e?e[c*2]:r.physX,T=e?e[c*2+1]:r.physY;return[{x:M,y:T,text:p,fontSize:ce(r,p),color:h,align:"center"}]})}function Ot(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function fe(t,e,n=720){const{center:i,radius:s,notch:a}=t,o=k=>X(e,k.x,k.y);if(!(a!==void 0&&t.diameter>150)){const k=[];for(let r=0;r<=n;r++){const c=2*Math.PI*r/n;k.push(o(re(t,c)))}return[{kind:"wafer-boundary",..._(k,!0),lineColor:"#888888",lineWidth:1}]}const f=1.25,y=a.length,m=Ot(a.type),$=Math.atan2(y,s),P=s-f,v=[];let x=!1;for(let k=0;k<=n;k++){const r=2*Math.PI*k/n;let c=r-m;for(;c>Math.PI;)c-=2*Math.PI;for(;c<-Math.PI;)c+=2*Math.PI;if(Math.abs(c)<=$){if(!x){x=!0;const p={x:i.x+s*Math.cos(m-$),y:i.y+s*Math.sin(m-$)},h={x:i.x+P*Math.cos(m),y:i.y+P*Math.sin(m)},M={x:i.x+s*Math.cos(m+$),y:i.y+s*Math.sin(m+$)};v.push(o(p),o(h),o(M))}continue}v.push(o({x:i.x+s*Math.cos(r),y:i.y+s*Math.sin(r)}))}return[{kind:"wafer-boundary",..._(v,!0),lineColor:"#888888",lineWidth:1}]}function de(t,e,n,i=360){const s=[],a=Math.max(1,Math.floor(n));for(let o=1;o<a;o++){const l=t.radius*o/a,f=[];for(let y=0;y<=i;y++){const m=2*Math.PI*y/i,$={x:t.center.x+l*Math.cos(m),y:t.center.y+l*Math.sin(m)};f.push(X(e,$.x,$.y))}s.push({kind:"ring-boundary",..._(f,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function he(t,e,n,i){const{x:s,y:a}=t.center,o=t.radius,l=(r,c)=>X(e,r,c),f=n-s,y=Math.sqrt(Math.max(0,o*o-f*f)),m=l(n,a-y),$=l(n,a+y),P=i-a,v=Math.sqrt(Math.max(0,o*o-P*P)),x=l(s-v,i),k=l(s+v,i);return[{kind:"quadrant-boundary",..._([m,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",..._([x,k]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function pe(t,e){return t.map(n=>{const i=n.width/2,s=n.height/2;return{kind:"reticle",points:[[[n.x-i,n.y-s],[n.x+i,n.y-s],[n.x+i,n.y+s],[n.x-i,n.y+s]].map(([o,l])=>X(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function me(t){const e=t.filter(n=>n.probeIndex!==void 0).sort((n,i)=>(n.probeIndex??0)-(i.probeIndex??0));return e.length?[{kind:"probe-path",..._(e.map(n=>({x:n.physX,y:n.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function ye(t,e,n,i,s,a,o,l,f,y,m,$,P,v="off",x,k,r){const c=e.width-o,p=e.height-o,h=a?p:c,M=a?c:p,T=B=>s==="softBin"?B.sbin:B.hbin,b=B=>it(B,x);if(e.partial){t.push({x:n,y:i,width:h,height:M,fill:ie,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:n,y:i,width:h,height:M,fill:se,type:"stacked",metadata:e.metadata});return}if(f!==void 0&&(s==="hardBin"||s==="softBin")&&T(e)!==f){t.push({x:n,y:i,width:h,height:M,fill:Nt,type:"hardBin",metadata:e.metadata});return}if(r!==void 0&&s==="metadata"&&b(e)!==r){t.push({x:n,y:i,width:h,height:M,fill:Nt,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(v==="test"){const U=G(e,m,P),st=U===void 0?w:U?ht:pt;t.push({x:n,y:i,width:h,height:M,fill:st,type:"value",metadata:e.metadata});return}const B=R(e,m),F=classifySpec(B,P);let W,Q;B===void 0?W=w:v==="spec"?W=F==="failLow"?Zt:F==="failHigh"?pt:ht:(W=l.forValue(y(B)),(F==="failLow"||F==="failHigh")&&(Q=F)),t.push({x:n,y:i,width:h,height:M,fill:W,type:"value",specMark:Q,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const B=T(e),F=B!=null?l.forBin(B):w;t.push({x:n,y:i,width:h,height:M,fill:F,type:s,metadata:e.metadata});return}if(s==="metadata"){const B=b(e),F=B!==void 0?k?.get(B)??w:w;t.push({x:n,y:i,width:h,height:M,fill:F,type:"metadata",metadata:e.metadata});return}const O=R(e,0),A=O!==void 0?l.forValue(y(O)):w;t.push({x:n,y:i,width:h,height:M,fill:A,type:"value",metadata:e.metadata})}function ge(t,e,n){const i=t.radius*.15,s=dt(e,i,0),a=dt(e,0,i),o=s.x+a.x>=0?-1:1,l=s.y+a.y>=0?-1:1,f={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},y={x:f.x+s.x,y:f.y+s.y},m={x:f.x+a.x,y:f.y+a.y};return n.push({x:y.x+s.x*.35,y:y.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:m.x+a.x*.35,y:m.y+a.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",..._([f,y]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",..._([f,m]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,n={},i){const s=n.reticles??[],{plotMode:a="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:f=!1,ringCount:y=4,showRingBoundaries:m=!1,showQuadrantBoundaries:$=!1,showXYIndicator:P=!1,dieGap:v=1,colorScheme:x="default",highlightBin:k,highlightMetadataValue:r,interactiveTransform:c,valueRange:p,testDefs:h,activeTest:M=0,activeMetadataKey:T,fallbackFormat:b="engineering",aggregationMethod:O,lotSize:A,dataAxisFlip:B,isLotStack:F=!1,logScale:W,colorbarRangeMode:Q="spec",passFailDisplay:U,showPartialDies:st=!0}=n,xt=U??"off",bt=i?.hbinDefs,Mt=i?.sbinDefs,St=i?.metadataFields,At=St?.find(u=>u.key===T),Et={x:(B?.x??!1)!==(c?.flipX??!1),y:(B?.y??!1)!==(c?.flipY??!1)},_t=bt?new Map(bt.map(u=>[u.bin,u])):null,wt=Mt?new Map(Mt.map(u=>[u.bin,u])):null,ot=a==="softBin"?wt:_t,rt=te(x),$t={forValue:le(rt.forValue),forBin:x==="custom"&&ot?u=>ot.get(u)?.color??rt.forBin(u):rt.forBin};let{testNumber:H}=resolveTestNumber(M,h);if(!h?.length&&a==="value"&&!e.some(d=>d.testValues&&H in d.testValues)){const d=e.reduce((g,L)=>{if(!L.testValues)return g;const S=Object.keys(L.testValues).map(Number),K=S.length?Math.min(...S):void 0;return K!==void 0&&(g===void 0||K<g)?K:g},void 0);d!==void 0&&(H=d)}let J;if(Array.isArray(p))J=p;else if(p){const{testNumber:u}=resolveTestNumber(p.test,h);J=u===H?p.range:void 0}const I=findTestDef(h,H),kt=a==="value"&&I!==void 0&&!mt(I),Wt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let C="off";a==="value"&&(kt?C="test":xt==="spec"&&Wt?C="spec":xt==="test"&&e.some(u=>G(u,H,I)!==void 0)&&(C="test"));const Bt=C==="spec"?"spec":Q;let V,N,vt=!1;if(J)[V,N]=J;else{const u=a==="stackedValues"||a==="stackedBins"||a==="stackedSoftBins",d=!u&&a==="value"&&Bt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let g=1/0,L=-1/0,S=!0;for(const K of e){const q=u?R(K,0):R(K,H);q!==void 0&&(q<g&&(g=q),q>L&&(L=q),Number.isInteger(q)||(S=!1))}isFinite(g)||(S=!1),vt=S,d?(V=I.limitLow!==void 0?I.limitLow:isFinite(g)?g:0,N=I.limitHigh!==void 0?I.limitHigh:isFinite(L)?L:1):(V=isFinite(g)?g:0,N=isFinite(L)?L:1)}V===N&&(N=V+1);const Tt=kt?!1:W??I?.logScale??!1,It=Tt&&V>0&&N>0,jt=It;let Z;if(It){const u=Math.log10(V),d=Math.log10(N)-u;Z=g=>g<=0?0:Ct((Math.log10(g)-u)/d)}else Z=u=>Ct((u-V)/(N-V));const D=oe(t,B,c);let Lt=null;if(t.notch){const u=Ot(t.notch.type),d=dt(D.physicalToScreen,Math.cos(u)*t.radius,Math.sin(u)*t.radius),g=Math.hypot(d.x,d.y);g>0&&(Lt={x:d.x/g,y:d.y/g})}let j=1/0;for(let u=0;u<e.length;u++){const d=e[u];d.width<j&&(j=d.width),d.height<j&&(j=d.height)}const zt=j!==1/0?Math.min(v,j*.12):v,Pt=[],lt=[],Kt=D.bakedToScreen.a===1&&D.bakedToScreen.b===0&&D.bakedToScreen.c===0&&D.bakedToScreen.d===1&&D.bakedToScreen.e===0&&D.bakedToScreen.f===0;let E=null;if(!Kt){E=new Float64Array(e.length*2);for(let u=0;u<e.length;u++){const d=X(D.bakedToScreen,e[u].physX,e[u].physY);E[u*2]=d.x,E[u*2+1]=d.y}}const qt=Jt(D.gridToScreen),Xt=a==="hardBin"||a==="softBin"||a==="stackedBins"||a==="stackedSoftBins",Yt=a==="softBin"||a==="stackedSoftBins";let tt=Xt?new Map:void 0;const ct=a==="value"&&C==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ut=a==="value"&&C==="test"?{pass:0,fail:0}:void 0,Dt=a==="metadata";let et=Dt?new Map:void 0,nt=null;if(Dt&&T){const u=new Set;for(const g of e){if(g.partial||g.edgeExcluded)continue;const L=it(g,T);L!==void 0&&u.add(L)}const d=[...u].sort(ne);nt=new Map(d.map((g,L)=>[g,At?.values?.find(S=>S.value===g)?.color??ee(L)]))}for(let u=0;u<e.length;u++){const d=e[u],g=E?E[u*2]:d.physX,L=E?E[u*2+1]:d.physY;if(lt.push({x:g,y:L}),tt&&!d.partial&&!d.edgeExcluded){const S=Yt?d.sbin:d.hbin;S!=null&&tt.set(S,(tt.get(S)??0)+1)}if(ct&&!d.partial&&!d.edgeExcluded){const S=classifySpec(R(d,H),I);S&&ct[S]++}if(ut&&!d.partial&&!d.edgeExcluded){const S=G(d,H,I);S!==void 0&&ut[S?"pass":"fail"]++}if(et&&!d.partial&&!d.edgeExcluded){const S=it(d,T);S!==void 0&&et.set(S,(et.get(S)??0)+1)}d.partial&&!st||ye(Pt,d,g,L,a,qt,zt,$t,k,Z,H,ot,I,C,T,nt,r)}const Ft=o?generateTextOverlay(e,E,{plotMode:a,colorFns:$t,normalize:Z,activeTest:M,valueRange:[V,N],testDefs:h,fallbackFormat:b,passFailDisplay:C,activeMetadataKey:T,metadataColorMap:nt}):[],z=fe(t,D.physicalToScreen);m&&z.push(...de(t,D.physicalToScreen,y)),$&&z.push(...he(t,D.physicalToScreen,t.center.x,t.center.y)),l&&z.push(...pe(s,D.gridToScreen)),f&&z.push(...me(e)),P&&z.push(...ge(t,D.gridToScreen,Ft));const Gt=lt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Pt,hoverPoints:lt,texts:Ft,overlays:z,plotMode:a,colorScheme:x,metadata:t.metadata??null,dies:e,valueRange:[V,N],allIntegerValues:vt,testDefs:h,activeTest:M,logScale:jt,logScaleRequested:Tt,aggrMethod:O,lotSize:A,axisFlip:Et,dataAxisFlip:{x:B?.x??!1,y:B?.y??!1},gridToScreen:D.gridToScreen,rotation:((t.orientation+(c?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:F,passFailDisplay:C,colorBySpec:C==="spec",colorbarRangeMode:Bt,waferCenter:t.center,waferRadius:t.radius,notchDir:Lt,binCounts:tt,metadataCounts:et,metadataColorMap:nt,activeMetadataKey:T,metadataFields:St,specCounts:ct,passFailCounts:ut,dieBounds:Gt}}export{getDieKey}from"../core/dies.js";
@@ -9,10 +9,19 @@ import { type View, type ViewOptions, type PlotMode } from './buildView.js';
9
9
  * such as −7, 0, 5. They are NOT millimetre values.
10
10
  */
11
11
  export interface DieResult {
12
- /** Die grid X position (prober step coordinate). */
13
- x: number;
14
- /** Die grid Y position (prober step coordinate). */
15
- y: number;
12
+ /**
13
+ * Die grid X position (prober step coordinate). Omit (together with `y`)
14
+ * when this die has no reported spatial position at all — it still counts
15
+ * toward every non-spatial stat (yield, bin counts, per-test
16
+ * histograms/correlation/scatter) but is never placed on a wafer
17
+ * map/gallery canvas and never enters ring/quadrant/sector/reticle/
18
+ * cluster/pattern analysis. A die must be either fully positioned (both
19
+ * `x` and `y`) or fully unpositioned (neither) — `buildWaferMap` rejects
20
+ * the half-state.
21
+ */
22
+ x?: number;
23
+ /** Die grid Y position (prober step coordinate). See `x`. */
24
+ y?: number;
16
25
  /**
17
26
  * Test values keyed by test number — a stable per-test identity such as
18
27
  * STDF TEST_NUM or an equivalent application-defined integer. Keying by test
@@ -415,13 +424,27 @@ export interface WaferWarning {
415
424
  * so the pitch was derived as `diameter ÷ grid span`, assuming the grid spans
416
425
  * the full wafer. That assumption fails whenever edge dies are absent, which
417
426
  * silently scales every die position. Supply `dieConfig.width`/`height`.
427
+ * - `'test-count-capped'` — raised by `analyzeWaferMap`: more tests were found in
428
+ * the die data than the analysis cap allows, so test-value analysis was skipped
429
+ * entirely and NO test findings were produced. Pass `testNumbers` to scope it.
418
430
  *
419
431
  * The union is intentionally open to string so future advisory codes can be
420
432
  * added without a breaking change; switch with a `default` branch.
421
433
  */
422
- code: 'partial-coverage' | 'geometry-conflict' | 'inferred-pitch' | (string & {});
434
+ code: 'partial-coverage' | 'geometry-conflict' | 'inferred-pitch' | 'test-count-capped' | (string & {});
423
435
  /** Human-readable explanation, suitable for direct display. */
424
436
  message: string;
437
+ /**
438
+ * How much this affects trust in what is on screen. Drives the built-in
439
+ * warning indicator's colour and ordering; default `'warning'` when absent.
440
+ *
441
+ * - `'error'` — the map may be *positionally wrong* (dies drawn in the wrong
442
+ * place). Nothing downstream of the geometry can be trusted.
443
+ * - `'warning'` — something the viewer expected is absent or degraded, but what
444
+ * is drawn is correct.
445
+ * - `'info'` — worth knowing, no impact on correctness.
446
+ */
447
+ severity?: 'error' | 'warning' | 'info';
425
448
  /** Inference confidence in [0, 1] for the related quantity, when one exists. */
426
449
  confidence?: number;
427
450
  }
@@ -484,13 +507,21 @@ export interface WaferMapResult {
484
507
  warnings: WaferWarning[];
485
508
  /** Die population statistics. */
486
509
  dataCoverage: {
487
- /** Dies inside the wafer boundary that have at least one value or bin. */
510
+ /** Positioned dies inside the wafer boundary that have at least one value or bin. */
488
511
  filledDies: number;
489
- /** Total dies inside the wafer boundary (including partial). */
512
+ /** Total positioned dies inside the wafer boundary (including partial). Excludes unpositioned dies — see `unpositionedDies`. */
490
513
  totalDies: number;
491
- /** Dies falling within the edge exclusion zone. */
514
+ /** Positioned dies falling within the edge exclusion zone. */
492
515
  edgeExcludedDies: number;
493
- /** `filledDies / totalDies` in [0, 1]. */
516
+ /**
517
+ * Dies with no reported x/y position at all (see `Die.x`/`hasPosition`).
518
+ * Always present, `0` when every die is positioned. These dies are
519
+ * counted in `dies` and in every non-spatial stat (yield, bin counts,
520
+ * per-test analysis) but never in `totalDies`/`filledDies`/`ratio`
521
+ * above, and never drawn on the map/gallery canvas.
522
+ */
523
+ unpositionedDies: number;
524
+ /** `filledDies / totalDies` in [0, 1] — `0` when there are no positioned dies, even if `unpositionedDies` is large. */
494
525
  ratio: number;
495
526
  };
496
527
  /** Yield statistics computed against `passBins`. */
@@ -1 +1 @@
1
- import{createWafer as $}from"../core/wafer.js";import{isYieldEligibleDie as Q,getDieKey as I}from"../core/dies.js";import{applyOrientation as A,transformDies as Z}from"../core/transforms.js";import{affineRotation as ee,affineMirror as te,affineCompose as ne,affinePoint as ie}from"../core/transforms.js";import{inferWaferFromXY as se}from"../core/inference/wafer.js";import{resolveGridPitch as oe}from"../core/inference/pitch.js";import{assignGridIndices as re}from"../core/inference/grid.js";import{generateReticleGrid as ae}from"../core/reticle.js";import{buildView as T}from"./buildView.js";import{modeOf as fe}from"../core/utils.js";import{aggregateValues as L,aggregateBinCounts as de}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function ce(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],testDefs:t.testDefs,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function le(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function ue(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let o=1/0,r=-1/0,h=1/0,f=-1/0;for(const i of t)i.x<o&&(o=i.x),i.x>r&&(r=i.x),i.y<h&&(h=i.y),i.y>f&&(f=i.y);return{offsetX:Math.round((r+o)/2),offsetY:Math.round((f+h)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function he(t,n,e,o,r,h){if(h)return{colMidX:(Math.round(h.x)-n)*o,colMidY:(Math.round(h.y)-e)*r,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let f=1/0,i=-1/0,d=1/0,a=-1/0;for(const s of t){const c=Math.round(s.x)-n,p=Math.round(s.y)-e;c<f&&(f=c),c>i&&(i=c),p<d&&(d=p),p>a&&(a=p)}return{colMidX:(f+i)/2*o,colMidY:(d+a)/2*r,anchored:!1}}function pe(t,n,e){let o=1/0,r=-1/0,h=1/0,f=-1/0,i=0,d=0;for(const c of t){const p=Math.round(c.x)-n,u=Math.round(c.y)-e;i+=p,d+=u,p<o&&(o=p),p>r&&(r=p),u<h&&(h=u),u>f&&(f=u)}const a=t.length,s=(c,p,u)=>{const g=(u-p)/2;return g<=0?0:Math.abs(c/a-(p+u)/2)/g};return s(i,o,r)>.11||s(d,h,f)>.11}function me(t,n){let e=t?.xAxisDirection==="left",o=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(o=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:o}}function ye(t,n){const{results:e,method:o,targetBin:r}=t;if(o==="mean"||o==="median"||o==="stddev"||o==="min"||o==="max"||o==="count"){const h=new Set((n??[]).filter(s=>!isParametricTest(s)).map(s=>s.testNumber)),f=new Set;for(const s of e)for(const c of s)if(c.testValues)for(const p of Object.keys(c.testValues)){const u=Number(p);h.has(u)||f.add(u)}if(f.size===0)return L(e,o);const i=[...f],d=i.map(s=>L(e,o,s)),a=new Map;for(let s=0;s<i.length;s++){const c=i[s];for(const p of d[s]){const u=I(p);a.has(u)||a.set(u,{template:p,testValues:{}});const g=a.get(u),m=p.testValues?.[0];m!==void 0&&(g.testValues[c]=m)}}return[...a.values()].map(({template:s,testValues:c})=>({...s,testValues:Object.keys(c).length>0?c:void 0}))}if(o==="countBin"||o==="percent"){if(r===void 0)return[];const h=de(e,r,"hard");if(o==="countBin")return h;const f=e.length;return h.map(i=>({...i,testValues:{0:f>0?(i.testValues?.[0]??0)/f*100:0}}))}if(o==="mode"){const h=new Map;for(const i of e)for(const d of i){const a=I(d);h.has(a)||h.set(a,[]),h.get(a).push(d)}const f=[];for(const[i,d]of h){const a=i.split(","),s=Number(a[0]),c=Number(a[1]),p=d.map(g=>g.hbin).filter(g=>g!==void 0),u=fe(p);u!==null&&f.push({x:s,y:c,hbin:u})}return f}return[]}function W(t){const n=t.length,e=t.filter(r=>r.edgeExcluded).length,o=t.filter(r=>dieHasTestData(r)||r.hbin!==void 0||r.sbin!==void 0).length;return{filledDies:o,totalDies:n,edgeExcludedDies:e,ratio:n>0?o/n:0}}function N(t,n,e="exclude"){const o=new Set(n),h=t.filter(u=>!u.partial).filter(u=>u.edgeExcluded).length,f=t.filter(u=>u.partial).length;let i=0,d=0,a=!1;for(const u of t){if(!Q(u))continue;const g=u.hbin??u.sbin;g!==void 0&&(a=!0,o.has(g)?i++:d++)}const s=i+d,c=a&&s>0?i/s*100:null;let p=null;if(e==="denominator-only"){const u=s+h;p=a&&u>0?i/u*100:null}return{passDies:i,failDies:d,edgeExcludedDies:h,partialDies:f,totalDies:s,yieldPercent:c,yieldPercentGross:p}}function _(t,n,e,o,r,h=0,f=0,i=0,d=0,a=0,s=!1,c=!1){if(!t)return[];const p=t.anchorDie??{x:0,y:0},u=ae(n,{width:t.width,height:t.height,diePitchX:o,diePitchY:r,anchorDie:{x:p.x-h,y:p.y-f},gridOrigin:{x:-i,y:-d}}),g=ne(te(s,c,n.center.x,n.center.y),ee(a,n.center.x,n.center.y));return u.filter(m=>{const x=m.width/2,O=m.height/2,S=[[m.x-x,m.y-O],[m.x+x,m.y-O],[m.x+x,m.y+O],[m.x-x,m.y+O]].map(([y,C])=>ie(g,y,C)),R=Math.min(...S.map(y=>y.x)),Y=Math.max(...S.map(y=>y.x)),E=Math.min(...S.map(y=>y.y)),D=Math.max(...S.map(y=>y.y));return e.some(y=>y.physX>=R&&y.physX<Y&&y.physY>=E&&y.physY<D)})}function ge(t,n,e){const o=(n.radius-e)**2;return t.map(r=>{const h=r.physX-n.center.x,f=r.physY-n.center.y;return h*h+f*f>o?{...r,edgeExcluded:!0}:r})}function xe(t,n,e){const o=new Map;for(const i of t){let d=o.get(i.x);d||(d=new Map,o.set(i.x,d)),d.set(i.y,(d.get(i.y)??0)+1)}const r=new Set(e);function h(i,d){const a=i.hbin,s=d.hbin;if(a===void 0||s===void 0)return!1;const c=r.has(a),p=r.has(s);return n==="best"?p!==c?p:s<a:p!==c?c:s>a}const f=new Map;for(const i of t){const d=I(i),a=f.get(d);n==="first"&&a||(n==="best"||n==="worst")&&a&&!h(a,i)||f.set(d,i)}return Array.from(f.values()).map(i=>{const a=o.get(i.x)?.get(i.y)??1;return a>1?{...i,retestCount:a}:i})}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const o=t.testPass?.[n];if(o!==void 0)return o;if(e!==void 0&&!isParametricTest(e)){const r=t.testValues?.[n];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 G(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function K(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function z(t){return(t.warnings??[]).map(n=>({code:we(n),message:n,confidence:t.wafer.confidence}))}function we(t){return t.includes(j)?"geometry-conflict":t.includes(H)?"inferred-pitch":"partial-coverage"}const j="do not fit inside the supplied",H="The die pitch was inferred as";export function buildWaferMap(t,n){const e=ce(t),{debug:o,...r}=n??{},h=e.lotStackOpts?ye(e.lotStackOpts,e.testDefs):e.results;for(let l=0;l<Math.min(5,h.length);l++){const M=h[l];if(typeof M.x=="string"||typeof M.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 }")}const f=xe(h,e.retestPolicy,e.passBins),i={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let l=e.explicitDies;if(f.length>0){const J=new Map(f.map(P=>[I(P),P]));l=l.map(P=>{const X=J.get(I(P));return X?G(P,X):P})}const M=e.waferOpts?.diameter??300,b=$({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});b.orientation!==0&&(l=A(l,b));const v=_(e.reticleOpts,b,l,1,1,0,0,0,0,b.orientation),q=r.showReticle??e.reticleOpts!==void 0,V=T(b,l,{...r,reticles:v,showReticle:q,plotMode:K(f,r),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields});return{wafer:b,dies:l,view:V,reticleConfig:e.reticleOpts,units:"mm",inference:i,warnings:z(i),plotMode:V.plotMode,metadata:V.metadata,isLotStack:!1,dataCoverage:W(l),yield:N(l,e.passBins,e.edgeDieYieldMode),reticles:v,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const d=f.map(l=>({x:l.x,y:l.y})),a=oe(d,e.dieOpts,e.waferOpts?.diameter);i.diePitch={confidence:a.confidence,units:a.units};const{pitchX:s,pitchY:c}=a,p=a.units,u=le(f,e.dieOpts),g=re(d);i.grid={confidence:g.confidence};const{offsetX:m,offsetY:x}=ue(d,u,g),{flipX:O,flipY:S}=me(e.dieOpts,u),{colMidX:R,colMidY:Y,anchored:E}=he(d,m,x,s,c,e.waferOpts?.center);let D=e.waferOpts?.diameter;const y=d.map(l=>({x:(Math.round(l.x)-m)*s-R,y:(Math.round(l.y)-x)*c-Y})),C=y.length>0?Math.max(...y.map(({x:l,y:M})=>Math.hypot(Math.abs(l)+s/2,Math.abs(M)+c/2))):0;if(D===void 0)if(d.length>0)if(p==="mm"){const l=se(y,{minRadius:C});D=l.diameter,i.wafer={confidence:l.confidence,method:l.method}}else D=C*2,i.wafer={confidence:a.confidence*.8,method:"extent"};else D=p==="mm"?300:30,i.wafer={confidence:0,method:"default"};if(!E&&d.length>0&&pe(d,m,x)&&(i.wafer.method="inferred-partial",(i.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0){const l=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(l&&C>D/2+1e-9){const M=y.filter(({x:b,y:v})=>Math.hypot(Math.abs(b)+s/2,Math.abs(v)+c/2)>D/2+1e-9).length;(i.warnings??=[]).push(`${M} of ${y.length} probed die positions ${j} ${D} mm wafer at the supplied die pitch of ${s} \xD7 ${c} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(C*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}else l||(i.warnings??=[]).push(`${H} ${s.toFixed(3)} \xD7 ${c.toFixed(3)} mm by assuming the die grid fits within the ${D} mm wafer. The assumed aspect ratio may not match the true die shape whenever edge dies are absent from the data (a reticle-complete map, or partial dies filtered out upstream). Supply dieConfig.width and dieConfig.height \u2014 the die pitch, not the diameter, is what fixes placement.`)}const k=$({diameter:D,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata}),Me={width:s,height:c};let w=f.map(l=>{const M=Math.round(l.x)-m,b=Math.round(l.y)-x,v={id:`${M}_${b}`,x:M,y:b,physX:M*s-R,physY:b*c-Y,width:s,height:c,insideWafer:!0,partial:!1};return G(v,l)});(m!==0||x!==0)&&(w=w.map(l=>({...l,x:l.x+m,y:l.y+x,id:`${l.x+m}_${l.y+x}`}))),w=A(w,k),(O||S)&&(w=Z(w,{flipX:O,flipY:S},k.center)),e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(w=ge(w,k,e.waferOpts.edgeExclusion));const B=_(e.reticleOpts,k,w,s,c,m,x,R,Y,k.orientation,O,S),U=r.showReticle??e.reticleOpts!==void 0,F=T(k,w,{...r,reticles:B,showReticle:U,plotMode:K(f,r),testDefs:e.testDefs,dataAxisFlip:{x:O,y:S},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields});return{wafer:k,dies:w,view:F,reticleConfig:e.reticleOpts,units:p,inference:i,warnings:z(i),plotMode:F.plotMode,metadata:F.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:W(w),yield:N(w,e.passBins,e.edgeDieYieldMode),reticles:B,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:F.aggrMethod,lotSize:F.lotSize}}
1
+ import{createWafer as z}from"../core/wafer.js";import{isYieldEligibleDie as ae,getDieKey as B,hasPosition as S}from"../core/dies.js";import{applyOrientation as j,transformDies as fe}from"../core/transforms.js";import{affineRotation as de,affineMirror as ce,affineCompose as le,affinePoint as ue}from"../core/transforms.js";import{inferWaferFromXY as he}from"../core/inference/wafer.js";import{resolveGridPitch as pe}from"../core/inference/pitch.js";import{assignGridIndices as me}from"../core/inference/grid.js";import{generateReticleGrid as ye}from"../core/reticle.js";import{buildView as H}from"./buildView.js";import{modeOf as ge}from"../core/utils.js";import{aggregateValues as U,aggregateBinCounts as xe}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function we(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],testDefs:t.testDefs,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function be(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Me(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let u=1/0,d=-1/0,f=1/0,l=-1/0;for(const o of t)o.x<u&&(u=o.x),o.x>d&&(d=o.x),o.y<f&&(f=o.y),o.y>l&&(l=o.y);return{offsetX:Math.round((d+u)/2),offsetY:Math.round((l+f)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function De(t,n,e,u,d,f){if(f)return{colMidX:(Math.round(f.x)-n)*u,colMidY:(Math.round(f.y)-e)*d,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let l=1/0,o=-1/0,h=1/0,p=-1/0;for(const i of t){const c=Math.round(i.x)-n,s=Math.round(i.y)-e;c<l&&(l=c),c>o&&(o=c),s<h&&(h=s),s>p&&(p=s)}return{colMidX:(l+o)/2*u,colMidY:(h+p)/2*d,anchored:!1}}function Oe(t,n,e){let u=1/0,d=-1/0,f=1/0,l=-1/0,o=0,h=0;for(const c of t){const s=Math.round(c.x)-n,r=Math.round(c.y)-e;o+=s,h+=r,s<u&&(u=s),s>d&&(d=s),r<f&&(f=r),r>l&&(l=r)}const p=t.length,i=(c,s,r)=>{const m=(r-s)/2;return m<=0?0:Math.abs(c/p-(s+r)/2)/m};return i(o,u,d)>.11||i(h,f,l)>.11}function Se(t,n){let e=t?.xAxisDirection==="left",u=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(u=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:u}}function ke(t,n){const{method:e,targetBin:u}=t,d=t.results.map(f=>f.filter(S));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const f=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),l=new Set;for(const i of d)for(const c of i)if(c.testValues)for(const s of Object.keys(c.testValues)){const r=Number(s);f.has(r)||l.add(r)}if(l.size===0)return U(d,e);const o=[...l],h=o.map(i=>U(d,e,i)),p=new Map;for(let i=0;i<o.length;i++){const c=o[i];for(const s of h[i]){const r=B(s);p.has(r)||p.set(r,{template:s,testValues:{}});const m=p.get(r),y=s.testValues?.[0];y!==void 0&&(m.testValues[c]=y)}}return[...p.values()].map(({template:i,testValues:c})=>({...i,testValues:Object.keys(c).length>0?c:void 0}))}if(e==="countBin"||e==="percent"){if(u===void 0)return[];const f=xe(d,u,"hard");if(e==="countBin")return f;const l=d.length;return f.map(o=>({...o,testValues:{0:l>0?(o.testValues?.[0]??0)/l*100:0}}))}if(e==="mode"){const f=new Map;for(const o of d)for(const h of o){const p=B(h);f.has(p)||f.set(p,[]),f.get(p).push(h)}const l=[];for(const[o,h]of f){const p=o.split(","),i=Number(p[0]),c=Number(p[1]),s=h.map(m=>m.hbin).filter(m=>m!==void 0),r=ge(s);r!==null&&l.push({x:i,y:c,hbin:r})}return l}return[]}function q(t){const n=t.filter(S),e=n.length,u=n.filter(f=>f.edgeExcluded).length,d=n.filter(f=>dieHasTestData(f)||f.hbin!==void 0||f.sbin!==void 0).length;return{filledDies:d,totalDies:e,edgeExcludedDies:u,unpositionedDies:t.length-n.length,ratio:e>0?d/e:0}}function J(t,n,e="exclude"){const u=new Set(n),f=t.filter(r=>!r.partial).filter(r=>r.edgeExcluded).length,l=t.filter(r=>r.partial).length;let o=0,h=0,p=!1;for(const r of t){if(!ae(r))continue;const m=r.hbin??r.sbin;m!==void 0&&(p=!0,u.has(m)?o++:h++)}const i=o+h,c=p&&i>0?o/i*100:null;let s=null;if(e==="denominator-only"){const r=i+f;s=p&&r>0?o/r*100:null}return{passDies:o,failDies:h,edgeExcludedDies:f,partialDies:l,totalDies:i,yieldPercent:c,yieldPercentGross:s}}function Q(t,n,e,u,d,f=0,l=0,o=0,h=0,p=0,i=!1,c=!1){if(!t)return[];const s=t.anchorDie??{x:0,y:0},r=ye(n,{width:t.width,height:t.height,diePitchX:u,diePitchY:d,anchorDie:{x:s.x-f,y:s.y-l},gridOrigin:{x:-o,y:-h}}),m=le(ce(i,c,n.center.x,n.center.y),de(p,n.center.x,n.center.y));return r.filter(y=>{const k=y.width/2,w=y.height/2,b=[[y.x-k,y.y-w],[y.x+k,y.y-w],[y.x+k,y.y+w],[y.x-k,y.y+w]].map(([g,M])=>ue(m,g,M)),R=Math.min(...b.map(g=>g.x)),P=Math.max(...b.map(g=>g.x)),F=Math.min(...b.map(g=>g.y)),E=Math.max(...b.map(g=>g.y));return e.some(g=>g.physX>=R&&g.physX<P&&g.physY>=F&&g.physY<E)})}function ve(t,n,e){const u=(n.radius-e)**2;return t.map(d=>{const f=d.physX-n.center.x,l=d.physY-n.center.y;return f*f+l*l>u?{...d,edgeExcluded:!0}:d})}function Ce(t,n,e){const u=t.filter(S),d=t.filter(i=>!S(i)),f=new Map;for(const i of u){let c=f.get(i.x);c||(c=new Map,f.set(i.x,c)),c.set(i.y,(c.get(i.y)??0)+1)}const l=new Set(e);function o(i,c){const s=i.hbin,r=c.hbin;if(s===void 0||r===void 0)return!1;const m=l.has(s),y=l.has(r);return n==="best"?y!==m?y:r<s:y!==m?m:r>s}const h=new Map;for(const i of u){const c=B(i),s=h.get(c);n==="first"&&s||(n==="best"||n==="worst")&&s&&!o(s,i)||h.set(c,i)}return[...Array.from(h.values()).map(i=>{const s=f.get(i.x)?.get(i.y)??1;return s>1?{...i,retestCount:s}:i}),...d]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const u=t.testPass?.[n];if(u!==void 0)return u;if(e!==void 0&&!isParametricTest(e)){const d=t.testValues?.[n];if(d===0||d===1)return d===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function T(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function Z(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function ee(t){return(t.warnings??[]).map(n=>({code:Re(n),message:n,severity:"error",confidence:t.wafer.confidence}))}function Re(t){return t.includes(te)?"geometry-conflict":t.includes(ne)?"inferred-pitch":"partial-coverage"}const te="do not fit inside the supplied",ne="The die pitch was inferred as";export function buildWaferMap(t,n){const e=we(t),{debug:u,...d}=n??{},f=e.lotStackOpts?ke(e.lotStackOpts,e.testDefs):e.results;for(let a=0;a<Math.min(5,f.length);a++){const x=f[a];if(typeof x.x=="string"||typeof x.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const a of f)if(a.x==null!=(a.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(a.x)}, y=${String(a.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const l=Ce(f,e.retestPolicy,e.passBins),o={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let a=e.explicitDies;const x=l.filter(S),C=l.filter(A=>!S(A));if(x.length>0){const A=new Map(x.map(O=>[B(O),O]));a=a.map(O=>{const K=A.get(B(O));return K?T(O,K):O})}const V=e.waferOpts?.diameter??300,Y=z({diameter:V,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});Y.orientation!==0&&(a=j(a,Y));const G=Q(e.reticleOpts,Y,a,1,1,0,0,0,0,Y.orientation),oe=d.showReticle??e.reticleOpts!==void 0,L=H(Y,a,{...d,reticles:G,showReticle:oe,plotMode:Z(l,d),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),re=C.map((A,O)=>T({id:`unpositioned_${O}`,width:a[0]?.width??1,height:a[0]?.height??1},A)),_=[...a,...re];return{wafer:Y,dies:_,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:o,warnings:ee(o),plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:q(_),yield:J(_,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const h=l.filter(S),p=l.filter(a=>!S(a)),i=h.map(a=>({x:a.x,y:a.y})),c=pe(i,e.dieOpts,e.waferOpts?.diameter);o.diePitch={confidence:c.confidence,units:c.units};const{pitchX:s,pitchY:r}=c,m=c.units,y=be(h,e.dieOpts),k=me(i);o.grid={confidence:k.confidence};const{offsetX:w,offsetY:b}=Me(i,y,k),{flipX:R,flipY:P}=Se(e.dieOpts,y),{colMidX:F,colMidY:E,anchored:g}=De(i,w,b,s,r,e.waferOpts?.center);let M=e.waferOpts?.diameter;const I=i.map(a=>({x:(Math.round(a.x)-w)*s-F,y:(Math.round(a.y)-b)*r-E})),X=I.length>0?Math.max(...I.map(({x:a,y:x})=>Math.hypot(Math.abs(a)+s/2,Math.abs(x)+r/2))):0;if(M===void 0)if(i.length>0)if(m==="mm"){const a=he(I,{minRadius:X});M=a.diameter,o.wafer={confidence:a.confidence,method:a.method}}else M=X*2,o.wafer={confidence:c.confidence*.8,method:"extent"};else M=m==="mm"?300:30,o.wafer={confidence:0,method:"default"};if(!g&&i.length>0&&Oe(i,w,b)&&(o.wafer.method="inferred-partial",(o.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0){const a=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(a&&X>M/2+1e-9){const x=I.filter(({x:C,y:V})=>Math.hypot(Math.abs(C)+s/2,Math.abs(V)+r/2)>M/2+1e-9).length;(o.warnings??=[]).push(`${x} of ${I.length} probed die positions ${te} ${M} mm wafer at the supplied die pitch of ${s} \xD7 ${r} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(X*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}else a||(o.warnings??=[]).push(`${ne} ${s.toFixed(3)} \xD7 ${r.toFixed(3)} mm by assuming the die grid fits within the ${M} mm wafer. The assumed aspect ratio may not match the true die shape whenever edge dies are absent from the data (a reticle-complete map, or partial dies filtered out upstream). Supply dieConfig.width and dieConfig.height \u2014 the die pitch, not the diameter, is what fixes placement.`)}const v=z({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata}),Pe={width:s,height:r};let D=h.map(a=>{const x=Math.round(a.x)-w,C=Math.round(a.y)-b,V={id:`${x}_${C}`,x,y:C,physX:x*s-F,physY:C*r-E,width:s,height:r,insideWafer:!0,partial:!1};return T(V,a)});(w!==0||b!==0)&&(D=D.map(a=>({...a,x:a.x+w,y:a.y+b,id:`${a.x+w}_${a.y+b}`}))),D=j(D,v),(R||P)&&(D=fe(D,{flipX:R,flipY:P},v.center)),e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(D=ve(D,v,e.waferOpts.edgeExclusion));const N=Q(e.reticleOpts,v,D,s,r,w,b,F,E,v.orientation,R,P),ie=d.showReticle??e.reticleOpts!==void 0,$=H(v,D,{...d,reticles:N,showReticle:ie,plotMode:Z(l,d),testDefs:e.testDefs,dataAxisFlip:{x:R,y:P},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),se=p.map((a,x)=>T({id:`unpositioned_${x}`,width:s,height:r},a)),W=[...D,...se];return{wafer:v,dies:W,view:$,reticleConfig:e.reticleOpts,units:m,inference:o,warnings:ee(o),plotMode:$.plotMode,metadata:$.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:q(W),yield:J(W,e.passBins,e.edgeDieYieldMode),reticles:N,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:$.aggrMethod,lotSize:$.lotSize}}
@@ -1,4 +1,4 @@
1
- export { buildView, resolveTestNumber, findTestDef, getUniqueTestNumbers, generateTextOverlay, getDieKey, buildHoverText, buildMapTitle, } from './buildView.js';
1
+ export { buildView, getDieKey, buildHoverText, buildMapTitle, } from './buildView.js';
2
2
  export type { View, MapTitleParts, PlotMode, ViewRect, ViewText, ViewHoverPoint, ViewOverlay, ViewOptions, } from './buildView.js';
3
3
  export * from './buildWaferMap.js';
4
4
  export type { WaferMetadata, DieMetadata } from '../core/metadata.js';
@@ -1 +1 @@
1
- export{buildView,resolveTestNumber,findTestDef,getUniqueTestNumbers,generateTextOverlay,getDieKey,buildHoverText,buildMapTitle}from"./buildView.js";export*from"./buildWaferMap.js";export{hardBinColor,hardBinGreyscale,valueToViridis,valueToGreyscale,softBinColor,contrastTextColor}from"./colorMap.js";export*from"./colorSchemes.js";
1
+ export{buildView,getDieKey,buildHoverText,buildMapTitle}from"./buildView.js";export*from"./buildWaferMap.js";export{hardBinColor,hardBinGreyscale,valueToViridis,valueToGreyscale,softBinColor,contrastTextColor}from"./colorMap.js";export*from"./colorSchemes.js";
@@ -1 +1 @@
1
- import{analyzeWaferMap as m}from"./analyzeWaferMap.js";import{median as u}from"../core/utils.js";const y=1.3,h=2;function g(o,s){const r={info:0,notable:1,unusual:2};return r[o]>=r[s]?o:s}function w(o){const s=new Map;for(const l of o){const a=new Set;for(const i of l.summary.findings){const t=[i.variable.kind,i.variable.index??"",i.variable.bin??"",i.comparison.family,i.comparison.left,i.effect.direction].join("|");if(a.has(t))continue;a.add(t);const n=s.get(t)??{finding:i,waferIndices:[],severity:i.severity};n.waferIndices.push(l.waferIndex),n.severity=g(n.severity,i.severity),s.set(t,n)}}const r=[];for(const[l,a]of s){if(a.waferIndices.length<h)continue;const i=a.waferIndices.length/Math.max(1,o.length),t=i>=.6||a.severity==="unusual"?"unusual":"notable",n=a.finding;r.push({id:`lot-repeat:${l}`,level:"lot",severity:t,variable:{...n.variable},comparison:{...n.comparison},effect:{direction:n.effect.direction,absoluteDelta:n.effect.absoluteDelta,relativeDelta:n.effect.relativeDelta,effectSize:n.effect.effectSize},stats:{method:"wafer-finding-frequency",sampleSizeLeft:a.waferIndices.length,sampleSizeRight:o.length-a.waferIndices.length},summary:`${n.summary} \u2014 seen on ${a.waferIndices.length}/${o.length} wafers (${Math.round(i*100)}%)`,highlight:{kind:"wafer",waferIndices:a.waferIndices}})}return r}function b(o){const s=o.map(t=>({waferIndex:t.waferIndex,yieldPercent:t.summary.stats.yieldPercent})).filter(t=>t.yieldPercent!==null);if(s.length<3)return[];const r=s.map(t=>t.yieldPercent),l=u(r),a=u(r.map(t=>Math.abs(t-l)));if(!Number.isFinite(a)||a===0)return[];const i=[];for(const t of s){const n=.6745*(t.yieldPercent-l)/a;if(Math.abs(n)<y)continue;const e=t.yieldPercent-l;i.push({id:`inter-wafer:yield:${t.waferIndex}`,level:"inter-wafer",severity:Math.abs(n)>=2?"unusual":"notable",variable:{kind:"yield",label:"Yield"},comparison:{family:"wafer",left:`Wafer ${t.waferIndex+1}`,right:"Lot median"},effect:{direction:e>0?"higher":"lower",absoluteDelta:e,relativeDelta:l===0?void 0:e/l,effectSize:n},stats:{method:"mad-z-score",sampleSizeLeft:1,sampleSizeRight:s.length-1},summary:`Wafer ${t.waferIndex+1} yield is ${Math.abs(e).toFixed(1)} percentage points ${e>0?"higher":"lower"} than the lot median`,highlight:{kind:"wafer",waferIndices:[t.waferIndex]}})}return i}export function analyzeWaferLot(o,s={}){const r=o.map((e,f)=>({waferIndex:f,summary:s.perWaferSummaries?.[f]??m(e,s)})),l=[...w(r),...b(r)].sort((e,f)=>{const c=e.severity==="unusual"?2:e.severity==="notable"?1:0,d=f.severity==="unusual"?2:f.severity==="notable"?1:0;return c!==d?d-c:e.summary.localeCompare(f.summary,"en")}),a=r.find(e=>e.summary.wafer)?.summary.wafer,i=a?Object.fromEntries(Object.entries(a).filter(([e])=>e!=="wafer"&&e!=="waferId")):void 0,t=r.map(({waferIndex:e,summary:f})=>({waferIndex:e,yieldPercent:f.stats.yieldPercent})),n=r.map(({waferIndex:e,summary:f})=>f.stats.perTestStats?.length?{waferIndex:e,tests:f.stats.perTestStats}:null).filter(e=>e!==null);return{level:"lot",hasNotableFindings:l.some(e=>e.severity!=="info")||r.some(e=>e.summary.hasNotableFindings),findings:l,lot:Object.keys(i??{}).length>0?i:void 0,stats:{waferCount:o.length},lotYieldSeries:t,perWafer:r,...n.length>0?{perWaferTestStats:n}:{}}}
1
+ import{analyzeWaferMap as m}from"./analyzeWaferMap.js";import{median as u}from"../core/utils.js";const y=1.3,h=2;function g(o,s){const i={info:0,notable:1,unusual:2};return i[o]>=i[s]?o:s}function b(o){const s=new Map;for(const r of o){const a=new Set,f=new Set(r.summary.findings.flatMap(e=>e.absorbedIds??[]));for(const e of r.summary.findings){if(f.has(e.id))continue;const n=[e.variable.kind,e.variable.index??"",e.variable.bin??"",e.comparison.family,e.comparison.left,e.effect.direction].join("|");if(a.has(n))continue;a.add(n);const t=s.get(n)??{finding:e,waferIndices:[],severity:e.severity};t.waferIndices.push(r.waferIndex),t.severity=g(t.severity,e.severity),s.set(n,t)}}const i=[];for(const[r,a]of s){if(a.waferIndices.length<h)continue;const f=a.waferIndices.length/Math.max(1,o.length),e=f>=.6||a.severity==="unusual"?"unusual":"notable",n=a.finding;i.push({id:`lot-repeat:${r}`,level:"lot",severity:e,variable:{...n.variable},comparison:{...n.comparison},effect:{direction:n.effect.direction,absoluteDelta:n.effect.absoluteDelta,relativeDelta:n.effect.relativeDelta,effectSize:n.effect.effectSize},stats:{method:"wafer-finding-frequency",sampleSizeLeft:a.waferIndices.length,sampleSizeRight:o.length-a.waferIndices.length},summary:`${n.summary} \u2014 seen on ${a.waferIndices.length}/${o.length} wafers (${Math.round(f*100)}%)`,highlight:{kind:"wafer",waferIndices:a.waferIndices}})}return i}function w(o){const s=o.map(e=>({waferIndex:e.waferIndex,yieldPercent:e.summary.stats.yieldPercent})).filter(e=>e.yieldPercent!==null);if(s.length<3)return[];const i=s.map(e=>e.yieldPercent),r=u(i),a=u(i.map(e=>Math.abs(e-r)));if(!Number.isFinite(a)||a===0)return[];const f=[];for(const e of s){const n=.6745*(e.yieldPercent-r)/a;if(Math.abs(n)<y)continue;const t=e.yieldPercent-r;f.push({id:`inter-wafer:yield:${e.waferIndex}`,level:"inter-wafer",severity:Math.abs(n)>=2?"unusual":"notable",variable:{kind:"yield",label:"Yield"},comparison:{family:"wafer",left:`Wafer ${e.waferIndex+1}`,right:"Lot median"},effect:{direction:t>0?"higher":"lower",absoluteDelta:t,relativeDelta:r===0?void 0:t/r,effectSize:n},stats:{method:"mad-z-score",sampleSizeLeft:1,sampleSizeRight:s.length-1},summary:`Wafer ${e.waferIndex+1} yield is ${Math.abs(t).toFixed(1)} percentage points ${t>0?"higher":"lower"} than the lot median`,highlight:{kind:"wafer",waferIndices:[e.waferIndex]}})}return f}export function analyzeWaferLot(o,s={}){const i=o.map((t,l)=>({waferIndex:l,summary:s.perWaferSummaries?.[l]??m(t,s)})),r=[...b(i),...w(i)].sort((t,l)=>{const c=t.severity==="unusual"?2:t.severity==="notable"?1:0,d=l.severity==="unusual"?2:l.severity==="notable"?1:0;return c!==d?d-c:t.summary.localeCompare(l.summary,"en")}),a=i.find(t=>t.summary.wafer)?.summary.wafer,f=a?Object.fromEntries(Object.entries(a).filter(([t])=>t!=="wafer"&&t!=="waferId")):void 0,e=i.map(({waferIndex:t,summary:l})=>({waferIndex:t,yieldPercent:l.stats.yieldPercent})),n=i.map(({waferIndex:t,summary:l})=>l.stats.perTestStats?.length?{waferIndex:t,tests:l.stats.perTestStats}:null).filter(t=>t!==null);return{level:"lot",hasNotableFindings:r.some(t=>t.severity!=="info")||i.some(t=>t.summary.hasNotableFindings),findings:r,lot:Object.keys(f??{}).length>0?f:void 0,stats:{waferCount:o.length},lotYieldSeries:e,perWafer:i,...n.length>0?{perWaferTestStats:n}:{}}}
@@ -1 +1 @@
1
- import{isYieldEligibleDie as X,getDieKey as T}from"../core/dies.js";import{buildWaferMap as De,getTestPassStatus as ue,isParametricTest as q}from"../renderer/buildWaferMap.js";import{buildQuadrantRegions as $e,buildReticlePositionRegions as ke,buildRingRegions as ze,buildSectorRegions as ve,buildTestSiteRegions as Me,sectorCompassNames as de,areQuadrantsAdjacent as Re,parseRegionKey as j}from"./regions.js";import{buildClusterFindings as Be}from"./clusterDetection.js";import{classifyPattern as Ve}from"./patternClassification.js";import{normalCdf as ge}from"./math.js";import{quantile as Z}from"./math.js";import{mean as G,clamp01 as he}from"../core/utils.js";const Fe={ringCount:4,passBins:[1],significanceLevel:.05,minimumEffectSize:.2,minimumRelativeEffect:1,minimumSampleSize:5,includePartial:!1,includeEdgeExcluded:!1,enableYieldAnalysis:!0,enableHardBinAnalysis:!0,enableSoftBinAnalysis:!0,enableTestValueAnalysis:!1,computePerTestStats:!1,enableReticlePositionAnalysis:!0,enableClusterAnalysis:!0,enableAngularAnalysis:!0,sectorCount:8,enablePatternClassification:!0,minimumClusterSize:5};function Pe(s,r){const o={...s};return o.minimumClusterSize=Math.max(5,Math.round(r*.003)),o}function Le(s){return"wafer"in s&&"dies"in s&&"view"in s?s:De(s)}function Ne(s,r){return X(s,r)?s.hbin!==void 0||s.sbin!==void 0||s.testValues!==void 0&&Object.keys(s.testValues).length>0:!1}function Ce(s,r,o){if(!s||r)return;const e=(o??[]).filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(e.length!==0)return n=>{for(const l of e){const a=l.testNumber,i=n.testValues?.[a];if(i!==void 0&&(l.limitLow!==void 0&&i<l.limitLow||l.limitHigh!==void 0&&i>l.limitHigh))return!0}return!1}}function Ae(s,r,o){const e=new Set,n=new Set,l=new Set,a=new Map,i=new Map;for(const t of s){if(t.testValues)for(const c of Object.keys(t.testValues))e.add(Number(c));if(t.testPass)for(const c of Object.keys(t.testPass))e.add(Number(c));t.hbin!==void 0&&n.add(t.hbin),t.sbin!==void 0&&l.add(t.sbin),X(t)&&(t.hbin!==void 0&&a.set(t.hbin,(a.get(t.hbin)??0)+1),t.sbin!==void 0&&i.set(t.sbin,(i.get(t.sbin)??0)+1))}return{totalDies:s.length,analyzedDies:r,excludedDies:s.length-r,yieldPercent:o,testsConsidered:[...e].sort((t,c)=>t-c),hardBinsConsidered:[...n].sort((t,c)=>t-c),softBinsConsidered:[...l].sort((t,c)=>t-c),...a.size?{hardBinCounts:Object.fromEntries(a)}:{},...i.size?{softBinCounts:Object.fromEntries(i)}:{}}}function Ke(s,r){if(!r?.length)return;const o=r.filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(!o.length)return;const e=[];for(const n of o){const l=n.testNumber;let a=0,i=0,t=0,c=0;for(const u of s){if(u.partial||u.edgeExcluded)continue;const b=u.testValues?.[l];b!==void 0&&(c++,n.limitLow!==void 0&&b<n.limitLow?i++:n.limitHigh!==void 0&&b>n.limitHigh?t++:a++)}e.push({testNumber:l,label:n.name,passDies:a,failLowDies:i,failHighDies:t,totalDies:c,yieldPercent:c>0?a/c*100:null})}return e.length?e:void 0}export function computeFunctionalYield(s,r){if(!r?.length)return;const o=r.filter(n=>!q(n));if(!o.length)return;const e=[];for(const n of o){const l=n.testNumber;let a=0,i=0;for(const c of s){if(c.partial||c.edgeExcluded)continue;const u=ue(c,l,n);u!==void 0&&(u?a++:i++)}const t=a+i;e.push({testNumber:l,label:n.name,passDies:a,failDies:i,totalDies:t,passRatePercent:t>0?a/t*100:null})}return e.length?e:void 0}function Ee(s,r,o,e){const n=[];for(const l of r){const a=[];for(const b of s){if(b.partial||b.edgeExcluded)continue;const g=b.testValues?.[l];g!==void 0&&a.push(g)}if(a.length<e)continue;const i=G(a),t=Math.sqrt(re(a,i)),c=a.slice().sort((b,g)=>b-g),u=o?.find(b=>b.testNumber===l)?.name??String(l);n.push({testNumber:l,label:u,count:a.length,min:c[0],max:c[c.length-1],mean:i,stddev:t,median:Z(c,.5),q1:Z(c,.25),q3:Z(c,.75)})}return n.length?n:void 0}function H(s,r,o,e){const n=(s+o)/(r+e),l=n*(1-n)*(1/r+1/e);if(!Number.isFinite(l)||l<=0)return 1;const a=(s/r-o/e)/Math.sqrt(l);return he(2*(1-ge(Math.abs(a))))}function ee(s){const r=new Map;for(const o of s){const e=`${o.variable.kind}:${o.comparison.family}`,n=r.get(e)??[];n.push(o),r.set(e,n)}for(const o of r.values()){const e=[...o].filter(a=>a.stats.pValue!==void 0).sort((a,i)=>(a.stats.pValue??1)-(i.stats.pValue??1)),n=e.length;let l=1;for(let a=n-1;a>=0;a--){const i=e[a],t=i.stats.pValue??1,c=Math.min(1,Math.min(l,t*n/(a+1)));i.stats.adjustedPValue=c,l=c}}return s}function te(s,r,o){const e=Math.abs(r),n=o!==void 0?Math.abs(o):0;return s<=.01&&(e>=.3||n>=2.5)?"unusual":s<=.05&&(e>=.2||n>=1.5)?"notable":"info"}function ne(s,r){return s<=.01&&Math.abs(r)>=.5?"unusual":s<=.05&&Math.abs(r)>=.15?"notable":"info"}function J(s){return s==="reticle-position"?"other reticle positions":s==="test-site"?"other test sites":"the rest of the map"}function O(s){return s==="reticle-position"?"Other reticle positions":s==="test-site"?"Other test sites":"Rest of map"}function me(s,r,o){const e=(Math.abs(r)*100).toFixed(1);return`${s} yield is ${e} percentage points ${r>0?"higher":"lower"} than ${J(o)}`}function ie(s,r){return r==="quadrant"&&!s.startsWith("Quadrant")?`quadrant ${s}`:s}function pe(s,r,o,e){const n=ie(s,e),l=(Math.abs(o)*100).toFixed(1);return`${n} has ${r} occurrence ${l} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function xe(s,r,o,e){const n=ie(s,e),l=(Math.abs(o)*100).toFixed(1);return`${n} has ${r} pass rate ${l} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function be(s,r,o,e,n,l){const a=ie(s,n),i=J(n),t=o>0?"higher":"lower";if(e!==void 0&&Number.isFinite(e)){const u=(Math.abs(e)*100).toFixed(1);return`${a} mean ${r} is ${u}% ${t} than ${i}`}const c=l?` ${l}`:"";return`${a} mean ${r} is ${Math.abs(o).toPrecision(3)}${c} ${t} than ${i}`}function ye(s,r,o){const e=r?.find(n=>n.bin===s);return e?.name?`${o} ${s} (${e.name})`:`${o} ${s}`}function Te(s,r){const o=r?.find(e=>e.testNumber===s);return{label:o?.name??`Test ${s}`,unit:o?.unit}}function se(s,r){const o=new Map(s.map(n=>[T(n),n])),e=new Map;for(const n of r){const l=[];for(const a of n.dieKeys){const i=o.get(a);i&&l.push(i)}e.set(n.key,l)}return e}function oe(s,r){return ee(s),s.filter(o=>{const e=o.stats.adjustedPValue??o.stats.pValue??1,n=Math.abs(o.effect.absoluteDelta??0),l=Math.abs(o.effect.relativeDelta??0);return e<=r.significanceLevel&&(n>=r.minimumEffectSize||l>=r.minimumRelativeEffect)}).map(o=>({...o,severity:te(o.stats.adjustedPValue??o.stats.pValue??1,o.effect.absoluteDelta??0,o.effect.relativeDelta)}))}function Y(s,r,o,e){const n=new Set(o),l=se(s,r),a=new Map,i=new Map;for(const[c,u]of l){let b=0,g=0;for(const d of u)d.hbin!==void 0&&(g++,n.has(d.hbin)&&b++);a.set(c,b),i.set(c,g)}const t=[];for(const c of r){const u=i.get(c.key)??0,b=a.get(c.key)??0;let g=0,d=0;for(const[$,p]of i)$!==c.key&&(g+=p,d+=a.get($)??0);if(u<e.minimumSampleSize||g<e.minimumSampleSize)continue;const h=b/u,m=d/g,S=h-m,z=H(b,u,d,g);t.push({id:`yield:${c.key}`,level:"wafer",severity:"info",variable:{kind:"yield",label:"Yield"},comparison:{family:c.family,left:c.label,right:O(c.family)},effect:{direction:S===0?"different":S>0?"higher":"lower",absoluteDelta:S,relativeDelta:m===0?void 0:S/m,effectSize:S},stats:{method:"two-proportion-z",pValue:z,sampleSizeLeft:u,sampleSizeRight:g},summary:me(c.label,S,c.family),highlight:{kind:"region",regionFamily:c.family,regionKeys:[c.key],dieKeys:[...c.dieKeys]}})}return oe(t,e)}function L(s,r,o,e,n,l){const a=d=>o==="soft"?d.sbin:d.hbin,i=[...new Set(s.map(a).filter(d=>d!==void 0))].sort((d,h)=>d-h),t=se(s,r),c=new Map,u=new Map;for(const[d,h]of t){const m=new Map;for(const S of h){const z=a(S);z!==void 0&&m.set(z,(m.get(z)??0)+1)}c.set(d,m),u.set(d,h.length)}const b=[],g=n==="hardBin"?"HBin":"SBin";for(const d of r){const h=u.get(d.key)??0,m=c.get(d.key);let S=0;const z=new Map;for(const[$,p]of c)if($!==d.key){S+=u.get($)??0;for(const[w,k]of p)z.set(w,(z.get(w)??0)+k)}if(!(h<l.minimumSampleSize||S<l.minimumSampleSize))for(const $ of i){const p=m.get($)??0,w=z.get($)??0,k=p/h,f=w/S,y=k-f,v=H(p,h,w,S),D=ye($,e,g);b.push({id:`${n}:${$}:${d.key}`,level:"wafer",severity:"info",variable:{kind:n,bin:$,label:D},comparison:{family:d.family,left:d.label,right:O(d.family)},effect:{direction:y===0?"different":y>0?"higher":"lower",absoluteDelta:y,relativeDelta:f===0?void 0:y/f,effectSize:y},stats:{method:"two-proportion-z",pValue:v,sampleSizeLeft:h,sampleSizeRight:S},summary:pe(d.label,D,y,d.family),highlight:{kind:"bin",bin:$,regionKeys:[d.key],dieKeys:[...d.dieKeys]}})}}return oe(b,l)}function Q(s,r,o,e){const n=(o??[]).filter(u=>!q(u));if(!n.length)return[];const l=n.map(u=>u.testNumber),a=se(s,r),i=new Map,t=new Map;for(const[u,b]of a){const g=new Uint32Array(n.length),d=new Uint32Array(n.length);for(const h of b)for(let m=0;m<n.length;m++){const S=ue(h,l[m],n[m]);S!==void 0&&(d[m]++,S&&g[m]++)}i.set(u,g),t.set(u,d)}const c=[];for(const u of r){const b=i.get(u.key),g=t.get(u.key);for(let d=0;d<n.length;d++){let h=0,m=0;for(const[y,v]of t)y!==u.key&&(m+=v[d],h+=i.get(y)[d]);const S=g[d],z=b[d];if(S<e.minimumSampleSize||m<e.minimumSampleSize)continue;const $=z/S,p=h/m,w=$-p,k=H(z,S,h,m),f=l[d];c.push({id:`functional:${f}:${u.key}`,level:"wafer",severity:"info",variable:{kind:"functionalTest",index:f,label:`${n[d].name} pass rate`},comparison:{family:u.family,left:u.label,right:O(u.family)},effect:{direction:w===0?"different":w>0?"higher":"lower",absoluteDelta:w,relativeDelta:p===0?void 0:w/p,effectSize:w},stats:{method:"two-proportion-z",pValue:k,sampleSizeLeft:S,sampleSizeRight:m},summary:xe(u.label,n[d].name,w,u.family),highlight:{kind:"region",regionFamily:u.family,regionKeys:[u.key],dieKeys:[...u.dieKeys]}})}}return oe(c,e)}function re(s,r){return s.length<2?0:s.reduce((o,e)=>o+(e-r)**2,0)/(s.length-1)}function Se(s,r,o,e,n,l){const a=Math.sqrt(o/s+l/e),i=r-n;if(!Number.isFinite(a)||a===0)return{pValue:1,effectSize:0,delta:i};const t=i/a,c=Math.sqrt(Math.max(0,(o+l)/2)),u=c===0?i:i/c;return{pValue:he(2*(1-ge(Math.abs(t)))),effectSize:u,delta:i}}function qe(s,r){const o=G(s),e=G(r);return Se(s.length,o,re(s,o),r.length,e,re(r,e))}const ae=250;function we(s,r,o){const e=a=>a.filter(i=>q(o?.find(t=>t.testNumber===i)));if(r)return{testNumbers:e(r.slice().sort((a,i)=>a-i))};const n=new Set;let l=!1;e:for(const a of s)for(const i of Object.keys(a.testValues??{})){const t=Number(i);if(!n.has(t)&&(n.add(t),n.size>ae)){l=!0;break e}}if(l){const a=`[wafermap] analyzeWaferMap: more than ${ae} tests found in die data. Pass testNumbers: [...] in options to enable test value analysis for specific tests. Auto-cap threshold is ${ae}.`;return console.warn(a),{testNumbers:[],warning:a}}return{testNumbers:e([...n].sort((a,i)=>a-i))}}function _(s,r,o,e){const n=we(s,e.testNumbers,o);if(n.warning)return{findings:[],warning:n.warning};const l=n.testNumbers,a=new Map;for(let h=0;h<r.length;h++)for(const m of r[h].dieKeys)a.set(m,h);const i=r.length,t=[],c=[];for(const h of s){const m=a.get(T(h));m!==void 0&&(t.push(h),c.push(m))}const u=[],b=new Float64Array(i),g=new Float64Array(i),d=new Float64Array(i);for(const h of l){b.fill(0),g.fill(0),d.fill(0);let m=0,S=0,z=0,$;for(let p=0;p<t.length;p++){const k=t[p].testValues?.[h];if(k===void 0)continue;$===void 0&&($=k);const f=k-$,y=c[p];b[y]+=1,g[y]+=f,d[y]+=f*f,m+=1,S+=f,z+=f*f}if($!==void 0)for(let p=0;p<i;p++){const w=b[p],k=m-w;if(w<e.minimumSampleSize||k<e.minimumSampleSize)continue;const f=g[p]/w,v=(S-g[p])/k,D=f+$,M=v+$,V=w>1?Math.max(0,d[p]-w*f*f)/(w-1):0,R=k>1?Math.max(0,z-d[p]-k*v*v)/(k-1):0,{pValue:C,effectSize:P,delta:A}=Se(w,D,V,k,M,R),F=r[p],{label:W,unit:U}=Te(h,o),x=M!==0?A/Math.abs(M):void 0;u.push({id:`test:${h}:${F.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:h,label:W,unit:U},comparison:{family:F.family,left:F.label,right:O(F.family)},effect:{direction:A===0?"different":A>0?"higher":"lower",absoluteDelta:A,relativeDelta:x,effectSize:P},stats:{method:"welch-z-approx",pValue:C,sampleSizeLeft:w,sampleSizeRight:k},summary:be(F.label,W,A,x,F.family,U),highlight:{kind:"region",regionFamily:F.family,regionKeys:[F.key],dieKeys:[...F.dieKeys]}})}}return ee(u),{activeTestNumbers:l,findings:u.filter(h=>{const m=h.stats.adjustedPValue??h.stats.pValue??1,S=Math.abs(h.effect.effectSize??0);return m<=e.significanceLevel&&S>=e.minimumEffectSize}).map(h=>({...h,severity:ne(h.stats.adjustedPValue??h.stats.pValue??1,h.effect.effectSize??0)}))}}function je(s,r,o,e){if(!o?.length)return[];const n=o.filter(i=>q(i)&&(i.limitLow!==void 0||i.limitHigh!==void 0));if(!n.length)return[];const l=[],a=new Map(s.map(i=>[T(i),i]));for(const i of n){const t=i.testNumber;for(const c of r){const u=new Map;for(const g of c){const d=[];for(const h of g.dieKeys){const m=a.get(h);m&&d.push(m)}u.set(g.key,d)}const b=[];for(const g of c){const d=u.get(g.key),h=[];for(const[D,M]of u)if(D!==g.key)for(const V of M)h.push(V);const m=D=>D.testValues?.[t]!==void 0,S=D=>{const M=D.testValues?.[t];return M===void 0?!1:i.limitLow!==void 0&&M<i.limitLow||i.limitHigh!==void 0&&M>i.limitHigh},z=d.filter(m),$=h.filter(m);if(z.length<e.minimumSampleSize||$.length<e.minimumSampleSize)continue;const p=z.filter(S).length,w=$.filter(S).length,k=p/z.length,f=w/$.length,y=k-f,v=H(p,z.length,w,$.length);b.push({id:`specLimit:${t}:${g.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:t,label:i.name,unit:i.unit},comparison:{family:g.family,left:g.label,right:O(g.family)},effect:{direction:y===0?"different":y>0?"higher":"lower",absoluteDelta:y,relativeDelta:f===0?void 0:y/f,effectSize:y},stats:{method:"two-proportion-z",pValue:v,sampleSizeLeft:z.length,sampleSizeRight:$.length},summary:`${g.label} spec-fail rate for ${i.name} is ${(Math.abs(y)*100).toFixed(1)} pp ${y>0?"higher":"lower"} than the rest of the wafer`,highlight:{kind:"region",regionFamily:g.family,regionKeys:[g.key],dieKeys:[...g.dieKeys]}})}ee(b),l.push(...b.filter(g=>{const d=g.stats.adjustedPValue??g.stats.pValue??1,h=Math.abs(g.effect.absoluteDelta??0),m=Math.abs(g.effect.relativeDelta??0);return d<=e.significanceLevel&&(h>=e.minimumEffectSize||m>=e.minimumRelativeEffect)}).map(g=>({...g,severity:ne(g.stats.adjustedPValue??g.stats.pValue??1,g.effect.effectSize??0)})))}}return l}function E(s){return s.highlight.regionKeys??[]}function He(s){const r=s.variable.bin??s.variable.index??"";return`${s.comparison.family}\0${s.variable.kind}\0${r}\0${s.effect.direction}`}function I(s,r){const o=E(s)[0]??"",e=j(o);return e.family==="ring"?e.ring??0:e.family==="sector"?de(r).indexOf(e.sector??""):0}function Oe(s,r,o){if(r==="quadrant"){const a=new Map;for(const u of s){const b=j(E(u)[0]??"").quadrant;b&&a.set(b,u)}const i=[...a.keys()],t=new Set,c=[];for(const u of i){if(t.has(u))continue;const b=[],g=[u];for(t.add(u);g.length;){const d=g.pop();b.push(d);for(const h of i)!t.has(h)&&Re(d,h)&&(t.add(h),g.push(h))}c.push(b.map(d=>a.get(d)))}return c}const e=[...s].sort((a,i)=>I(a,o)-I(i,o)),n=[];let l=[];for(const a of e){if(l.length===0){l.push(a);continue}const i=I(l[l.length-1],o);I(a,o)-i===1?l.push(a):(n.push(l),l=[a])}if(l.length&&n.push(l),r==="sector"&&n.length>1){const i=de(o).length,t=I(n[0][0],o),c=I(n[n.length-1][n[n.length-1].length-1],o);if(t===0&&c===i-1){const u=n.pop();n[0]=[...u,...n[0]]}}return n}function Ie(s,r){const o=s.map(a=>j(E(a)[0]??"").ring).filter(a=>a!==void 0).sort((a,i)=>a-i),e=o[0],n=o[o.length-1];let l="";return e===n&&e===1&&r>1?l=" (core)":e===n&&e===r&&r>1&&(l=" (edge)"),e===n?`Ring ${e}${l}`:`Rings ${e}\u2013${n}`}function Ye(s,r){const o=s.map(e=>j(E(e)[0]??"").sector??"");return o.length===1?`Sector ${o[0]}`:`Sectors ${o[0]}\u2013${o[o.length-1]}`}function Qe(s){const r=s.map(o=>j(E(o)[0]??"").quadrant??"");return r.length===1?r[0]:r.length===2?`Quadrants ${r[0]} & ${r[1]}`:`Quadrants ${r.slice(0,-1).join(", ")} & ${r[r.length-1]}`}function le(s){return[...new Set(s)]}function _e(s,r){const o=s[0],e=o.comparison.family,n=o.variable.kind,l=le(s.flatMap(p=>p.highlight.dieKeys??[])),a=le(s.flatMap(E)),i=new Set(l),t=e==="ring"?Ie(s,r.ringCount):e==="sector"?Ye(s,r.sectorCount):Qe(s),c=n==="softBin"?r.softEligibleDies:n==="test"?r.testDies:r.eligibleDies,u=c.filter(p=>i.has(T(p))),b=c.filter(p=>!i.has(T(p)));let g,d,h,m,S;if(n==="test"){const p=o.variable.index,w=R=>R.testValues?.[p],k=u.map(w).filter(R=>R!==void 0),f=b.map(w).filter(R=>R!==void 0),{pValue:y,effectSize:v,delta:D}=qe(k,f),M=G(f),V=M!==0?D/Math.abs(M):void 0;g={direction:D===0?"different":D>0?"higher":"lower",absoluteDelta:D,relativeDelta:V,effectSize:v},d={method:"welch-z-approx",pValue:y,sampleSizeLeft:k.length,sampleSizeRight:f.length},h=ne(y,v),m=be(t,o.variable.label,D,V,e,o.variable.unit),S=`test:${p}`}else if(n==="yield"){const p=new Set(r.passBins),w=u.filter(P=>P.hbin!==void 0),k=b.filter(P=>P.hbin!==void 0),f=w.length,y=k.length,v=w.filter(P=>p.has(P.hbin)).length,D=k.filter(P=>p.has(P.hbin)).length,M=f>0?v/f:0,V=y>0?D/y:0,R=M-V,C=H(v,f,D,y);g={direction:R===0?"different":R>0?"higher":"lower",absoluteDelta:R,relativeDelta:V===0?void 0:R/V,effectSize:R},d={method:"two-proportion-z",pValue:C,sampleSizeLeft:f,sampleSizeRight:y},h=te(C,R,g.relativeDelta),m=me(t,R,e),S="yield"}else{const p=o.variable.bin,w=R=>n==="softBin"?R.sbin:R.hbin,k=u.filter(R=>w(R)===p).length,f=b.filter(R=>w(R)===p).length,y=k/u.length,v=f/b.length,D=y-v,M=H(k,u.length,f,b.length);g={direction:D===0?"different":D>0?"higher":"lower",absoluteDelta:D,relativeDelta:v===0?void 0:D/v,effectSize:D},d={method:"two-proportion-z",pValue:M,sampleSizeLeft:u.length,sampleSizeRight:b.length},h=te(M,D,g.relativeDelta);const V=n==="softBin"?r.sbinDefs:r.hbinDefs;m=pe(t,ye(p,V,n==="softBin"?"SBin":"HBin"),D,e),S=`${n}:${p}`}const z=le(a.map(p=>p.replace(`${e}:`,""))).join("-"),$=n==="hardBin"||n==="softBin"?{kind:"bin",bin:o.variable.bin,regionKeys:a,dieKeys:l}:{kind:"region",regionFamily:e,regionKeys:a,dieKeys:l};return{...o,id:`${S}:${e}:${z}`,severity:h,comparison:{family:e,left:t,right:O(e)},effect:g,stats:d,summary:m,highlight:$,relatedIds:s.map(p=>p.id)}}function We(s,r){const o=new Set(["ring","quadrant","sector"]),e=[],n=new Map;for(const a of s){if(!o.has(a.comparison.family)){e.push(a);continue}const i=He(a),t=n.get(i)??[];t.push(a),n.set(i,t)}const l=[];for(const a of n.values()){if(a.length===1){l.push(a[0]);continue}const i=a[0].comparison.family;for(const t of Oe(a,i,r.sectorCount))l.push(t.length===1?t[0]:_e(t,r))}return[...e,...l]}export function analyzeWaferMap(s,r={}){const o={...Fe,...r},e=Le(s),n=e.isLotStack,l=e.aggrMethod,a=!n||l==="mode"||l==="countBin"||l==="percent",i=e.dies.filter(f=>Ne(f,o)),t=Pe(o,i.length),c=e.dies.filter(f=>X(f,t)),u=ze(c,e.wafer,t.ringCount),b=$e(c,e.wafer,t.ringCount),g=t.enableReticlePositionAnalysis?ke(c,e.reticleConfig):[],d=t.enableTestSiteAnalysis===!1?[]:Me(c,t.enableTestSiteAnalysis===!0),h=t.enableAngularAnalysis?ve(c,e.wafer,t.sectorCount):[],m=[];if(t.enableYieldAnalysis&&a&&m.push(...Y(i,u,t.passBins,t),...Y(i,b,t.passBins,t),...Y(i,g,t.passBins,t),...Y(i,d,t.passBins,t),...Y(i,h,t.passBins,t)),t.enableHardBinAnalysis&&a&&m.push(...L(i,u,"hard",e.hbinDefs,"hardBin",t),...L(i,b,"hard",e.hbinDefs,"hardBin",t),...L(i,g,"hard",e.hbinDefs,"hardBin",t),...L(i,d,"hard",e.hbinDefs,"hardBin",t),...L(i,h,"hard",e.hbinDefs,"hardBin",t)),t.enableSoftBinAnalysis){const f=i.filter(y=>y.sbin!==void 0);m.push(...L(f,u,"soft",e.sbinDefs,"softBin",t),...L(f,b,"soft",e.sbinDefs,"softBin",t),...L(f,g,"soft",e.sbinDefs,"softBin",t),...L(f,d,"soft",e.sbinDefs,"softBin",t),...L(f,h,"soft",e.sbinDefs,"softBin",t))}const S=[];let z;if(t.enableTestValueAnalysis){const f=_(i,u,e.testDefs,t),y=_(i,b,e.testDefs,t),v=_(i,g,e.testDefs,t),D=_(i,d,e.testDefs,t),M=_(i,h,e.testDefs,t);m.push(...f.findings,...y.findings,...v.findings,...D.findings,...M.findings),f.warning&&S.push(f.warning),z=f.activeTestNumbers,m.push(...je(i,[u,b,g,d,h],e.testDefs,t)),m.push(...Q(i,u,e.testDefs,t),...Q(i,b,e.testDefs,t),...Q(i,g,e.testDefs,t),...Q(i,d,e.testDefs,t),...Q(i,h,e.testDefs,t))}if(t.enableClusterAnalysis){const f=Ce(n,a,e.testDefs);(!n||a||f!==void 0)&&m.push(...Be(i,e.wafer,{...t,isFailingDie:f}))}const $=We(m,{eligibleDies:i,softEligibleDies:i.filter(f=>f.sbin!==void 0),testDies:i,passBins:t.passBins,ringCount:t.ringCount,sectorCount:t.sectorCount,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs});if(m.length=0,m.push(...$),t.enablePatternClassification&&a){const f=Ve(i,e.wafer,{passBins:t.passBins,ringCount:t.ringCount});if(f!==null&&f.pattern!=="random"&&f.pattern!=="none"){let U=function(B){const N=E(B).map(K=>j(K).ring).filter(K=>K!==void 0);if(N.length===0)return!0;const fe=N.some(K=>K===W),ce=N.some(K=>K===1);return F==="edge-ring"||F==="edge-local"?fe:F==="center"?ce:F==="donut"?!fe&&!ce:!0};const v={center:"Center cluster",donut:"Donut","edge-ring":"Edge-ring","edge-local":"Edge-local",scratch:"Scratch","near-full":"Near-full"}[f.pattern]??f.pattern,D=f.confidence==="high"?"unusual":f.confidence==="medium"?"notable":"info",M=f.features,V=B=>`${(B*100).toFixed(0)}%`,R=f.pattern==="edge-ring"||f.pattern==="edge-local"?`${V(M.edgeRdd)} of edge dies failing`:f.pattern==="center"||f.pattern==="donut"?`centroid at ${V(M.centroidDistNorm)} of wafer radius from centre`:f.pattern==="scratch"?`linear score ${M.linearScore.toFixed(2)}, eccentricity ${M.eccentricity.toFixed(2)}`:`${V(M.globalRdd)} of dies failing`,C=i.filter(B=>{const N=B.hbin??B.sbin;return N!==void 0&&!new Set(t.passBins).has(N)}),P={"edge-ring":["ring","edge-arc"],"edge-local":["edge-arc","sector","quadrant"],center:["ring","cluster"],donut:["ring"],scratch:["cluster","sector","quadrant"],"near-full":["ring"]},A=new Set(P[f.pattern]??[]),F=f.pattern,W=t.ringCount,x=[];for(const B of m){if(!A.has(B.comparison.family)||B.comparison.family==="ring"&&!U(B))continue;x.push(B.id);const N=B.comparison.family==="ring"||B.comparison.family==="quadrant"||B.comparison.family==="sector";D!=="info"&&N&&(B.severity="info")}m.push({id:`spatial-pattern:${f.pattern}`,level:"wafer",severity:D,variable:{kind:"spatialPattern",label:v},comparison:{family:"spatial-pattern",left:v,right:"Wafer"},effect:{direction:"different",effectSize:M.globalRdd},stats:{method:"geometry",sampleSizeLeft:C.length,sampleSizeRight:i.length},summary:`Spatial pattern: ${v.toLowerCase()} (${f.confidence} confidence) \u2014 ${R}${f.note?` [${f.note}]`:""}`,highlight:{kind:"dies",dieKeys:C.map(B=>T(B))},relatedIds:x.length>0?x:void 0})}}m.sort((f,y)=>{const v=f.stats.adjustedPValue??f.stats.pValue??1,D=y.stats.adjustedPValue??y.stats.pValue??1;return v!==D?v-D:Math.abs(y.effect.absoluteDelta??0)-Math.abs(f.effect.absoluteDelta??0)});const p=Ae(e.dies,i.length,e.yield.yieldPercent);n&&(p.isLotStack=!0,l&&(p.aggregationMethod=l));const w=Ke(e.dies,e.testDefs);w&&(p.testSpecYield=w);const k=computeFunctionalYield(e.dies,e.testDefs);if(k&&(p.functionalYield=k),z===void 0&&t.computePerTestStats){const f=we(i,t.testNumbers,e.testDefs);f.warning&&S.push(f.warning),z=f.testNumbers}if(z?.length){const f=Ee(e.dies,z,e.testDefs,t.minimumSampleSize);f&&(p.perTestStats=f)}return S.length>0&&(p.warnings=S),{level:"wafer",hasNotableFindings:m.some(f=>f.severity==="notable"||f.severity==="unusual"),findings:m,wafer:e.wafer.metadata??void 0,stats:p}}
1
+ import{isYieldEligibleDie as Z,getDieKey as x,isPositionedDie as ue}from"../core/dies.js";import{buildWaferMap as ke,getTestPassStatus as de,isParametricTest as q}from"../renderer/buildWaferMap.js";import{buildQuadrantRegions as ze,buildReticlePositionRegions as Me,buildRingRegions as Re,buildSectorRegions as Be,buildTestSiteRegions as Fe,sectorCompassNames as ge,areQuadrantsAdjacent as Pe,parseRegionKey as j}from"./regions.js";import{buildClusterFindings as Ve}from"./clusterDetection.js";import{classifyPattern as Le}from"./patternClassification.js";import{normalCdf as he}from"./math.js";import{quantile as ee}from"./math.js";import{mean as G,clamp01 as me}from"../core/utils.js";const Ne={ringCount:4,passBins:[1],significanceLevel:.05,minimumEffectSize:.2,minimumRelativeEffect:1,minimumSampleSize:5,includePartial:!1,includeEdgeExcluded:!1,enableYieldAnalysis:!0,enableHardBinAnalysis:!0,enableSoftBinAnalysis:!0,enableTestValueAnalysis:!1,computePerTestStats:!1,enableReticlePositionAnalysis:!0,enableClusterAnalysis:!0,enableAngularAnalysis:!0,sectorCount:8,enablePatternClassification:!0,minimumClusterSize:5};function Ce(s,a){const o={...s};return o.minimumClusterSize=Math.max(5,Math.round(a*.003)),o}function Ke(s){return"wafer"in s&&"dies"in s&&"view"in s?s:ke(s)}function Ee(s,a){return Z(s,a)?s.hbin!==void 0||s.sbin!==void 0||s.testValues!==void 0&&Object.keys(s.testValues).length>0:!1}function Ae(s,a,o){if(!s||a)return;const e=(o??[]).filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(e.length!==0)return n=>{for(const f of e){const r=f.testNumber,i=n.testValues?.[r];if(i!==void 0&&(f.limitLow!==void 0&&i<f.limitLow||f.limitHigh!==void 0&&i>f.limitHigh))return!0}return!1}}function xe(s,a,o){const e=new Set,n=new Set,f=new Set,r=new Map,i=new Map;for(const l of s){if(l.testValues)for(const t of Object.keys(l.testValues))e.add(Number(t));if(l.testPass)for(const t of Object.keys(l.testPass))e.add(Number(t));l.hbin!==void 0&&n.add(l.hbin),l.sbin!==void 0&&f.add(l.sbin),Z(l)&&(l.hbin!==void 0&&r.set(l.hbin,(r.get(l.hbin)??0)+1),l.sbin!==void 0&&i.set(l.sbin,(i.get(l.sbin)??0)+1))}return{totalDies:s.length,analyzedDies:a,excludedDies:s.length-a,yieldPercent:o,testsConsidered:[...e].sort((l,t)=>l-t),hardBinsConsidered:[...n].sort((l,t)=>l-t),softBinsConsidered:[...f].sort((l,t)=>l-t),...r.size?{hardBinCounts:Object.fromEntries(r)}:{},...i.size?{softBinCounts:Object.fromEntries(i)}:{}}}function Te(s,a){if(!a?.length)return;const o=a.filter(n=>q(n)&&(n.limitLow!==void 0||n.limitHigh!==void 0));if(!o.length)return;const e=[];for(const n of o){const f=n.testNumber;let r=0,i=0,l=0,t=0;for(const c of s){if(c.partial||c.edgeExcluded)continue;const m=c.testValues?.[f];m!==void 0&&(t++,n.limitLow!==void 0&&m<n.limitLow?i++:n.limitHigh!==void 0&&m>n.limitHigh?l++:r++)}e.push({testNumber:f,label:n.name,passDies:r,failLowDies:i,failHighDies:l,totalDies:t,yieldPercent:t>0?r/t*100:null})}return e.length?e:void 0}export function computeFunctionalYield(s,a){if(!a?.length)return;const o=a.filter(n=>!q(n));if(!o.length)return;const e=[];for(const n of o){const f=n.testNumber;let r=0,i=0;for(const t of s){if(t.partial||t.edgeExcluded)continue;const c=de(t,f,n);c!==void 0&&(c?r++:i++)}const l=r+i;e.push({testNumber:f,label:n.name,passDies:r,failDies:i,totalDies:l,passRatePercent:l>0?r/l*100:null})}return e.length?e:void 0}function qe(s,a,o,e){const n=[];for(const f of a){const r=[];for(const m of s){if(m.partial||m.edgeExcluded)continue;const g=m.testValues?.[f];g!==void 0&&r.push(g)}if(r.length<e)continue;const i=G(r),l=Math.sqrt(re(r,i)),t=r.slice().sort((m,g)=>m-g),c=o?.find(m=>m.testNumber===f)?.name??String(f);n.push({testNumber:f,label:c,count:r.length,min:t[0],max:t[t.length-1],mean:i,stddev:l,median:ee(t,.5),q1:ee(t,.25),q3:ee(t,.75)})}return n.length?n:void 0}function H(s,a,o,e){const n=(s+o)/(a+e),f=n*(1-n)*(1/a+1/e);if(!Number.isFinite(f)||f<=0)return 1;const r=(s/a-o/e)/Math.sqrt(f);return me(2*(1-he(Math.abs(r))))}function te(s){const a=new Map;for(const o of s){const e=`${o.variable.kind}:${o.comparison.family}`,n=a.get(e)??[];n.push(o),a.set(e,n)}for(const o of a.values()){const e=[...o].filter(r=>r.stats.pValue!==void 0).sort((r,i)=>(r.stats.pValue??1)-(i.stats.pValue??1)),n=e.length;let f=1;for(let r=n-1;r>=0;r--){const i=e[r],l=i.stats.pValue??1,t=Math.min(1,Math.min(f,l*n/(r+1)));i.stats.adjustedPValue=t,f=t}}return s}function ne(s,a,o){const e=Math.abs(a),n=o!==void 0?Math.abs(o):0;return s<=.01&&(e>=.3||n>=2.5)?"unusual":s<=.05&&(e>=.2||n>=1.5)?"notable":"info"}function ie(s,a){return s<=.01&&Math.abs(a)>=.5?"unusual":s<=.05&&Math.abs(a)>=.15?"notable":"info"}function J(s){return s==="reticle-position"?"other reticle positions":s==="test-site"?"other test sites":"the rest of the map"}function O(s){return s==="reticle-position"?"Other reticle positions":s==="test-site"?"Other test sites":"Rest of map"}function be(s,a,o){const e=(Math.abs(a)*100).toFixed(1);return`${s} yield is ${e} percentage points ${a>0?"higher":"lower"} than ${J(o)}`}function se(s,a){return a==="quadrant"&&!s.startsWith("Quadrant")?`quadrant ${s}`:s}function pe(s,a,o,e){const n=se(s,e),f=(Math.abs(o)*100).toFixed(1);return`${n} has ${a} occurrence ${f} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function je(s,a,o,e){const n=se(s,e),f=(Math.abs(o)*100).toFixed(1);return`${n} has ${a} pass rate ${f} percentage points ${o>0?"higher":"lower"} than ${J(e)}`}function ye(s,a,o,e,n,f){const r=se(s,n),i=J(n),l=o>0?"higher":"lower";if(e!==void 0&&Number.isFinite(e)){const c=(Math.abs(e)*100).toFixed(1);return`${r} mean ${a} is ${c}% ${l} than ${i}`}const t=f?` ${f}`:"";return`${r} mean ${a} is ${Math.abs(o).toPrecision(3)}${t} ${l} than ${i}`}function Se(s,a,o){const e=a?.find(n=>n.bin===s);return e?.name?`${o} ${s} (${e.name})`:`${o} ${s}`}function He(s,a){const o=a?.find(e=>e.testNumber===s);return{label:o?.name??`Test ${s}`,unit:o?.unit}}function oe(s,a){const o=new Map(s.map(n=>[x(n),n])),e=new Map;for(const n of a){const f=[];for(const r of n.dieKeys){const i=o.get(r);i&&f.push(i)}e.set(n.key,f)}return e}function ae(s,a){return te(s),s.filter(o=>{const e=o.stats.adjustedPValue??o.stats.pValue??1,n=Math.abs(o.effect.absoluteDelta??0),f=Math.abs(o.effect.relativeDelta??0);return e<=a.significanceLevel&&(n>=a.minimumEffectSize||f>=a.minimumRelativeEffect)}).map(o=>({...o,severity:ne(o.stats.adjustedPValue??o.stats.pValue??1,o.effect.absoluteDelta??0,o.effect.relativeDelta)}))}function Y(s,a,o,e){const n=new Set(o),f=oe(s,a),r=new Map,i=new Map;for(const[t,c]of f){let m=0,g=0;for(const d of c)d.hbin!==void 0&&(g++,n.has(d.hbin)&&m++);r.set(t,m),i.set(t,g)}const l=[];for(const t of a){const c=i.get(t.key)??0,m=r.get(t.key)??0;let g=0,d=0;for(const[z,b]of i)z!==t.key&&(g+=b,d+=r.get(z)??0);if(c<e.minimumSampleSize||g<e.minimumSampleSize)continue;const h=m/c,p=d/g,y=h-p,S=H(m,c,d,g);l.push({id:`yield:${t.key}`,level:"wafer",severity:"info",variable:{kind:"yield",label:"Yield"},comparison:{family:t.family,left:t.label,right:O(t.family)},effect:{direction:y===0?"different":y>0?"higher":"lower",absoluteDelta:y,relativeDelta:p===0?void 0:y/p,effectSize:y},stats:{method:"two-proportion-z",pValue:S,sampleSizeLeft:c,sampleSizeRight:g},summary:be(t.label,y,t.family),highlight:{kind:"region",regionFamily:t.family,regionKeys:[t.key],dieKeys:[...t.dieKeys]}})}return ae(l,e)}function L(s,a,o,e,n,f){const r=d=>o==="soft"?d.sbin:d.hbin,i=[...new Set(s.map(r).filter(d=>d!==void 0))].sort((d,h)=>d-h),l=oe(s,a),t=new Map,c=new Map;for(const[d,h]of l){const p=new Map;for(const y of h){const S=r(y);S!==void 0&&p.set(S,(p.get(S)??0)+1)}t.set(d,p),c.set(d,h.length)}const m=[],g=n==="hardBin"?"HBin":"SBin";for(const d of a){const h=c.get(d.key)??0,p=t.get(d.key);let y=0;const S=new Map;for(const[z,b]of t)if(z!==d.key){y+=c.get(z)??0;for(const[w,D]of b)S.set(w,(S.get(w)??0)+D)}if(!(h<f.minimumSampleSize||y<f.minimumSampleSize))for(const z of i){const b=p.get(z)??0,w=S.get(z)??0,D=b/h,v=w/y,k=D-v,u=H(b,h,w,y),$=Se(z,e,g);m.push({id:`${n}:${z}:${d.key}`,level:"wafer",severity:"info",variable:{kind:n,bin:z,label:$},comparison:{family:d.family,left:d.label,right:O(d.family)},effect:{direction:k===0?"different":k>0?"higher":"lower",absoluteDelta:k,relativeDelta:v===0?void 0:k/v,effectSize:k},stats:{method:"two-proportion-z",pValue:u,sampleSizeLeft:h,sampleSizeRight:y},summary:pe(d.label,$,k,d.family),highlight:{kind:"bin",bin:z,regionKeys:[d.key],dieKeys:[...d.dieKeys]}})}}return ae(m,f)}function Q(s,a,o,e){const n=(o??[]).filter(c=>!q(c));if(!n.length)return[];const f=n.map(c=>c.testNumber),r=oe(s,a),i=new Map,l=new Map;for(const[c,m]of r){const g=new Uint32Array(n.length),d=new Uint32Array(n.length);for(const h of m)for(let p=0;p<n.length;p++){const y=de(h,f[p],n[p]);y!==void 0&&(d[p]++,y&&g[p]++)}i.set(c,g),l.set(c,d)}const t=[];for(const c of a){const m=i.get(c.key),g=l.get(c.key);for(let d=0;d<n.length;d++){let h=0,p=0;for(const[k,u]of l)k!==c.key&&(p+=u[d],h+=i.get(k)[d]);const y=g[d],S=m[d];if(y<e.minimumSampleSize||p<e.minimumSampleSize)continue;const z=S/y,b=h/p,w=z-b,D=H(S,y,h,p),v=f[d];t.push({id:`functional:${v}:${c.key}`,level:"wafer",severity:"info",variable:{kind:"functionalTest",index:v,label:`${n[d].name} pass rate`},comparison:{family:c.family,left:c.label,right:O(c.family)},effect:{direction:w===0?"different":w>0?"higher":"lower",absoluteDelta:w,relativeDelta:b===0?void 0:w/b,effectSize:w},stats:{method:"two-proportion-z",pValue:D,sampleSizeLeft:y,sampleSizeRight:p},summary:je(c.label,n[d].name,w,c.family),highlight:{kind:"region",regionFamily:c.family,regionKeys:[c.key],dieKeys:[...c.dieKeys]}})}}return ae(t,e)}function re(s,a){return s.length<2?0:s.reduce((o,e)=>o+(e-a)**2,0)/(s.length-1)}function we(s,a,o,e,n,f){const r=Math.sqrt(o/s+f/e),i=a-n;if(!Number.isFinite(r)||r===0)return{pValue:1,effectSize:0,delta:i};const l=i/r,t=Math.sqrt(Math.max(0,(o+f)/2)),c=t===0?i:i/t;return{pValue:me(2*(1-he(Math.abs(l)))),effectSize:c,delta:i}}function Oe(s,a){const o=G(s),e=G(a);return we(s.length,o,re(s,o),a.length,e,re(a,e))}const De=250;function ve(s,a,o){const e=r=>r.filter(i=>q(o?.find(l=>l.testNumber===i)));if(a)return{testNumbers:e(a.slice().sort((r,i)=>r-i))};const n=new Set;let f=!1;e:for(const r of s)for(const i of Object.keys(r.testValues??{})){const l=Number(i);if(!n.has(l)&&(n.add(l),n.size>De)){f=!0;break e}}if(f){const r=`Test-value analysis was skipped: more than ${De} tests were found in the die data, so no test findings were computed. Pass testNumbers: [...] to analyse specific tests.`;return console.warn(`[wafermap] analyzeWaferMap: ${r}`),{testNumbers:[],warning:{code:"test-count-capped",message:r,severity:"warning"}}}return{testNumbers:e([...n].sort((r,i)=>r-i))}}function _(s,a,o,e){const n=ve(s,e.testNumbers,o);if(n.warning)return{findings:[],warning:n.warning};const f=n.testNumbers,r=new Map;for(let h=0;h<a.length;h++)for(const p of a[h].dieKeys)r.set(p,h);const i=a.length,l=[],t=[];for(const h of s){const p=r.get(x(h));p!==void 0&&(l.push(h),t.push(p))}const c=[],m=new Float64Array(i),g=new Float64Array(i),d=new Float64Array(i);for(const h of f){m.fill(0),g.fill(0),d.fill(0);let p=0,y=0,S=0,z;for(let b=0;b<l.length;b++){const D=l[b].testValues?.[h];if(D===void 0)continue;z===void 0&&(z=D);const v=D-z,k=t[b];m[k]+=1,g[k]+=v,d[k]+=v*v,p+=1,y+=v,S+=v*v}if(z!==void 0)for(let b=0;b<i;b++){const w=m[b],D=p-w;if(w<e.minimumSampleSize||D<e.minimumSampleSize)continue;const v=g[b]/w,u=(y-g[b])/D,$=v+z,R=u+z,B=w>1?Math.max(0,d[b]-w*v*v)/(w-1):0,M=D>1?Math.max(0,S-d[b]-D*u*u)/(D-1):0,{pValue:N,effectSize:P,delta:C}=we(w,$,B,D,R,M),V=a[b],{label:W,unit:E}=He(h,o),U=R!==0?C/Math.abs(R):void 0;c.push({id:`test:${h}:${V.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:h,label:W,unit:E},comparison:{family:V.family,left:V.label,right:O(V.family)},effect:{direction:C===0?"different":C>0?"higher":"lower",absoluteDelta:C,relativeDelta:U,effectSize:P},stats:{method:"welch-z-approx",pValue:N,sampleSizeLeft:w,sampleSizeRight:D},summary:ye(V.label,W,C,U,V.family,E),highlight:{kind:"region",regionFamily:V.family,regionKeys:[V.key],dieKeys:[...V.dieKeys]}})}}return te(c),{activeTestNumbers:f,findings:c.filter(h=>{const p=h.stats.adjustedPValue??h.stats.pValue??1,y=Math.abs(h.effect.effectSize??0);return p<=e.significanceLevel&&y>=e.minimumEffectSize}).map(h=>({...h,severity:ie(h.stats.adjustedPValue??h.stats.pValue??1,h.effect.effectSize??0)}))}}function Ie(s,a,o,e){if(!o?.length)return[];const n=o.filter(i=>q(i)&&(i.limitLow!==void 0||i.limitHigh!==void 0));if(!n.length)return[];const f=[],r=new Map(s.map(i=>[x(i),i]));for(const i of n){const l=i.testNumber;for(const t of a){const c=new Map;for(const g of t){const d=[];for(const h of g.dieKeys){const p=r.get(h);p&&d.push(p)}c.set(g.key,d)}const m=[];for(const g of t){const d=c.get(g.key),h=[];for(const[$,R]of c)if($!==g.key)for(const B of R)h.push(B);const p=$=>$.testValues?.[l]!==void 0,y=$=>{const R=$.testValues?.[l];return R===void 0?!1:i.limitLow!==void 0&&R<i.limitLow||i.limitHigh!==void 0&&R>i.limitHigh},S=d.filter(p),z=h.filter(p);if(S.length<e.minimumSampleSize||z.length<e.minimumSampleSize)continue;const b=S.filter(y).length,w=z.filter(y).length,D=b/S.length,v=w/z.length,k=D-v,u=H(b,S.length,w,z.length);m.push({id:`specLimit:${l}:${g.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:l,label:i.name,unit:i.unit},comparison:{family:g.family,left:g.label,right:O(g.family)},effect:{direction:k===0?"different":k>0?"higher":"lower",absoluteDelta:k,relativeDelta:v===0?void 0:k/v,effectSize:k},stats:{method:"two-proportion-z",pValue:u,sampleSizeLeft:S.length,sampleSizeRight:z.length},summary:`${g.label} spec-fail rate for ${i.name} is ${(Math.abs(k)*100).toFixed(1)} pp ${k>0?"higher":"lower"} than the rest of the wafer`,highlight:{kind:"region",regionFamily:g.family,regionKeys:[g.key],dieKeys:[...g.dieKeys]}})}te(m),f.push(...m.filter(g=>{const d=g.stats.adjustedPValue??g.stats.pValue??1,h=Math.abs(g.effect.absoluteDelta??0),p=Math.abs(g.effect.relativeDelta??0);return d<=e.significanceLevel&&(h>=e.minimumEffectSize||p>=e.minimumRelativeEffect)}).map(g=>({...g,severity:ie(g.stats.adjustedPValue??g.stats.pValue??1,g.effect.effectSize??0)})))}}return f}function T(s){return s.highlight.regionKeys??[]}function Ye(s){const a=s.variable.bin??s.variable.index??"";return`${s.comparison.family}\0${s.variable.kind}\0${a}\0${s.effect.direction}`}function I(s,a){const o=T(s)[0]??"",e=j(o);return e.family==="ring"?e.ring??0:e.family==="sector"?ge(a).indexOf(e.sector??""):0}function Qe(s,a,o){if(a==="quadrant"){const r=new Map;for(const c of s){const m=j(T(c)[0]??"").quadrant;m&&r.set(m,c)}const i=[...r.keys()],l=new Set,t=[];for(const c of i){if(l.has(c))continue;const m=[],g=[c];for(l.add(c);g.length;){const d=g.pop();m.push(d);for(const h of i)!l.has(h)&&Pe(d,h)&&(l.add(h),g.push(h))}t.push(m.map(d=>r.get(d)))}return t}const e=[...s].sort((r,i)=>I(r,o)-I(i,o)),n=[];let f=[];for(const r of e){if(f.length===0){f.push(r);continue}const i=I(f[f.length-1],o);I(r,o)-i===1?f.push(r):(n.push(f),f=[r])}if(f.length&&n.push(f),a==="sector"&&n.length>1){const i=ge(o).length,l=I(n[0][0],o),t=I(n[n.length-1][n[n.length-1].length-1],o);if(l===0&&t===i-1){const c=n.pop();n[0]=[...c,...n[0]]}}return n}function _e(s,a){const o=s.map(r=>j(T(r)[0]??"").ring).filter(r=>r!==void 0).sort((r,i)=>r-i),e=o[0],n=o[o.length-1];let f="";return e===n&&e===1&&a>1?f=" (core)":e===n&&e===a&&a>1&&(f=" (edge)"),e===n?`Ring ${e}${f}`:`Rings ${e}\u2013${n}`}function We(s,a){const o=s.map(e=>j(T(e)[0]??"").sector??"");return o.length===1?`Sector ${o[0]}`:`Sectors ${o[0]}\u2013${o[o.length-1]}`}function Ue(s){const a=s.map(o=>j(T(o)[0]??"").quadrant??"");return a.length===1?a[0]:a.length===2?`Quadrants ${a[0]} & ${a[1]}`:`Quadrants ${a.slice(0,-1).join(", ")} & ${a[a.length-1]}`}function le(s){return[...new Set(s)]}function Ge(s,a){const o=s[0],e=o.comparison.family,n=o.variable.kind,f=le(s.flatMap(b=>b.highlight.dieKeys??[])),r=le(s.flatMap(T)),i=new Set(f),l=e==="ring"?_e(s,a.ringCount):e==="sector"?We(s,a.sectorCount):Ue(s),t=n==="softBin"?a.softEligibleDies:n==="test"?a.testDies:a.eligibleDies,c=t.filter(b=>i.has(x(b))),m=t.filter(b=>!i.has(x(b)));let g,d,h,p,y;if(n==="test"){const b=o.variable.index,w=M=>M.testValues?.[b],D=c.map(w).filter(M=>M!==void 0),v=m.map(w).filter(M=>M!==void 0),{pValue:k,effectSize:u,delta:$}=Oe(D,v),R=G(v),B=R!==0?$/Math.abs(R):void 0;g={direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:B,effectSize:u},d={method:"welch-z-approx",pValue:k,sampleSizeLeft:D.length,sampleSizeRight:v.length},h=ie(k,u),p=ye(l,o.variable.label,$,B,e,o.variable.unit),y=`test:${b}`}else if(n==="yield"){const b=new Set(a.passBins),w=c.filter(P=>P.hbin!==void 0),D=m.filter(P=>P.hbin!==void 0),v=w.length,k=D.length,u=w.filter(P=>b.has(P.hbin)).length,$=D.filter(P=>b.has(P.hbin)).length,R=v>0?u/v:0,B=k>0?$/k:0,M=R-B,N=H(u,v,$,k);g={direction:M===0?"different":M>0?"higher":"lower",absoluteDelta:M,relativeDelta:B===0?void 0:M/B,effectSize:M},d={method:"two-proportion-z",pValue:N,sampleSizeLeft:v,sampleSizeRight:k},h=ne(N,M,g.relativeDelta),p=be(l,M,e),y="yield"}else{const b=o.variable.bin,w=M=>n==="softBin"?M.sbin:M.hbin,D=c.filter(M=>w(M)===b).length,v=m.filter(M=>w(M)===b).length,k=D/c.length,u=v/m.length,$=k-u,R=H(D,c.length,v,m.length);g={direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:u===0?void 0:$/u,effectSize:$},d={method:"two-proportion-z",pValue:R,sampleSizeLeft:c.length,sampleSizeRight:m.length},h=ne(R,$,g.relativeDelta);const B=n==="softBin"?a.sbinDefs:a.hbinDefs;p=pe(l,Se(b,B,n==="softBin"?"SBin":"HBin"),$,e),y=`${n}:${b}`}const S=le(r.map(b=>b.replace(`${e}:`,""))).join("-"),z=n==="hardBin"||n==="softBin"?{kind:"bin",bin:o.variable.bin,regionKeys:r,dieKeys:f}:{kind:"region",regionFamily:e,regionKeys:r,dieKeys:f};return{...o,id:`${y}:${e}:${S}`,severity:h,comparison:{family:e,left:l,right:O(e)},effect:g,stats:d,summary:p,highlight:z,relatedIds:s.map(b=>b.id)}}function Je(s,a){const o=new Set(["ring","quadrant","sector"]),e=[],n=new Map;for(const r of s){if(!o.has(r.comparison.family)){e.push(r);continue}const i=Ye(r),l=n.get(i)??[];l.push(r),n.set(i,l)}const f=[];for(const r of n.values()){if(r.length===1){f.push(r[0]);continue}const i=r[0].comparison.family;for(const l of Qe(r,i,a.sectorCount))f.push(l.length===1?l[0]:Ge(l,a))}return[...e,...f]}function Xe(s){const a=new Map,o=new Map;for(const n of s){const f=x(n);n.hbin!==void 0&&n.hbin!==null&&(a.get(n.hbin)??a.set(n.hbin,new Set).get(n.hbin)).add(f),n.sbin!==void 0&&n.sbin!==null&&(o.get(n.sbin)??o.set(n.sbin,new Set).get(n.sbin)).add(f)}const e=new Set;for(const[n,f]of a)for(const[r,i]of o){if(f.size!==i.size)continue;let l=!0;for(const t of f)if(!i.has(t)){l=!1;break}l&&e.add(`${n}:${r}`)}return e}function Ze(s){return`${s.comparison.family}\0${s.comparison.left}`}function et(s,a,o){const e=(i,l)=>{i.absorbedIds=[...i.absorbedIds??[],l.id]},n=new Map;for(const i of s){const l=Ze(i);(n.get(l)??n.set(l,[]).get(l)).push(i)}const f=Xe(a),r=new Set;for(const i of n.values()){for(const t of i)if(!(t.variable.kind!=="hardBin"||t.variable.bin===void 0))for(const c of i){if(c.variable.kind!=="softBin"||c.variable.bin===void 0||r.has(c.id)||!f.has(`${t.variable.bin}:${c.variable.bin}`)||c.effect.direction!==t.effect.direction)continue;e(t,c),r.add(c.id);const m=`${t.variable.label} and ${c.variable.label} (same dies)`;t.summary=t.summary.replace(t.variable.label,m),t.variable.label=m;break}if(o.length!==1)continue;const l=i.find(t=>t.variable.kind==="yield");if(l){for(const t of i)if(!(t===l||r.has(t.id)||!((t.variable.kind==="hardBin"||t.variable.kind==="softBin")&&t.variable.bin===o[0]))){e(l,t),r.add(t.id);for(const m of t.absorbedIds??[])l.absorbedIds=[...l.absorbedIds??[],m]}}}}export function analyzeWaferMap(s,a={}){const o={...Ne,...a},e=Ke(s),n=e.isLotStack,f=e.aggrMethod,r=!n||f==="mode"||f==="countBin"||f==="percent",i=e.dies.filter(u=>Ee(u,o)),l=i.filter(ue),t=Ce(o,i.length),c=e.dies.filter(u=>Z(u,t)),m=c.filter(ue),g=Re(m,e.wafer,t.ringCount),d=ze(m,e.wafer,t.ringCount),h=t.enableReticlePositionAnalysis?Me(m,e.reticleConfig):[],p=t.enableTestSiteAnalysis===!1?[]:Fe(c,t.enableTestSiteAnalysis===!0),y=t.enableAngularAnalysis?Be(m,e.wafer,t.sectorCount):[],S=[];if(t.enableYieldAnalysis&&r&&S.push(...Y(i,g,t.passBins,t),...Y(i,d,t.passBins,t),...Y(i,h,t.passBins,t),...Y(i,p,t.passBins,t),...Y(i,y,t.passBins,t)),t.enableHardBinAnalysis&&r&&S.push(...L(i,g,"hard",e.hbinDefs,"hardBin",t),...L(i,d,"hard",e.hbinDefs,"hardBin",t),...L(i,h,"hard",e.hbinDefs,"hardBin",t),...L(i,p,"hard",e.hbinDefs,"hardBin",t),...L(i,y,"hard",e.hbinDefs,"hardBin",t)),t.enableSoftBinAnalysis){const u=i.filter($=>$.sbin!==void 0);S.push(...L(u,g,"soft",e.sbinDefs,"softBin",t),...L(u,d,"soft",e.sbinDefs,"softBin",t),...L(u,h,"soft",e.sbinDefs,"softBin",t),...L(u,p,"soft",e.sbinDefs,"softBin",t),...L(u,y,"soft",e.sbinDefs,"softBin",t))}const z=[];let b;if(t.enableTestValueAnalysis){const u=_(i,g,e.testDefs,t),$=_(i,d,e.testDefs,t),R=_(i,h,e.testDefs,t),B=_(i,p,e.testDefs,t),M=_(i,y,e.testDefs,t);S.push(...u.findings,...$.findings,...R.findings,...B.findings,...M.findings),u.warning&&z.push(u.warning),b=u.activeTestNumbers,S.push(...Ie(i,[g,d,h,p,y],e.testDefs,t)),S.push(...Q(i,g,e.testDefs,t),...Q(i,d,e.testDefs,t),...Q(i,h,e.testDefs,t),...Q(i,p,e.testDefs,t),...Q(i,y,e.testDefs,t))}if(t.enableClusterAnalysis){const u=Ae(n,r,e.testDefs);(!n||r||u!==void 0)&&S.push(...Ve(l,e.wafer,{...t,isFailingDie:u}))}const w=Je(S,{eligibleDies:i,softEligibleDies:i.filter(u=>u.sbin!==void 0),testDies:i,passBins:t.passBins,ringCount:t.ringCount,sectorCount:t.sectorCount,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs});if(S.length=0,S.push(...w),t.enablePatternClassification&&r){const u=Le(l,e.wafer,{passBins:t.passBins,ringCount:t.ringCount});if(u!==null&&u.pattern!=="random"&&u.pattern!=="none"){let $e=function(F){const K=T(F).map(A=>j(A).ring).filter(A=>A!==void 0);if(K.length===0)return!0;const fe=K.some(A=>A===U),ce=K.some(A=>A===1);return E==="edge-ring"||E==="edge-local"?fe:E==="center"?ce:E==="donut"?!fe&&!ce:!0};const R={center:"Center cluster",donut:"Donut","edge-ring":"Edge-ring","edge-local":"Edge-local",scratch:"Scratch","near-full":"Near-full"}[u.pattern]??u.pattern,B=u.confidence==="high"?"unusual":u.confidence==="medium"?"notable":"info",M=u.features,N=F=>`${(F*100).toFixed(0)}%`,P=u.pattern==="edge-ring"||u.pattern==="edge-local"?`${N(M.edgeRdd)} of edge dies failing`:u.pattern==="center"||u.pattern==="donut"?`centroid at ${N(M.centroidDistNorm)} of wafer radius from centre`:u.pattern==="scratch"?`linear score ${M.linearScore.toFixed(2)}, eccentricity ${M.eccentricity.toFixed(2)}`:`${N(M.globalRdd)} of dies failing`,C=i.filter(F=>{const K=F.hbin??F.sbin;return K!==void 0&&!new Set(t.passBins).has(K)}),V={"edge-ring":["ring","edge-arc"],"edge-local":["edge-arc","sector","quadrant"],center:["ring","cluster"],donut:["ring"],scratch:["cluster","sector","quadrant"],"near-full":["ring"]},W=new Set(V[u.pattern]??[]),E=u.pattern,U=t.ringCount,X=[];for(const F of S){if(!W.has(F.comparison.family)||F.comparison.family==="ring"&&!$e(F))continue;X.push(F.id);const K=F.comparison.family==="ring"||F.comparison.family==="quadrant"||F.comparison.family==="sector";B!=="info"&&K&&(F.severity="info")}S.push({id:`spatial-pattern:${u.pattern}`,level:"wafer",severity:B,variable:{kind:"spatialPattern",label:R},comparison:{family:"spatial-pattern",left:R,right:"Wafer"},effect:{direction:"different",effectSize:M.globalRdd},stats:{method:"geometry",sampleSizeLeft:C.length,sampleSizeRight:i.length},summary:`Spatial pattern: ${R.toLowerCase()} (${u.confidence} confidence) \u2014 ${P}${u.note?` [${u.note}]`:""}`,highlight:{kind:"dies",dieKeys:C.map(F=>x(F))},relatedIds:X.length>0?X:void 0})}}S.sort((u,$)=>{const R=u.stats.adjustedPValue??u.stats.pValue??1,B=$.stats.adjustedPValue??$.stats.pValue??1;return R!==B?R-B:Math.abs($.effect.absoluteDelta??0)-Math.abs(u.effect.absoluteDelta??0)});const D=xe(e.dies,i.length,e.yield.yieldPercent);n&&(D.isLotStack=!0,f&&(D.aggregationMethod=f));const v=Te(e.dies,e.testDefs);v&&(D.testSpecYield=v);const k=computeFunctionalYield(e.dies,e.testDefs);if(k&&(D.functionalYield=k),b===void 0&&t.computePerTestStats){const u=ve(i,t.testNumbers,e.testDefs);u.warning&&z.push(u.warning),b=u.testNumbers}if(b?.length){const u=qe(e.dies,b,e.testDefs,t.minimumSampleSize);u&&(D.perTestStats=u)}return z.length>0&&(D.warnings=z),et(S,i,t.passBins),{level:"wafer",hasNotableFindings:S.some(u=>u.severity==="notable"||u.severity==="unusual"),findings:S,wafer:e.wafer.metadata??void 0,stats:D}}
@@ -1,4 +1,4 @@
1
- import type { Die, Wafer } from '../core/index.js';
1
+ import type { Die, PositionedDie, Wafer } from '../core/index.js';
2
2
  import type { StatsFinding } from './types.js';
3
3
  interface ClusterOptions {
4
4
  passBins: number[];
@@ -13,6 +13,6 @@ interface ClusterOptions {
13
13
  /** Optional override for the failure predicate. Default: hard bin, then soft bin, not in passBins. */
14
14
  isFailingDie?: (die: Die) => boolean;
15
15
  }
16
- export declare function buildClusterFindings(dies: Die[], wafer: Wafer, options: ClusterOptions): StatsFinding[];
16
+ export declare function buildClusterFindings(dies: PositionedDie[], wafer: Wafer, options: ClusterOptions): StatsFinding[];
17
17
  export {};
18
18
  //# sourceMappingURL=clusterDetection.d.ts.map
@@ -1,4 +1,4 @@
1
- import type { Die, Wafer } from '../core/index.js';
1
+ import type { PositionedDie, Wafer } from '../core/index.js';
2
2
  export type PatternLabel = 'center' | 'donut' | 'edge-ring' | 'edge-local' | 'scratch' | 'near-full' | 'random' | 'none';
3
3
  export interface PatternFeatures {
4
4
  /** Ratio of failing dies to total eligible dies (whole-wafer). */
@@ -100,7 +100,7 @@ export declare const DEFAULT_PATTERN_THRESHOLDS: PatternThresholds;
100
100
  * Returns `null` when the failing-die count is below the minimum threshold
101
101
  * (`thresholds.minimumFailingDies`, default 5) — too few failures to classify.
102
102
  */
103
- export declare function classifyPattern(dies: Die[], wafer: Wafer, options: {
103
+ export declare function classifyPattern(dies: PositionedDie[], wafer: Wafer, options: {
104
104
  passBins: number[];
105
105
  ringCount?: number;
106
106
  }): PatternClassification | null;
@@ -1,4 +1,4 @@
1
- import { type Die, type Wafer } from '../core/index.js';
1
+ import { type Die, type PositionedDie, type Wafer } from '../core/index.js';
2
2
  import type { ReticleConfig } from '../renderer/buildWaferMap.js';
3
3
  export interface StatsRegion {
4
4
  family: 'ring' | 'quadrant' | 'reticle-position' | 'test-site' | 'sector';
@@ -22,7 +22,7 @@ export interface RegionYieldDatum {
22
22
  * the ring/quadrant yield diagrams, which previously each recomputed the
23
23
  * same pass/total tally independently.
24
24
  */
25
- export declare function buildRegionYieldData(diesByWafer: Die[][], allWafers: Wafer[], ringCount: number, passBins: number[], regionBuilder: (dies: Die[], wafer: Wafer, ringCount: number) => StatsRegion[]): RegionYieldDatum[];
25
+ export declare function buildRegionYieldData(diesByWafer: Die[][], allWafers: Wafer[], ringCount: number, passBins: number[], regionBuilder: (dies: PositionedDie[], wafer: Wafer, ringCount: number) => StatsRegion[]): RegionYieldDatum[];
26
26
  /** Compass bearing names for a given sector count, ordered CCW from East. */
27
27
  export declare function sectorCompassNames(sectorCount: number): string[];
28
28
  /** True when two quadrants share an edge (NE–NW, NE–SE, NW–SW, SE–SW); diagonals are not adjacent. */
@@ -38,9 +38,9 @@ export interface ParsedRegionKey {
38
38
  * structured parts. Always parse identity from the key, never from the label.
39
39
  */
40
40
  export declare function parseRegionKey(key: string): ParsedRegionKey;
41
- export declare function buildRingRegions(dies: Die[], wafer: Wafer, ringCount: number): StatsRegion[];
42
- export declare function buildQuadrantRegions(dies: Die[], wafer: Wafer, ringCount: number): StatsRegion[];
43
- export declare function buildReticlePositionRegions(dies: Die[], reticleConfig: ReticleConfig | undefined): StatsRegion[];
41
+ export declare function buildRingRegions(dies: PositionedDie[], wafer: Wafer, ringCount: number): StatsRegion[];
42
+ export declare function buildQuadrantRegions(dies: PositionedDie[], wafer: Wafer, ringCount: number): StatsRegion[];
43
+ export declare function buildReticlePositionRegions(dies: PositionedDie[], reticleConfig: ReticleConfig | undefined): StatsRegion[];
44
44
  /**
45
45
  * Group dies by siteNum for parallel test site analysis.
46
46
  *
@@ -53,5 +53,5 @@ export declare function buildReticlePositionRegions(dies: Die[], reticleConfig:
53
53
  * validated the data).
54
54
  */
55
55
  export declare function buildTestSiteRegions(dies: Die[], forceEnable?: boolean): StatsRegion[];
56
- export declare function buildSectorRegions(dies: Die[], wafer: Wafer, sectorCount: number): StatsRegion[];
56
+ export declare function buildSectorRegions(dies: PositionedDie[], wafer: Wafer, sectorCount: number): StatsRegion[];
57
57
  //# sourceMappingURL=regions.d.ts.map
@@ -1 +1 @@
1
- import{classifyDie as E,getReticleCell as W,getRingLabel as b,getDieKey as M}from"../core/index.js";function y(t){return M(t)}export function buildRegionYieldData(t,s,o,e,n){const i=new Set(e),r=new Map,a=[];for(let l=0;l<s.length;l++){const c=t[l];if(!c?.length)continue;const d=n(c,s[l],o),S=new Map(c.map(u=>[y(u),u]));for(const u of d){a.includes(u.key)||a.push(u.key);const g=r.get(u.key)??{label:u.label,pass:0,total:0};for(const h of u.dieKeys){const f=S.get(h);if(!f||f.partial||f.edgeExcluded)continue;const p=f.hbin??f.sbin;p!=null&&(g.total++,i.has(p)&&g.pass++)}r.set(u.key,g)}}return a.map(l=>{const c=r.get(l);return c.total===0?null:{key:l,label:c.label,n:c.total,yieldPercent:c.pass/c.total*100}}).filter(l=>l!==null)}const k=["E","ENE","NE","NNE","N","NNW","NW","WNW","W","WSW","SW","SSW","S","SSE","SE","ESE"],x=["E","NE","N","NW","W","SW","S","SE"],R=["E","N","W","S"];export function sectorCompassNames(t){const s=[4,8,16,32].includes(t)?t:16;return s===4?R:s===8?x:k}const K={NE:["NW","SE"],NW:["NE","SW"],SE:["NE","SW"],SW:["NW","SE"]};export function areQuadrantsAdjacent(t,s){return K[t]?.includes(s)??!1}export function parseRegionKey(t){return t.startsWith("ring:")?{family:"ring",ring:Number(t.slice(5))}:t.startsWith("quadrant:")?{family:"quadrant",quadrant:t.slice(9)}:t.startsWith("sector:")?{family:"sector",sector:t.slice(7)}:{family:"unknown"}}export function buildRingRegions(t,s,o){const e=new Map;for(const n of t){const{ring:i}=E(n,s,{ringCount:o}),r=`ring:${i}`,a=e.get(r)??{family:"ring",key:r,label:b(i,o),dieKeys:[]};a.dieKeys.push(y(n)),e.set(r,a)}return[...e.values()].sort((n,i)=>n.key.localeCompare(i.key))}export function buildQuadrantRegions(t,s,o){const e=new Map;for(const i of t){const{quadrant:r}=E(i,s,{ringCount:o}),a=`quadrant:${r}`,l=e.get(a)??{family:"quadrant",key:a,label:r,dieKeys:[]};l.dieKeys.push(y(i)),e.set(a,l)}const n=new Map([["quadrant:NE",0],["quadrant:NW",1],["quadrant:SE",2],["quadrant:SW",3]]);return[...e.values()].sort((i,r)=>(n.get(i.key)??4)-(n.get(r.key)??4))}export function buildReticlePositionRegions(t,s){if(!s)return[];const o=new Map;for(const e of t){const{column:n,row:i}=W(e,s),r=`reticle-position:cell:${n},${i}`,a=o.get(r)??{family:"reticle-position",key:r,label:`Reticle cell (${n}, ${i})`,dieKeys:[]};a.dieKeys.push(y(e)),o.set(r,a)}return[...o.values()].sort((e,n)=>e.key.localeCompare(n.key))}const q=3;export function buildTestSiteRegions(t,s=!1){const o=new Map;for(const e of t){if(e.siteNum===void 0)continue;const n=o.get(e.siteNum)??[];n.push(y(e)),o.set(e.siteNum,n)}if(!s){let e=0;for(const n of o.values())n.length>=q&&e++;if(e<2)return[]}return[...o.entries()].sort(([e],[n])=>e-n).map(([e,n])=>({family:"test-site",key:`test-site:${e}`,label:`Site ${e}`,dieKeys:n}))}export function buildSectorRegions(t,s,o){const e=[4,8,16,32].includes(o)?o:16,n=sectorCompassNames(e),i=new Map,r=s.center.x,a=s.center.y,l=s.radius;for(const c of t){const d=c.physX-r,S=c.physY-a;if(Math.hypot(d,S)/l<.2)continue;const g=Math.PI/e,h=(Math.atan2(S,d)+2*Math.PI+g)%(2*Math.PI),f=Math.floor(h/(2*Math.PI)*e)%e,p=n[f],N=`sector:${p}`,m=i.get(N)??{family:"sector",key:N,label:`Sector ${p}`,dieKeys:[]};m.dieKeys.push(y(c)),i.set(N,m)}return[...i.values()].sort((c,d)=>c.key.localeCompare(d.key))}
1
+ import{classifyDie as E,getReticleCell as W,getRingLabel as b,isPositionedDie as M,getDieKey as k}from"../core/index.js";function y(t){return k(t)}export function buildRegionYieldData(t,s,o,e,n){const i=new Set(e),r=new Map,a=[];for(let l=0;l<s.length;l++){const c=t[l];if(!c?.length)continue;const d=n(c.filter(M),s[l],o),S=new Map(c.map(u=>[y(u),u]));for(const u of d){a.includes(u.key)||a.push(u.key);const g=r.get(u.key)??{label:u.label,pass:0,total:0};for(const h of u.dieKeys){const f=S.get(h);if(!f||f.partial||f.edgeExcluded)continue;const p=f.hbin??f.sbin;p!=null&&(g.total++,i.has(p)&&g.pass++)}r.set(u.key,g)}}return a.map(l=>{const c=r.get(l);return c.total===0?null:{key:l,label:c.label,n:c.total,yieldPercent:c.pass/c.total*100}}).filter(l=>l!==null)}const x=["E","ENE","NE","NNE","N","NNW","NW","WNW","W","WSW","SW","SSW","S","SSE","SE","ESE"],R=["E","NE","N","NW","W","SW","S","SE"],K=["E","N","W","S"];export function sectorCompassNames(t){const s=[4,8,16,32].includes(t)?t:16;return s===4?K:s===8?R:x}const q={NE:["NW","SE"],NW:["NE","SW"],SE:["NE","SW"],SW:["NW","SE"]};export function areQuadrantsAdjacent(t,s){return q[t]?.includes(s)??!1}export function parseRegionKey(t){return t.startsWith("ring:")?{family:"ring",ring:Number(t.slice(5))}:t.startsWith("quadrant:")?{family:"quadrant",quadrant:t.slice(9)}:t.startsWith("sector:")?{family:"sector",sector:t.slice(7)}:{family:"unknown"}}export function buildRingRegions(t,s,o){const e=new Map;for(const n of t){const{ring:i}=E(n,s,{ringCount:o}),r=`ring:${i}`,a=e.get(r)??{family:"ring",key:r,label:b(i,o),dieKeys:[]};a.dieKeys.push(y(n)),e.set(r,a)}return[...e.values()].sort((n,i)=>n.key.localeCompare(i.key))}export function buildQuadrantRegions(t,s,o){const e=new Map;for(const i of t){const{quadrant:r}=E(i,s,{ringCount:o}),a=`quadrant:${r}`,l=e.get(a)??{family:"quadrant",key:a,label:r,dieKeys:[]};l.dieKeys.push(y(i)),e.set(a,l)}const n=new Map([["quadrant:NE",0],["quadrant:NW",1],["quadrant:SE",2],["quadrant:SW",3]]);return[...e.values()].sort((i,r)=>(n.get(i.key)??4)-(n.get(r.key)??4))}export function buildReticlePositionRegions(t,s){if(!s)return[];const o=new Map;for(const e of t){const{column:n,row:i}=W(e,s),r=`reticle-position:cell:${n},${i}`,a=o.get(r)??{family:"reticle-position",key:r,label:`Reticle cell (${n}, ${i})`,dieKeys:[]};a.dieKeys.push(y(e)),o.set(r,a)}return[...o.values()].sort((e,n)=>e.key.localeCompare(n.key))}const w=3;export function buildTestSiteRegions(t,s=!1){const o=new Map;for(const e of t){if(e.siteNum===void 0)continue;const n=o.get(e.siteNum)??[];n.push(y(e)),o.set(e.siteNum,n)}if(!s){let e=0;for(const n of o.values())n.length>=w&&e++;if(e<2)return[]}return[...o.entries()].sort(([e],[n])=>e-n).map(([e,n])=>({family:"test-site",key:`test-site:${e}`,label:`Site ${e}`,dieKeys:n}))}export function buildSectorRegions(t,s,o){const e=[4,8,16,32].includes(o)?o:16,n=sectorCompassNames(e),i=new Map,r=s.center.x,a=s.center.y,l=s.radius;for(const c of t){const d=c.physX-r,S=c.physY-a;if(Math.hypot(d,S)/l<.2)continue;const g=Math.PI/e,h=(Math.atan2(S,d)+2*Math.PI+g)%(2*Math.PI),f=Math.floor(h/(2*Math.PI)*e)%e,p=n[f],N=`sector:${p}`,m=i.get(N)??{family:"sector",key:N,label:`Sector ${p}`,dieKeys:[]};m.dieKeys.push(y(c)),i.set(N,m)}return[...i.values()].sort((c,d)=>c.key.localeCompare(d.key))}
@@ -1,11 +1,11 @@
1
- import{formatFindingDelta as g,formatFindingCoverage as h,formatFindingTooltip as b,escHtml as a,buildMetadataRows as d,renderDefinitionList as c,renderSection as p,renderSeverityBadge as w,reportStyles as v}from"./reportHtml.js";import{buildFindingsNarrative as y}from"./findingsNarrative.js";import{plainBinTerms as f}from"../renderer/fmt.js";function $(t,n){if(t.level==="lot"){const o=t,l=o.perWafer.map(r=>r.summary.wafer?.wafer??r.summary.wafer?.waferId).filter(r=>r!=null).map(String).join(", ");return c([...d(o.perWafer.map(r=>({metadata:r.summary.wafer}))),...l?[{label:"Wafers",value:l}]:[],{label:"Wafer count",value:String(o.stats.waferCount)},{label:"Generated",value:n}])}const e=t,i=e.stats.yieldPercent!==null?`${e.stats.yieldPercent.toFixed(1)}%`:"N/A";return c([...d([{metadata:e.wafer}]),{label:"Total dies",value:String(e.stats.totalDies)},{label:"Analysed dies",value:String(e.stats.analyzedDies)},{label:"Yield",value:i},{label:"Generated",value:n}])}function R(t,n){return t.length?t.map(e=>`<tr title="${a(b(e))}">
2
- <td class="tight">${w(e.severity)}</td>
3
- <td class="tight">${a(e.comparison.left)}</td>
4
- <td>${a(f(e.variable.label))}</td>
5
- <td class="numeric">${a(g(e))}</td>
6
- <td class="numeric">${a(h(e,n))}</td>
1
+ import{formatFindingDelta as b,formatFindingCoverage as w,formatFindingTooltip as v,escHtml as r,buildMetadataRows as c,renderDefinitionList as f,renderSection as p,renderSeverityBadge as $,reportStyles as y}from"./reportHtml.js";import{buildFindingsNarrative as R}from"./findingsNarrative.js";import{plainBinTerms as u}from"../renderer/fmt.js";function S(t,n){if(t.level==="lot"){const o=t,l=o.perWafer.map(a=>a.summary.wafer?.wafer??a.summary.wafer?.waferId).filter(a=>a!=null).map(String).join(", ");return f([...c(o.perWafer.map(a=>({metadata:a.summary.wafer}))),...l?[{label:"Wafers",value:l}]:[],{label:"Wafer count",value:String(o.stats.waferCount)},{label:"Generated",value:n}])}const e=t,i=e.stats.yieldPercent!==null?`${e.stats.yieldPercent.toFixed(1)}%`:"N/A";return f([...c([{metadata:e.wafer}]),{label:"Total dies",value:String(e.stats.totalDies)},{label:"Analysed dies",value:String(e.stats.analyzedDies)},{label:"Yield",value:i},{label:"Generated",value:n}])}function F(t,n){return t.length?t.map(e=>`<tr title="${r(v(e))}">
2
+ <td class="tight">${$(e.severity)}</td>
3
+ <td class="tight">${r(e.comparison.left)}</td>
4
+ <td>${r(u(e.variable.label))}</td>
5
+ <td class="numeric">${r(b(e))}</td>
6
+ <td class="numeric">${r(w(e,n))}</td>
7
7
  </tr>`).join(`
8
- `):'<tr><td colspan="5" class="no-data">No significant findings</td></tr>'}function F(t,n){return`<table class="report-table findings-table compact">
8
+ `):'<tr><td colspan="5" class="no-data">No significant findings</td></tr>'}function H(t,n){return`<table class="report-table findings-table compact">
9
9
  <thead>
10
10
  <tr>
11
11
  <th>Severity</th>
@@ -16,26 +16,26 @@ import{formatFindingDelta as g,formatFindingCoverage as h,formatFindingTooltip a
16
16
  </tr>
17
17
  </thead>
18
18
  <tbody>
19
- ${R(t,n)}
19
+ ${F(t,n)}
20
20
  </tbody>
21
- </table>`}export function renderFindingsReportHtml(t,n={}){const e=t.level==="lot",i=n.title??(e?"Lot Findings Report":"Wafer Findings Report"),o=new Date().toLocaleString(),l=t.findings,r=t.level==="lot"?t.stats.waferCount:void 0,s=f(y(l)??""),u=s?`<p class="findings-narrative">${a(s)}</p>
22
- `:"",m=[p("Summary",$(t,o)),p("Findings",u+F(l,r))].join(`
21
+ </table>`}export function renderFindingsReportHtml(t,n={}){const e=t.level==="lot",i=n.title??(e?"Lot Findings Report":"Wafer Findings Report"),o=new Date().toLocaleString(),l=new Set(t.findings.flatMap(s=>s.absorbedIds??[])),a=t.findings.filter(s=>!l.has(s.id)),m=t.level==="lot"?t.stats.waferCount:void 0,d=u(R(a)??""),g=d?`<p class="findings-narrative">${r(d)}</p>
22
+ `:"",h=[p("Summary",S(t,o)),p("Findings",g+H(a,m))].join(`
23
23
  `);return`<!DOCTYPE html>
24
24
  <html lang="en">
25
25
  <head>
26
26
  <meta charset="UTF-8">
27
- <title>${a(i)}</title>
27
+ <title>${r(i)}</title>
28
28
  <style>
29
- ${v()}
29
+ ${y()}
30
30
  </style>
31
31
  </head>
32
32
  <body>
33
33
  <main class="report">
34
34
  <header class="report-header">
35
- <h1>${a(i)}</h1>
36
- <p class="report-subtitle">Generated ${a(o)}</p>
35
+ <h1>${r(i)}</h1>
36
+ <p class="report-subtitle">Generated ${r(o)}</p>
37
37
  </header>
38
- ${m}
38
+ ${h}
39
39
  </main>
40
40
  </body>
41
41
  </html>`}export function openHtmlReport(t){if(typeof window.__openHtmlReport=="function"){window.__openHtmlReport(t);return}const n=window.open("","_blank");n&&(n.document.write(t),n.document.close())}export function setReportOpener(t){window.__openHtmlReport=t}