@wafertools/wafermap 0.25.0 → 0.26.1
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.
- package/CHANGELOG.md +33 -1
- package/dist/packages/canvas-adapter/dieList.d.ts +17 -0
- package/dist/packages/canvas-adapter/dieList.js +7 -7
- package/dist/packages/canvas-adapter/icons.js +1 -1
- package/dist/packages/canvas-adapter/index.d.ts +4 -0
- package/dist/packages/canvas-adapter/index.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.js +2 -2
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toolbar.d.ts +55 -0
- package/dist/packages/canvas-adapter/toolbar.js +2 -2
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +42 -4
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +5 -1
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/stats/analyzeWaferLot.js +1 -1
- package/dist/packages/stats/reportHtml.d.ts +6 -4
- package/dist/packages/stats/types.d.ts +14 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{hasPosition as Zt}from"../core/dies.js";import{getReticleCell as te}from"../core/reticle.js";import{metadataCategoricalValue as ee,metadataDisplayValue as ne}from"../core/metadata.js";import{affineRotation as dt,affineMirror as Rt,affineCompose as it,affinePoint as Y,affineVector as ht,affineSwapsAxes as ae}from"../core/transforms.js";import{contrastTextColor as G,SPEC_PASS_FILL as pt,SPEC_FAIL_LOW as ie,SPEC_FAIL_HIGH as mt}from"./colorMap.js";import{getColorScheme as se}from"./colorSchemes.js";import{getDieTestValue as R,getTestPassStatus as Q,isParametricTest as yt}from"./buildWaferMap.js";import{fmt as gt,fmtColorbarAxis as xt,fmtAggregationMethod as Ct}from"./fmt.js";import{metadataValueColor as oe}from"./colorMap.js";import{compareNatural as re,clamp01 as Nt}from"../core/utils.js";import{prettyKey as Ht}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function findTestDef(t,e){return t?.find(a=>a.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,a)=>e-a)}const le="#d3d6db",Ot="#e8e9ea",ce="#eceef0",j="#d6d9dd";function st(t,e){return e?ee(t.metadata?.[e]):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function ue(t,e,a){const{x:i,y:s}=t.center,n=dt(t.orientation,i,s),o=it(Rt(e?.x??!1,e?.y??!1,i,s),dt(t.orientation,i,s)),r=it(Rt(a?.flipX??!1,a?.flipY??!1,i,s),dt(a?.rotation??0,i,s));return{bakedToScreen:r,physicalToScreen:it(r,n),gridToScreen:it(r,o)}}function W(t,e=!1){return{points:[t],closed:e}}function fe(t,e){const{center:a,radius:i,notch:s}=t;let n=a.x+i*Math.cos(e),o=a.y+i*Math.sin(e);if(!s)return{x:n,y:o};const r=Math.sqrt(i**2-(s.length/2)**2),u=s.length/2,x=n-a.x,y=o-a.y;return s.type==="bottom"&&y<-r?(o=a.y-r,n=a.x+Math.max(-u,Math.min(u,x))):s.type==="top"&&y>r?(o=a.y+r,n=a.x+Math.max(-u,Math.min(u,x))):s.type==="left"&&x<-r?(n=a.x-r,o=a.y+Math.max(-u,Math.min(u,y))):s.type==="right"&&x>r&&(n=a.x+r,o=a.y+Math.max(-u,Math.min(u,y))),{x:n,y:o}}function At(t,e){return t.map(e).join(" / ")}function de(t,e=256){const a=Array.from({length:e},(i,s)=>t(s/(e-1)));return i=>a[Math.min(e-1,Math.max(0,Math.round(i*(e-1))))]}function he(t,e){const a=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(a*.55,i*1.8))))}function pe(t,e,a){if(e?.length){const i=e.flatMap(s=>{const n=s.testNumber;if(!yt(s)){const u=Q(t,n,s);return u===void 0?[]:[{key:n,label:s.name,value:u?"Pass":"Fail"}]}const o=R(t,n);if(o===void 0)return[];const r=t.testPass?.[n]===!1?" <i>(recorded fail)</i>":"";return[{key:n,label:s.name,value:`${gt(o,s.unit,a)}${r}`}]});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:gt(s,void 0,a)})):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,a={}){const{testDefs:i,hbinDefs:s,sbinDefs:n,fallbackFormat:o,aggrMethod:r,lotSize:u,waferMeta:x,activeTest:y,reticleConfig:B,metadataFields:V}=a,v=s?new Map(s.map(c=>[c.bin,c])):null,T=n?new Map(n.map(c=>[c.bin,c])):null,m=[`Die (${t.x}, ${t.y})`];if(B&&Zt(t)){const c=te(t,B);m.push(`Reticle (${c.column}, ${c.row})`)}if(e==="stackedValues"){const c=R(t,0);if(c!==void 0){const f=i?.[0],h=f?.testNumber,g=f?.name??(h!=null?`Test ${h}`:"Value"),M=r?` (${r})`:"";m.push(`${g}${M}: ${gt(c,f?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const c=R(t,0),f=e==="stackedSoftBins"?t.sbin:t.hbin;if(c!==void 0){const h=e==="stackedSoftBins"?T:v,g=f!==void 0?h?.get(f)?.name?`${f} \xB7 ${h.get(f).name}`:`Bin ${f}`:"Bin";let M;if(r==="percent")M=`${c.toFixed(0)}%`;else{const H=u?` (${(c/u*100).toFixed(0)}% of lot)`:"";M=`${c}${H}`}const p=r?` [${Ct(r)}]`:"";m.push(`${g}: ${M}${p}`)}}else{const c=pe(t,i,o);if(e==="value"&&c.length){const{testNumber:f}=resolveTestNumber(y??0,i),h=findTestDef(i,f),g=R(t,f);let M=c.findIndex(E=>E.key===f);M<0&&(M=0);const p=c[M];let H=`<b>${p.label}: ${p.value}</b>`;if(p.key===f&&h){const E=classifySpec(g,h);(E==="failLow"||E==="failHigh")&&(H+=" <i>(out of spec)</i>")}m.push(H);const A=c.length-1;A>0&&m.push(`<i>+${A} more test${A===1?"":"s"}</i>`)}else if(c.length){const f=c.length;m.push(`<i>${f} test result${f===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const f=[];if(t.hbin!==void 0){const h=v?.get(t.hbin)?.name,g=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);f.push(`HBin: ${g}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,g=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);f.push(`SBin: ${g}`)}m.push(f.join(" \xB7 "))}}t.retestCount!==void 0&&m.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&m.push(`Site: ${t.siteNum}`),t.partId!==void 0&&m.push(`Part ID: ${t.partId}`),t.partial&&m.push("<i>partial die</i>"),t.probeIndex!==void 0&&m.push(`Probe: #${t.probeIndex}`);const S={...x??{},...t.metadata??{}};for(const[c,f]of Object.entries(S)){const h=ne(f);if(h===void 0)continue;const g=V?.find(M=>M.key===c)?.label??Ht(c);m.push(`${g}: ${h}`)}return m.join("<br>")}export function buildMapTitle(t,e="engineering",a){const i=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=n=>{const o=a?.length===1?a[0]:void 0;return o===void 0?`${n} Bin`:o.name?`${n} Bin ${o.bin} \xB7 ${o.name}`:`${n} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:i};case"stackedSoftBins":return{primary:s("Soft"),secondary:i};case"metadata":{const n=t.metadataFields?.find(r=>r.key===t.activeMetadataKey),o=t.activeMetadataKey?Ht(t.activeMetadataKey):"Metadata";return{primary:n?.label??o,secondary:""}}case"stackedValues":{const n=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:r}=xt(o,n?.name,n?.unit,e),u=r||"Value",x=t.aggrMethod?` \xB7 ${Ct(t.aggrMethod)}`:"";return{primary:`${u}${x}`,secondary:i}}default:{const n=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:r}=xt(o,n?.name,n?.unit,e),u=r||n?.name;if(t.passFailDisplay!=="off"){const x=n?.testNumber??t.activeTest,y=u?`${u} \xB7 #${x}`:`Test ${x}`,B=t.passFailDisplay==="spec"?"Spec pass/fail":n!==void 0&&!yt(n)?"Functional pass/fail":"Tester pass/fail";return{primary:y,secondary:B}}return{primary:u??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,a){const{plotMode:i,colorFns:s,normalize:n,activeTest:o,valueRange:r,testDefs:u,fallbackFormat:x,passFailDisplay:y="off",activeMetadataKey:B,metadataColorMap:V}=a,v=findTestDef(u,o),{tickFmt:T}=xt(r[1],v?.name,v?.unit,x),{testNumber:m}=resolveTestNumber(o,u);return t.flatMap((S,c)=>{let f="",h="#111111";if(i==="value"&&y==="test"){const p=Q(S,m,v);if(p===void 0)return[];f=p?"P":"F",h=G(p?pt:mt)}else if(i==="value"){const p=R(S,m);if(p===void 0)return[];f=At([p],T),h=G(s.forValue(n(p)))}else if(i==="hardBin"||i==="softBin"){const p=i==="softBin"?S.sbin:S.hbin;if(p===void 0)return[];f=String(p),h=G(s.forBin(p))}else if(i==="metadata"){const p=st(S,B);if(p===void 0)return[];f=p,h=G(V?.get(f)??j)}else{const p=R(S,0);if(p===void 0)return[];f=At([p],T),h=G(s.forValue(n(p)))}const g=e?e[c*2]:S.physX,M=e?e[c*2+1]:S.physY;return[{x:g,y:M,text:f,fontSize:he(S,f),color:h,align:"center"}]})}function Et(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function me(t,e,a=720){const{center:i,radius:s,notch:n}=t,o=m=>Y(e,m.x,m.y);if(!(n!==void 0&&t.diameter>150)){const m=[];for(let S=0;S<=a;S++){const c=2*Math.PI*S/a;m.push(o(fe(t,c)))}return[{kind:"wafer-boundary",...W(m,!0),lineColor:"#888888",lineWidth:1}]}const u=1.25,x=n.length,y=Et(n.type),B=Math.atan2(x,s),V=s-u,v=[];let T=!1;for(let m=0;m<=a;m++){const S=2*Math.PI*m/a;let c=S-y;for(;c>Math.PI;)c-=2*Math.PI;for(;c<-Math.PI;)c+=2*Math.PI;if(Math.abs(c)<=B){if(!T){T=!0;const f={x:i.x+s*Math.cos(y-B),y:i.y+s*Math.sin(y-B)},h={x:i.x+V*Math.cos(y),y:i.y+V*Math.sin(y)},g={x:i.x+s*Math.cos(y+B),y:i.y+s*Math.sin(y+B)};v.push(o(f),o(h),o(g))}continue}v.push(o({x:i.x+s*Math.cos(S),y:i.y+s*Math.sin(S)}))}return[{kind:"wafer-boundary",...W(v,!0),lineColor:"#888888",lineWidth:1}]}function ye(t,e,a,i=360){const s=[],n=Math.max(1,Math.floor(a));for(let o=1;o<n;o++){const r=t.radius*o/n,u=[];for(let x=0;x<=i;x++){const y=2*Math.PI*x/i,B={x:t.center.x+r*Math.cos(y),y:t.center.y+r*Math.sin(y)};u.push(Y(e,B.x,B.y))}s.push({kind:"ring-boundary",...W(u,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function ge(t,e,a,i){const{x:s,y:n}=t.center,o=t.radius,r=(S,c)=>Y(e,S,c),u=a-s,x=Math.sqrt(Math.max(0,o*o-u*u)),y=r(a,n-x),B=r(a,n+x),V=i-n,v=Math.sqrt(Math.max(0,o*o-V*V)),T=r(s-v,i),m=r(s+v,i);return[{kind:"quadrant-boundary",...W([y,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...W([T,m]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function xe(t,e){return t.map(a=>{const i=a.width/2,s=a.height/2;return{kind:"reticle",points:[[[a.x-i,a.y-s],[a.x+i,a.y-s],[a.x+i,a.y+s],[a.x-i,a.y+s]].map(([o,r])=>Y(e,o,r))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function be(t){const e=t.filter(a=>a.probeIndex!==void 0).sort((a,i)=>(a.probeIndex??0)-(i.probeIndex??0));return e.length?[{kind:"probe-path",...W(e.map(a=>({x:a.physX,y:a.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function Me(t,e,a,i,s,n,o,r,u,x,y,B,V,v,T="off",m,S,c){const f=e.width-o,h=e.height-o,g=n?h:f,M=n?f:h,p=$=>s==="softBin"?$.sbin:$.hbin,H=$=>st($,m);if(e.partial){t.push({x:a,y:i,width:g,height:M,fill:le,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:a,y:i,width:g,height:M,fill:ce,type:"stacked",metadata:e.metadata});return}if(u!==void 0&&(s==="hardBin"||s==="softBin")&&p(e)!==u){t.push({x:a,y:i,width:g,height:M,fill:Ot,type:"hardBin",metadata:e.metadata});return}if(c!==void 0&&s==="metadata"&&H(e)!==c){t.push({x:a,y:i,width:g,height:M,fill:Ot,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(T==="test"){const J=Q(e,y,v),ot=J===void 0?j:J?pt:mt;t.push({x:a,y:i,width:g,height:M,fill:ot,type:"value",metadata:e.metadata});return}const $=R(e,y),D=classifySpec($,v);let _,U;$===void 0?_=j:T==="spec"?_=D==="failLow"?ie:D==="failHigh"?mt:pt:(_=r.forValue(x($)),(D==="failLow"||D==="failHigh")&&(U=D)),t.push({x:a,y:i,width:g,height:M,fill:_,type:"value",specMark:U,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const $=p(e),D=$!=null?r.forBin($):j,_=$!=null&&!V.has($)?!0:void 0;t.push({x:a,y:i,width:g,height:M,fill:D,type:s,binFail:_,metadata:e.metadata});return}if(s==="metadata"){const $=H(e),D=$!==void 0?S?.get($)??j:j;t.push({x:a,y:i,width:g,height:M,fill:D,type:"metadata",metadata:e.metadata});return}const A=R(e,0),E=A!==void 0?r.forValue(x(A)):j;t.push({x:a,y:i,width:g,height:M,fill:E,type:"value",metadata:e.metadata})}function Se(t,e,a){const i=t.radius*.15,s=ht(e,i,0),n=ht(e,0,i),o=s.x+n.x>=0?-1:1,r=s.y+n.y>=0?-1:1,u={x:t.center.x+o*t.radius*.9,y:t.center.y+r*t.radius*.9},x={x:u.x+s.x,y:u.y+s.y},y={x:u.x+n.x,y:u.y+n.y};return a.push({x:x.x+s.x*.35,y:x.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:y.x+n.x*.35,y:y.y+n.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...W([u,x]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...W([u,y]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,a={},i){const s=a.reticles??[],{plotMode:n="value",showDieLabels:o=!1,showReticle:r=!1,showProbePath:u=!1,ringCount:x=4,showRingBoundaries:y=!1,showQuadrantBoundaries:B=!1,showXYIndicator:V=!1,dieGap:v=1,colorScheme:T="default",passBins:m=[1],highlightBin:S,highlightMetadataValue:c,interactiveTransform:f,valueRange:h,testDefs:g,activeTest:M=0,activeMetadataKey:p,fallbackFormat:H="engineering",aggregationMethod:A,lotSize:E,dataAxisFlip:$,isLotStack:D=!1,logScale:_,colorbarRangeMode:U="spec",passFailDisplay:J,showPartialDies:ot=!0}=a,bt=J??"off",Mt=i?.hbinDefs,St=i?.sbinDefs,$t=i?.metadataFields,_t=$t?.find(l=>l.key===p),wt={x:($?.x??!1)!==(f?.flipX??!1),y:($?.y??!1)!==(f?.flipY??!1)},Wt=Mt?new Map(Mt.map(l=>[l.bin,l])):null,jt=St?new Map(St.map(l=>[l.bin,l])):null,rt=n==="softBin"?jt:Wt,lt=se(T),zt=new Set(m),kt={forValue:de(lt.forValue),forBin:T==="custom"&&rt?l=>rt.get(l)?.color??lt.forBin(l):lt.forBin};let{testNumber:O}=resolveTestNumber(M,g);if(!g?.length&&n==="value"&&!e.some(d=>d.testValues&&O in d.testValues)){const d=e.reduce((b,F)=>{if(!F.testValues)return b;const k=Object.keys(F.testValues).map(Number),q=k.length?Math.min(...k):void 0;return q!==void 0&&(b===void 0||q<b)?q:b},void 0);d!==void 0&&(O=d)}let Z;if(Array.isArray(h))Z=h;else if(h){const{testNumber:l}=resolveTestNumber(h.test,g);Z=l===O?h.range:void 0}const I=findTestDef(g,O),Bt=n==="value"&&I!==void 0&&!yt(I),Kt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let C="off";n==="value"&&(Bt?C="test":bt==="spec"&&Kt?C="spec":bt==="test"&&e.some(l=>Q(l,O,I)!==void 0)&&(C="test"));const vt=C==="spec"?"spec":U;let P,N,Tt=!1;if(Z)[P,N]=Z;else{const l=n==="stackedValues"||n==="stackedBins"||n==="stackedSoftBins",d=!l&&n==="value"&&vt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let b=1/0,F=-1/0,k=!0;for(const q of e){const X=l?R(q,0):R(q,O);X!==void 0&&(X<b&&(b=X),X>F&&(F=X),Number.isInteger(X)||(k=!1))}isFinite(b)||(k=!1),Tt=k,d?(P=I.limitLow!==void 0?I.limitLow:isFinite(b)?b:0,N=I.limitHigh!==void 0?I.limitHigh:isFinite(F)?F:1):(P=isFinite(b)?b:0,N=isFinite(F)?F:1)}P===N&&(N=P+1);const It=Bt?!1:_??I?.logScale??!1,Ft=It&&P>0&&N>0,qt=Ft;let tt;if(Ft){const l=Math.log10(P),d=Math.log10(N)-l;tt=b=>b<=0?0:Nt((Math.log10(b)-l)/d)}else tt=l=>Nt((l-P)/(N-P));const L=ue(t,$,f);let Lt=null;if(t.notch){const l=Et(t.notch.type),d=ht(L.physicalToScreen,Math.cos(l)*t.radius,Math.sin(l)*t.radius),b=Math.hypot(d.x,d.y);b>0&&(Lt={x:d.x/b,y:d.y/b})}let z=1/0;for(let l=0;l<e.length;l++){const d=e[l];d.width<z&&(z=d.width),d.height<z&&(z=d.height)}const Xt=z!==1/0?Math.min(v,z*.12):v,Vt=[],ct=[],Yt=L.bakedToScreen.a===1&&L.bakedToScreen.b===0&&L.bakedToScreen.c===0&&L.bakedToScreen.d===1&&L.bakedToScreen.e===0&&L.bakedToScreen.f===0;let w=null;if(!Yt){w=new Float64Array(e.length*2);for(let l=0;l<e.length;l++){const d=Y(L.bakedToScreen,e[l].physX,e[l].physY);w[l*2]=d.x,w[l*2+1]=d.y}}const Gt=ae(L.gridToScreen),Qt=n==="hardBin"||n==="softBin"||n==="stackedBins"||n==="stackedSoftBins",Ut=n==="softBin"||n==="stackedSoftBins";let et=Qt?new Map:void 0;const ut=n==="value"&&C==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ft=n==="value"&&C==="test"?{pass:0,fail:0}:void 0,Dt=n==="metadata";let nt=Dt?new Map:void 0,at=null;if(Dt&&p){const l=new Set;for(const b of e){if(b.partial||b.edgeExcluded)continue;const F=st(b,p);F!==void 0&&l.add(F)}const d=[...l].sort(re);at=new Map(d.map((b,F)=>[b,_t?.values?.find(k=>k.value===b)?.color??oe(F)]))}for(let l=0;l<e.length;l++){const d=e[l],b=w?w[l*2]:d.physX,F=w?w[l*2+1]:d.physY;if(ct.push({x:b,y:F}),et&&!d.partial&&!d.edgeExcluded){const k=Ut?d.sbin:d.hbin;k!=null&&et.set(k,(et.get(k)??0)+1)}if(ut&&!d.partial&&!d.edgeExcluded){const k=classifySpec(R(d,O),I);k&&ut[k]++}if(ft&&!d.partial&&!d.edgeExcluded){const k=Q(d,O,I);k!==void 0&&ft[k?"pass":"fail"]++}if(nt&&!d.partial&&!d.edgeExcluded){const k=st(d,p);k!==void 0&&nt.set(k,(nt.get(k)??0)+1)}d.partial&&!ot||Me(Vt,d,b,F,n,Gt,Xt,kt,S,tt,O,rt,zt,I,C,p,at,c)}const Pt=o?generateTextOverlay(e,w,{plotMode:n,colorFns:kt,normalize:tt,activeTest:M,valueRange:[P,N],testDefs:g,fallbackFormat:H,passFailDisplay:C,activeMetadataKey:p,metadataColorMap:at}):[],K=me(t,L.physicalToScreen);y&&K.push(...ye(t,L.physicalToScreen,x)),B&&K.push(...ge(t,L.physicalToScreen,t.center.x,t.center.y)),r&&K.push(...xe(s,L.gridToScreen)),u&&K.push(...be(e)),V&&K.push(...Se(t,L.gridToScreen,Pt));const Jt=ct.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:Vt,hoverPoints:ct,texts:Pt,overlays:K,plotMode:n,colorScheme:T,metadata:t.metadata??null,dies:e,valueRange:[P,N],allIntegerValues:Tt,testDefs:g,activeTest:M,logScale:qt,logScaleRequested:It,aggrMethod:A,lotSize:E,axisFlip:wt,dataAxisFlip:{x:$?.x??!1,y:$?.y??!1},gridToScreen:L.gridToScreen,rotation:((t.orientation+(f?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:D,passFailDisplay:C,colorBySpec:C==="spec",colorbarRangeMode:vt,waferCenter:t.center,waferRadius:t.radius,notchDir:Lt,binCounts:et,metadataCounts:nt,metadataColorMap:at,activeMetadataKey:p,metadataFields:$t,specCounts:ut,passFailCounts:ft,dieBounds:Jt}}export{getDieKey}from"../core/dies.js";
|
|
1
|
+
import{hasPosition as Zt}from"../core/dies.js";import{getReticleCell as te}from"../core/reticle.js";import{metadataCategoricalValue as ee,metadataDisplayValue as ne}from"../core/metadata.js";import{affineRotation as dt,affineMirror as Rt,affineCompose as it,affinePoint as Y,affineVector as ht,affineSwapsAxes as ae}from"../core/transforms.js";import{contrastTextColor as G,SPEC_PASS_FILL as pt,SPEC_FAIL_LOW as ie,SPEC_FAIL_HIGH as mt}from"./colorMap.js";import{getColorScheme as se}from"./colorSchemes.js";import{getDieTestValue as R,getTestPassStatus as Q,isParametricTest as yt}from"./buildWaferMap.js";import{fmt as gt,fmtColorbarAxis as bt,fmtAggregationMethod as Ct}from"./fmt.js";import{metadataValueColor as oe}from"./colorMap.js";import{compareNatural as re,clamp01 as Nt}from"../core/utils.js";import{prettyKey as Ht}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function findTestDef(t,e){return t?.find(a=>a.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,a)=>e-a)}const le="#d3d6db",Ot="#e8e9ea",ce="#aab0ba",j="#d6d9dd";function st(t,e){return e?ee(t.metadata?.[e]):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function ue(t,e,a){const{x:i,y:s}=t.center,n=dt(t.orientation,i,s),o=it(Rt(e?.x??!1,e?.y??!1,i,s),dt(t.orientation,i,s)),r=it(Rt(a?.flipX??!1,a?.flipY??!1,i,s),dt(a?.rotation??0,i,s));return{bakedToScreen:r,physicalToScreen:it(r,n),gridToScreen:it(r,o)}}function W(t,e=!1){return{points:[t],closed:e}}function fe(t,e){const{center:a,radius:i,notch:s}=t;let n=a.x+i*Math.cos(e),o=a.y+i*Math.sin(e);if(!s)return{x:n,y:o};const r=Math.sqrt(i**2-(s.length/2)**2),u=s.length/2,b=n-a.x,y=o-a.y;return s.type==="bottom"&&y<-r?(o=a.y-r,n=a.x+Math.max(-u,Math.min(u,b))):s.type==="top"&&y>r?(o=a.y+r,n=a.x+Math.max(-u,Math.min(u,b))):s.type==="left"&&b<-r?(n=a.x-r,o=a.y+Math.max(-u,Math.min(u,y))):s.type==="right"&&b>r&&(n=a.x+r,o=a.y+Math.max(-u,Math.min(u,y))),{x:n,y:o}}function At(t,e){return t.map(e).join(" / ")}function de(t,e=256){const a=Array.from({length:e},(i,s)=>t(s/(e-1)));return i=>a[Math.min(e-1,Math.max(0,Math.round(i*(e-1))))]}function he(t,e){const a=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(a*.55,i*1.8))))}function pe(t,e,a){if(e?.length){const i=e.flatMap(s=>{const n=s.testNumber;if(!yt(s)){const u=Q(t,n,s);return u===void 0?[]:[{key:n,label:s.name,value:u?"Pass":"Fail"}]}const o=R(t,n);if(o===void 0)return[];const r=t.testPass?.[n]===!1?" <i>(recorded fail)</i>":"";return[{key:n,label:s.name,value:`${gt(o,s.unit,a)}${r}`}]});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:gt(s,void 0,a)})):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,a={}){const{testDefs:i,hbinDefs:s,sbinDefs:n,fallbackFormat:o,aggrMethod:r,lotSize:u,waferMeta:b,activeTest:y,reticleConfig:B,metadataFields:V}=a,v=s?new Map(s.map(c=>[c.bin,c])):null,T=n?new Map(n.map(c=>[c.bin,c])):null,m=[`Die (${t.x}, ${t.y})`];if(B&&Zt(t)){const c=te(t,B);m.push(`Reticle (${c.column}, ${c.row})`)}if(e==="stackedValues"){const c=R(t,0);if(c!==void 0){const f=i?.[0],h=f?.testNumber,g=f?.name??(h!=null?`Test ${h}`:"Value"),M=r?` (${r})`:"";m.push(`${g}${M}: ${gt(c,f?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const c=R(t,0),f=e==="stackedSoftBins"?t.sbin:t.hbin;if(c!==void 0){const h=e==="stackedSoftBins"?T:v,g=f!==void 0?h?.get(f)?.name?`${f} \xB7 ${h.get(f).name}`:`Bin ${f}`:"Bin";let M;if(r==="percent")M=`${c.toFixed(0)}%`;else{const H=u?` (${(c/u*100).toFixed(0)}% of lot)`:"";M=`${c}${H}`}const p=r?` [${Ct(r)}]`:"";m.push(`${g}: ${M}${p}`)}}else{const c=pe(t,i,o);if(e==="value"&&c.length){const{testNumber:f}=resolveTestNumber(y??0,i),h=findTestDef(i,f),g=R(t,f);let M=c.findIndex(E=>E.key===f);M<0&&(M=0);const p=c[M];let H=`<b>${p.label}: ${p.value}</b>`;if(p.key===f&&h){const E=classifySpec(g,h);(E==="failLow"||E==="failHigh")&&(H+=" <i>(out of spec)</i>")}m.push(H);const A=c.length-1;A>0&&m.push(`<i>+${A} more test${A===1?"":"s"}</i>`)}else if(c.length){const f=c.length;m.push(`<i>${f} test result${f===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const f=[];if(t.hbin!==void 0){const h=v?.get(t.hbin)?.name,g=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);f.push(`HBin: ${g}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,g=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);f.push(`SBin: ${g}`)}m.push(f.join(" \xB7 "))}}t.retestCount!==void 0&&m.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&m.push(`Site: ${t.siteNum}`),t.partId!==void 0&&m.push(`Part ID: ${t.partId}`),t.partial&&m.push("<i>partial die</i>"),t.probeIndex!==void 0&&m.push(`Probe: #${t.probeIndex}`);const S={...b??{},...t.metadata??{}};for(const[c,f]of Object.entries(S)){const h=ne(f);if(h===void 0)continue;const g=V?.find(M=>M.key===c)?.label??Ht(c);m.push(`${g}: ${h}`)}return m.join("<br>")}export function buildMapTitle(t,e="engineering",a){const i=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=n=>{const o=a?.length===1?a[0]:void 0;return o===void 0?`${n} Bin`:o.name?`${n} Bin ${o.bin} \xB7 ${o.name}`:`${n} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:i};case"stackedSoftBins":return{primary:s("Soft"),secondary:i};case"metadata":{const n=t.metadataFields?.find(r=>r.key===t.activeMetadataKey),o=t.activeMetadataKey?Ht(t.activeMetadataKey):"Metadata";return{primary:n?.label??o,secondary:""}}case"stackedValues":{const n=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:r}=bt(o,n?.name,n?.unit,e),u=r||"Value",b=t.aggrMethod?` \xB7 ${Ct(t.aggrMethod)}`:"";return{primary:`${u}${b}`,secondary:i}}default:{const n=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:r}=bt(o,n?.name,n?.unit,e),u=r||n?.name;if(t.passFailDisplay!=="off"){const b=n?.testNumber??t.activeTest,y=u?`${u} \xB7 #${b}`:`Test ${b}`,B=t.passFailDisplay==="spec"?"Spec pass/fail":n!==void 0&&!yt(n)?"Functional pass/fail":"Tester pass/fail";return{primary:y,secondary:B}}return{primary:u??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,a){const{plotMode:i,colorFns:s,normalize:n,activeTest:o,valueRange:r,testDefs:u,fallbackFormat:b,passFailDisplay:y="off",activeMetadataKey:B,metadataColorMap:V}=a,v=findTestDef(u,o),{tickFmt:T}=bt(r[1],v?.name,v?.unit,b),{testNumber:m}=resolveTestNumber(o,u);return t.flatMap((S,c)=>{let f="",h="#111111";if(i==="value"&&y==="test"){const p=Q(S,m,v);if(p===void 0)return[];f=p?"P":"F",h=G(p?pt:mt)}else if(i==="value"){const p=R(S,m);if(p===void 0)return[];f=At([p],T),h=G(s.forValue(n(p)))}else if(i==="hardBin"||i==="softBin"){const p=i==="softBin"?S.sbin:S.hbin;if(p===void 0)return[];f=String(p),h=G(s.forBin(p))}else if(i==="metadata"){const p=st(S,B);if(p===void 0)return[];f=p,h=G(V?.get(f)??j)}else{const p=R(S,0);if(p===void 0)return[];f=At([p],T),h=G(s.forValue(n(p)))}const g=e?e[c*2]:S.physX,M=e?e[c*2+1]:S.physY;return[{x:g,y:M,text:f,fontSize:he(S,f),color:h,align:"center"}]})}function Et(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function me(t,e,a=720){const{center:i,radius:s,notch:n}=t,o=m=>Y(e,m.x,m.y);if(!(n!==void 0&&t.diameter>150)){const m=[];for(let S=0;S<=a;S++){const c=2*Math.PI*S/a;m.push(o(fe(t,c)))}return[{kind:"wafer-boundary",...W(m,!0),lineColor:"#888888",lineWidth:1}]}const u=1.25,b=n.length,y=Et(n.type),B=Math.atan2(b,s),V=s-u,v=[];let T=!1;for(let m=0;m<=a;m++){const S=2*Math.PI*m/a;let c=S-y;for(;c>Math.PI;)c-=2*Math.PI;for(;c<-Math.PI;)c+=2*Math.PI;if(Math.abs(c)<=B){if(!T){T=!0;const f={x:i.x+s*Math.cos(y-B),y:i.y+s*Math.sin(y-B)},h={x:i.x+V*Math.cos(y),y:i.y+V*Math.sin(y)},g={x:i.x+s*Math.cos(y+B),y:i.y+s*Math.sin(y+B)};v.push(o(f),o(h),o(g))}continue}v.push(o({x:i.x+s*Math.cos(S),y:i.y+s*Math.sin(S)}))}return[{kind:"wafer-boundary",...W(v,!0),lineColor:"#888888",lineWidth:1}]}function ye(t,e,a,i=360){const s=[],n=Math.max(1,Math.floor(a));for(let o=1;o<n;o++){const r=t.radius*o/n,u=[];for(let b=0;b<=i;b++){const y=2*Math.PI*b/i,B={x:t.center.x+r*Math.cos(y),y:t.center.y+r*Math.sin(y)};u.push(Y(e,B.x,B.y))}s.push({kind:"ring-boundary",...W(u,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function ge(t,e,a,i){const{x:s,y:n}=t.center,o=t.radius,r=(S,c)=>Y(e,S,c),u=a-s,b=Math.sqrt(Math.max(0,o*o-u*u)),y=r(a,n-b),B=r(a,n+b),V=i-n,v=Math.sqrt(Math.max(0,o*o-V*V)),T=r(s-v,i),m=r(s+v,i);return[{kind:"quadrant-boundary",...W([y,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...W([T,m]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function be(t,e){return t.map(a=>{const i=a.width/2,s=a.height/2;return{kind:"reticle",points:[[[a.x-i,a.y-s],[a.x+i,a.y-s],[a.x+i,a.y+s],[a.x-i,a.y+s]].map(([o,r])=>Y(e,o,r))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function xe(t){const e=t.filter(a=>a.probeIndex!==void 0).sort((a,i)=>(a.probeIndex??0)-(i.probeIndex??0));return e.length?[{kind:"probe-path",...W(e.map(a=>({x:a.physX,y:a.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function Me(t,e,a,i,s,n,o,r,u,b,y,B,V,v,T="off",m,S,c){const f=e.width-o,h=e.height-o,g=n?h:f,M=n?f:h,p=$=>s==="softBin"?$.sbin:$.hbin,H=$=>st($,m);if(e.partial){t.push({x:a,y:i,width:g,height:M,fill:le,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:a,y:i,width:g,height:M,fill:ce,type:"stacked",metadata:e.metadata});return}if(u!==void 0&&(s==="hardBin"||s==="softBin")&&p(e)!==u){t.push({x:a,y:i,width:g,height:M,fill:Ot,type:"hardBin",metadata:e.metadata});return}if(c!==void 0&&s==="metadata"&&H(e)!==c){t.push({x:a,y:i,width:g,height:M,fill:Ot,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(T==="test"){const J=Q(e,y,v),ot=J===void 0?j:J?pt:mt;t.push({x:a,y:i,width:g,height:M,fill:ot,type:"value",metadata:e.metadata});return}const $=R(e,y),D=classifySpec($,v);let _,U;$===void 0?_=j:T==="spec"?_=D==="failLow"?ie:D==="failHigh"?mt:pt:(_=r.forValue(b($)),(D==="failLow"||D==="failHigh")&&(U=D)),t.push({x:a,y:i,width:g,height:M,fill:_,type:"value",specMark:U,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const $=p(e),D=$!=null?r.forBin($):j,_=$!=null&&!V.has($)?!0:void 0;t.push({x:a,y:i,width:g,height:M,fill:D,type:s,binFail:_,metadata:e.metadata});return}if(s==="metadata"){const $=H(e),D=$!==void 0?S?.get($)??j:j;t.push({x:a,y:i,width:g,height:M,fill:D,type:"metadata",metadata:e.metadata});return}const A=R(e,0),E=A!==void 0?r.forValue(b(A)):j;t.push({x:a,y:i,width:g,height:M,fill:E,type:"value",metadata:e.metadata})}function Se(t,e,a){const i=t.radius*.15,s=ht(e,i,0),n=ht(e,0,i),o=s.x+n.x>=0?-1:1,r=s.y+n.y>=0?-1:1,u={x:t.center.x+o*t.radius*.9,y:t.center.y+r*t.radius*.9},b={x:u.x+s.x,y:u.y+s.y},y={x:u.x+n.x,y:u.y+n.y};return a.push({x:b.x+s.x*.35,y:b.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:y.x+n.x*.35,y:y.y+n.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...W([u,b]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...W([u,y]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,a={},i){const s=a.reticles??[],{plotMode:n="value",showDieLabels:o=!1,showReticle:r=!1,showProbePath:u=!1,ringCount:b=4,showRingBoundaries:y=!1,showQuadrantBoundaries:B=!1,showXYIndicator:V=!1,dieGap:v=1,colorScheme:T="default",passBins:m=[1],highlightBin:S,highlightMetadataValue:c,interactiveTransform:f,valueRange:h,testDefs:g,activeTest:M=0,activeMetadataKey:p,fallbackFormat:H="engineering",aggregationMethod:A,lotSize:E,dataAxisFlip:$,isLotStack:D=!1,logScale:_,colorbarRangeMode:U="spec",passFailDisplay:J,showPartialDies:ot=!0}=a,xt=J??"off",Mt=i?.hbinDefs,St=i?.sbinDefs,$t=i?.metadataFields,_t=$t?.find(l=>l.key===p),wt={x:($?.x??!1)!==(f?.flipX??!1),y:($?.y??!1)!==(f?.flipY??!1)},Wt=Mt?new Map(Mt.map(l=>[l.bin,l])):null,jt=St?new Map(St.map(l=>[l.bin,l])):null,rt=n==="softBin"?jt:Wt,lt=se(T),zt=new Set(m),kt={forValue:de(lt.forValue),forBin:T==="custom"&&rt?l=>rt.get(l)?.color??lt.forBin(l):lt.forBin};let{testNumber:O}=resolveTestNumber(M,g);if(!g?.length&&n==="value"&&!e.some(d=>d.testValues&&O in d.testValues)){const d=e.reduce((x,F)=>{if(!F.testValues)return x;const k=Object.keys(F.testValues).map(Number),q=k.length?Math.min(...k):void 0;return q!==void 0&&(x===void 0||q<x)?q:x},void 0);d!==void 0&&(O=d)}let Z;if(Array.isArray(h))Z=h;else if(h){const{testNumber:l}=resolveTestNumber(h.test,g);Z=l===O?h.range:void 0}const I=findTestDef(g,O),Bt=n==="value"&&I!==void 0&&!yt(I),Kt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let C="off";n==="value"&&(Bt?C="test":xt==="spec"&&Kt?C="spec":xt==="test"&&e.some(l=>Q(l,O,I)!==void 0)&&(C="test"));const vt=C==="spec"?"spec":U;let P,N,Tt=!1;if(Z)[P,N]=Z;else{const l=n==="stackedValues"||n==="stackedBins"||n==="stackedSoftBins",d=!l&&n==="value"&&vt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let x=1/0,F=-1/0,k=!0;for(const q of e){const X=l?R(q,0):R(q,O);X!==void 0&&(X<x&&(x=X),X>F&&(F=X),Number.isInteger(X)||(k=!1))}isFinite(x)||(k=!1),Tt=k,d?(P=I.limitLow!==void 0?I.limitLow:isFinite(x)?x:0,N=I.limitHigh!==void 0?I.limitHigh:isFinite(F)?F:1):(P=isFinite(x)?x:0,N=isFinite(F)?F:1)}P===N&&(N=P+1);const It=Bt?!1:_??I?.logScale??!1,Ft=It&&P>0&&N>0,qt=Ft;let tt;if(Ft){const l=Math.log10(P),d=Math.log10(N)-l;tt=x=>x<=0?0:Nt((Math.log10(x)-l)/d)}else tt=l=>Nt((l-P)/(N-P));const L=ue(t,$,f);let Lt=null;if(t.notch){const l=Et(t.notch.type),d=ht(L.physicalToScreen,Math.cos(l)*t.radius,Math.sin(l)*t.radius),x=Math.hypot(d.x,d.y);x>0&&(Lt={x:d.x/x,y:d.y/x})}let z=1/0;for(let l=0;l<e.length;l++){const d=e[l];d.width<z&&(z=d.width),d.height<z&&(z=d.height)}const Xt=z!==1/0?Math.min(v,z*.12):v,Vt=[],ct=[],Yt=L.bakedToScreen.a===1&&L.bakedToScreen.b===0&&L.bakedToScreen.c===0&&L.bakedToScreen.d===1&&L.bakedToScreen.e===0&&L.bakedToScreen.f===0;let w=null;if(!Yt){w=new Float64Array(e.length*2);for(let l=0;l<e.length;l++){const d=Y(L.bakedToScreen,e[l].physX,e[l].physY);w[l*2]=d.x,w[l*2+1]=d.y}}const Gt=ae(L.gridToScreen),Qt=n==="hardBin"||n==="softBin"||n==="stackedBins"||n==="stackedSoftBins",Ut=n==="softBin"||n==="stackedSoftBins";let et=Qt?new Map:void 0;const ut=n==="value"&&C==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ft=n==="value"&&C==="test"?{pass:0,fail:0}:void 0,Dt=n==="metadata";let nt=Dt?new Map:void 0,at=null;if(Dt&&p){const l=new Set;for(const x of e){if(x.partial||x.edgeExcluded)continue;const F=st(x,p);F!==void 0&&l.add(F)}const d=[...l].sort(re);at=new Map(d.map((x,F)=>[x,_t?.values?.find(k=>k.value===x)?.color??oe(F)]))}for(let l=0;l<e.length;l++){const d=e[l],x=w?w[l*2]:d.physX,F=w?w[l*2+1]:d.physY;if(ct.push({x,y:F}),et&&!d.partial&&!d.edgeExcluded){const k=Ut?d.sbin:d.hbin;k!=null&&et.set(k,(et.get(k)??0)+1)}if(ut&&!d.partial&&!d.edgeExcluded){const k=classifySpec(R(d,O),I);k&&ut[k]++}if(ft&&!d.partial&&!d.edgeExcluded){const k=Q(d,O,I);k!==void 0&&ft[k?"pass":"fail"]++}if(nt&&!d.partial&&!d.edgeExcluded){const k=st(d,p);k!==void 0&&nt.set(k,(nt.get(k)??0)+1)}d.partial&&!ot||Me(Vt,d,x,F,n,Gt,Xt,kt,S,tt,O,rt,zt,I,C,p,at,c)}const Pt=o?generateTextOverlay(e,w,{plotMode:n,colorFns:kt,normalize:tt,activeTest:M,valueRange:[P,N],testDefs:g,fallbackFormat:H,passFailDisplay:C,activeMetadataKey:p,metadataColorMap:at}):[],K=me(t,L.physicalToScreen);y&&K.push(...ye(t,L.physicalToScreen,b)),B&&K.push(...ge(t,L.physicalToScreen,t.center.x,t.center.y)),r&&K.push(...be(s,L.gridToScreen)),u&&K.push(...xe(e)),V&&K.push(...Se(t,L.gridToScreen,Pt));const Jt=ct.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:Vt,hoverPoints:ct,texts:Pt,overlays:K,plotMode:n,colorScheme:T,metadata:t.metadata??null,dies:e,valueRange:[P,N],allIntegerValues:Tt,testDefs:g,activeTest:M,logScale:qt,logScaleRequested:It,aggrMethod:A,lotSize:E,axisFlip:wt,dataAxisFlip:{x:$?.x??!1,y:$?.y??!1},gridToScreen:L.gridToScreen,rotation:((t.orientation+(f?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:D,passFailDisplay:C,colorBySpec:C==="spec",colorbarRangeMode:vt,waferCenter:t.center,waferRadius:t.radius,notchDir:Lt,binCounts:et,metadataCounts:nt,metadataColorMap:at,activeMetadataKey:p,metadataFields:$t,specCounts:ut,passFailCounts:ft,dieBounds:Jt}}export{getDieKey}from"../core/dies.js";
|
|
@@ -427,11 +427,15 @@ export interface WaferWarning {
|
|
|
427
427
|
* - `'test-count-capped'` — raised by `analyzeWaferMap`: more tests were found in
|
|
428
428
|
* the die data than the analysis cap allows, so test-value analysis was skipped
|
|
429
429
|
* entirely and NO test findings were produced. Pass `testNumbers` to scope it.
|
|
430
|
+
* - `'edge-exclusion-exceeds-radius'` — `waferConfig.edgeExclusion` is larger than
|
|
431
|
+
* the resolved wafer radius (most likely when the diameter was itself inferred
|
|
432
|
+
* from sparse/partial data). The excluded band is clamped to the whole wafer
|
|
433
|
+
* rather than silently producing a smaller, wrong ring.
|
|
430
434
|
*
|
|
431
435
|
* The union is intentionally open to string so future advisory codes can be
|
|
432
436
|
* added without a breaking change; switch with a `default` branch.
|
|
433
437
|
*/
|
|
434
|
-
code: 'partial-coverage' | 'geometry-conflict' | 'inferred-pitch' | 'test-count-capped' | (string & {});
|
|
438
|
+
code: 'partial-coverage' | 'geometry-conflict' | 'inferred-pitch' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | (string & {});
|
|
435
439
|
/** Human-readable explanation, suitable for direct display. */
|
|
436
440
|
message: string;
|
|
437
441
|
/**
|
|
@@ -1 +1 @@
|
|
|
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
|
+
import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as fe,getDieKey as B,hasPosition as k}from"../core/dies.js";import{applyOrientation as H,transformDies as de}from"../core/transforms.js";import{affineRotation as ce,affineMirror as le,affineCompose as ue,affinePoint as he}from"../core/transforms.js";import{inferWaferFromXY as pe}from"../core/inference/wafer.js";import{resolveGridPitch as me}from"../core/inference/pitch.js";import{assignGridIndices as ye}from"../core/inference/grid.js";import{generateReticleGrid as ge}from"../core/reticle.js";import{buildView as U}from"./buildView.js";import{modeOf as xe}from"../core/utils.js";import{aggregateValues as q,aggregateBinCounts as we}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function be(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],testDefs:t.testDefs,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Me(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function De(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let u=1/0,c=-1/0,a=1/0,l=-1/0;for(const s of t)s.x<u&&(u=s.x),s.x>c&&(c=s.x),s.y<a&&(a=s.y),s.y>l&&(l=s.y);return{offsetX:Math.round((c+u)/2),offsetY:Math.round((l+a)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Oe(t,n,e,u,c,a){if(a)return{colMidX:(Math.round(a.x)-n)*u,colMidY:(Math.round(a.y)-e)*c,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let l=1/0,s=-1/0,h=1/0,p=-1/0;for(const i of t){const d=Math.round(i.x)-n,o=Math.round(i.y)-e;d<l&&(l=d),d>s&&(s=d),o<h&&(h=o),o>p&&(p=o)}return{colMidX:(l+s)/2*u,colMidY:(h+p)/2*c,anchored:!1}}function Se(t,n,e){let u=1/0,c=-1/0,a=1/0,l=-1/0,s=0,h=0;for(const d of t){const o=Math.round(d.x)-n,r=Math.round(d.y)-e;s+=o,h+=r,o<u&&(u=o),o>c&&(c=o),r<a&&(a=r),r>l&&(l=r)}const p=t.length,i=(d,o,r)=>{const m=(r-o)/2;return m<=0?0:Math.abs(d/p-(o+r)/2)/m};return i(s,u,c)>.11||i(h,a,l)>.11}function ke(t,n){let e=t?.xAxisDirection==="left",u=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(u=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:u}}function ve(t,n){const{method:e,targetBin:u}=t,c=t.results.map(a=>a.filter(k));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const a=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),l=new Set;for(const i of c)for(const d of i)if(d.testValues)for(const o of Object.keys(d.testValues)){const r=Number(o);a.has(r)||l.add(r)}if(l.size===0)return q(c,e);const s=[...l],h=s.map(i=>q(c,e,i)),p=new Map;for(let i=0;i<s.length;i++){const d=s[i];for(const o of h[i]){const r=B(o);p.has(r)||p.set(r,{template:o,testValues:{}});const m=p.get(r),y=o.testValues?.[0];y!==void 0&&(m.testValues[d]=y)}}return[...p.values()].map(({template:i,testValues:d})=>({...i,testValues:Object.keys(d).length>0?d:void 0}))}if(e==="countBin"||e==="percent"){if(u===void 0)return[];const a=we(c,u,"hard");if(e==="countBin")return a;const l=c.length;return a.map(s=>({...s,testValues:{0:l>0?(s.testValues?.[0]??0)/l*100:0}}))}if(e==="mode"){const a=new Map;for(const s of c)for(const h of s){const p=B(h);a.has(p)||a.set(p,[]),a.get(p).push(h)}const l=[];for(const[s,h]of a){const p=s.split(","),i=Number(p[0]),d=Number(p[1]),o=h.map(m=>m.hbin).filter(m=>m!==void 0),r=xe(o);r!==null&&l.push({x:i,y:d,hbin:r})}return l}return[]}function J(t){const n=t.filter(k),e=n.length,u=n.filter(a=>a.edgeExcluded).length,c=n.filter(a=>dieHasTestData(a)||a.hbin!==void 0||a.sbin!==void 0).length;return{filledDies:c,totalDies:e,edgeExcludedDies:u,unpositionedDies:t.length-n.length,ratio:e>0?c/e:0}}function Q(t,n,e="exclude"){const u=new Set(n),a=t.filter(r=>!r.partial).filter(r=>r.edgeExcluded).length,l=t.filter(r=>r.partial).length;let s=0,h=0,p=!1;for(const r of t){if(!fe(r))continue;const m=r.hbin??r.sbin;m!==void 0&&(p=!0,u.has(m)?s++:h++)}const i=s+h,d=p&&i>0?s/i*100:null;let o=null;if(e==="denominator-only"){const r=i+a;o=p&&r>0?s/r*100:null}return{passDies:s,failDies:h,edgeExcludedDies:a,partialDies:l,totalDies:i,yieldPercent:d,yieldPercentGross:o}}function Z(t,n,e,u,c,a=0,l=0,s=0,h=0,p=0,i=!1,d=!1){if(!t)return[];const o=t.anchorDie??{x:0,y:0},r=ge(n,{width:t.width,height:t.height,diePitchX:u,diePitchY:c,anchorDie:{x:o.x-a,y:o.y-l},gridOrigin:{x:-s,y:-h}}),m=ue(le(i,d,n.center.x,n.center.y),ce(p,n.center.x,n.center.y));return r.filter(y=>{const v=y.width/2,w=y.height/2,b=[[y.x-v,y.y-w],[y.x+v,y.y-w],[y.x+v,y.y+w],[y.x-v,y.y+w]].map(([g,M])=>he(m,g,M)),C=Math.min(...b.map(g=>g.x)),P=Math.max(...b.map(g=>g.x)),Y=Math.min(...b.map(g=>g.y)),F=Math.max(...b.map(g=>g.y));return e.some(g=>g.physX>=C&&g.physX<P&&g.physY>=Y&&g.physY<F)})}function Re(t,n,e){const c=Math.max(0,n.radius-e)**2;return t.map(a=>{const l=a.physX-n.center.x,s=a.physY-n.center.y;return l*l+s*s>c?{...a,edgeExcluded:!0}:a})}function Ce(t,n,e){const u=t.filter(k),c=t.filter(i=>!k(i)),a=new Map;for(const i of u){let d=a.get(i.x);d||(d=new Map,a.set(i.x,d)),d.set(i.y,(d.get(i.y)??0)+1)}const l=new Set(e);function s(i,d){const o=i.hbin,r=d.hbin;if(o===void 0||r===void 0)return!1;const m=l.has(o),y=l.has(r);return n==="best"?y!==m?y:r<o:y!==m?m:r>o}const h=new Map;for(const i of u){const d=B(i),o=h.get(d);n==="first"&&o||(n==="best"||n==="worst")&&o&&!s(o,i)||h.set(d,i)}return[...Array.from(h.values()).map(i=>{const o=a.get(i.x)?.get(i.y)??1;return o>1?{...i,retestCount:o}:i}),...c]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const u=t.testPass?.[n];if(u!==void 0)return u;if(e!==void 0&&!isParametricTest(e)){const c=t.testValues?.[n];if(c===0||c===1)return c===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function T(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function ee(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function te(t){return(t.warnings??[]).map(n=>({code:Pe(n),message:n,severity:"error",confidence:t.wafer.confidence}))}function Pe(t){return t.includes(ne)?"geometry-conflict":t.includes(ie)?"inferred-pitch":"partial-coverage"}const ne="do not fit inside the supplied",ie="The die pitch was inferred as";export function buildWaferMap(t,n){const e=be(t),{debug:u,...c}=n??{},a=e.lotStackOpts?ve(e.lotStackOpts,e.testDefs):e.results;for(let f=0;f<Math.min(5,a.length);f++){const x=a[f];if(typeof x.x=="string"||typeof x.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const f of a)if(f.x==null!=(f.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(f.x)}, y=${String(f.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const l=Ce(a,e.retestPolicy,e.passBins),s={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let f=e.explicitDies;const x=l.filter(k),R=l.filter(A=>!k(A));if(x.length>0){const A=new Map(x.map(S=>[B(S),S]));f=f.map(S=>{const z=A.get(B(S));return z?T(S,z):S})}const V=e.waferOpts?.diameter??300,E=j({diameter:V,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});E.orientation!==0&&(f=H(f,E));const K=Z(e.reticleOpts,E,f,1,1,0,0,0,0,E.orientation),re=c.showReticle??e.reticleOpts!==void 0,L=U(E,f,{...c,reticles:K,showReticle:re,plotMode:ee(l,c),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ae=R.map((A,S)=>T({id:`unpositioned_${S}`,width:f[0]?.width??1,height:f[0]?.height??1},A)),_=[...f,...ae];return{wafer:E,dies:_,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:s,warnings:te(s),plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:J(_),yield:Q(_,e.passBins,e.edgeDieYieldMode),reticles:K,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const h=l.filter(k),p=l.filter(f=>!k(f)),i=h.map(f=>({x:f.x,y:f.y})),d=me(i,e.dieOpts,e.waferOpts?.diameter);s.diePitch={confidence:d.confidence,units:d.units};const{pitchX:o,pitchY:r}=d,m=d.units,y=Me(h,e.dieOpts),v=ye(i);s.grid={confidence:v.confidence};const{offsetX:w,offsetY:b}=De(i,y,v),{flipX:C,flipY:P}=ke(e.dieOpts,y),{colMidX:Y,colMidY:F,anchored:g}=Oe(i,w,b,o,r,e.waferOpts?.center);let M=e.waferOpts?.diameter;const $=i.map(f=>({x:(Math.round(f.x)-w)*o-Y,y:(Math.round(f.y)-b)*r-F})),X=$.length>0?Math.max(...$.map(({x:f,y:x})=>Math.hypot(Math.abs(f)+o/2,Math.abs(x)+r/2))):0;if(M===void 0)if(i.length>0)if(m==="mm"){const f=pe($,{minRadius:X});M=f.diameter,s.wafer={confidence:f.confidence,method:f.method}}else M=X*2,s.wafer={confidence:d.confidence*.8,method:"extent"};else M=m==="mm"?300:30,s.wafer={confidence:0,method:"default"};if(!g&&i.length>0&&Se(i,w,b)&&(s.wafer.method="inferred-partial",(s.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0){const f=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(f&&X>M/2+1e-9){const x=$.filter(({x:R,y:V})=>Math.hypot(Math.abs(R)+o/2,Math.abs(V)+r/2)>M/2+1e-9).length;(s.warnings??=[]).push(`${x} of ${$.length} probed die positions ${ne} ${M} mm wafer at the supplied die pitch of ${o} \xD7 ${r} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(X*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}else f||(s.warnings??=[]).push(`${ie} ${o.toFixed(3)} \xD7 ${r.toFixed(3)} mm by assuming the die grid fits within the ${M} mm wafer. The assumed aspect ratio may not match the true die shape whenever edge dies are absent from the data (a reticle-complete map, or partial dies filtered out upstream). Supply dieConfig.width and dieConfig.height \u2014 the die pitch, not the diameter, is what fixes placement.`)}const O=j({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata}),Ee={width:o,height:r};let D=h.map(f=>{const x=Math.round(f.x)-w,R=Math.round(f.y)-b,V={id:`${x}_${R}`,x,y:R,physX:x*o-Y,physY:R*r-F,width:o,height:r,insideWafer:!0,partial:!1};return T(V,f)});(w!==0||b!==0)&&(D=D.map(f=>({...f,x:f.x+w,y:f.y+b,id:`${f.x+w}_${f.y+b}`}))),D=H(D,O),(C||P)&&(D=de(D,{flipX:C,flipY:P},O.center));const N=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(D=Re(D,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&N.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${O.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const G=Z(e.reticleOpts,O,D,o,r,w,b,Y,F,O.orientation,C,P),se=c.showReticle??e.reticleOpts!==void 0,I=U(O,D,{...c,reticles:G,showReticle:se,plotMode:ee(l,c),testDefs:e.testDefs,dataAxisFlip:{x:C,y:P},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),oe=p.map((f,x)=>T({id:`unpositioned_${x}`,width:o,height:r},f)),W=[...D,...oe];return{wafer:O,dies:W,view:I,reticleConfig:e.reticleOpts,units:m,inference:s,warnings:[...te(s),...N],plotMode:I.plotMode,metadata:I.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:J(W),yield:Q(W,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:I.aggrMethod,lotSize:I.lotSize}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{analyzeWaferMap as
|
|
1
|
+
import{analyzeWaferMap as h}from"./analyzeWaferMap.js";import{median as y}from"../core/utils.js";const g=1.3,b=2;function p(f,r){const s={info:0,notable:1,unusual:2};return s[f]>=s[r]?f:r}function w(f){const r=new Map;for(const o of f){const n=new Set,c=new Set(o.summary.findings.flatMap(e=>e.absorbedIds??[]));for(const e of o.summary.findings){if(c.has(e.id))continue;const a=[e.variable.kind,e.variable.index??"",e.variable.bin??"",e.comparison.family,e.comparison.left,e.effect.direction].join("|");if(n.has(a))continue;n.add(a);const l=r.get(a)??{finding:e,waferIndices:[],severity:e.severity};l.waferIndices.push(o.waferIndex),l.severity=p(l.severity,e.severity),r.set(a,l)}}const s=[];for(const[o,n]of r){if(n.waferIndices.length<b)continue;const c=n.waferIndices.length/Math.max(1,f.length),e=c>=.6||n.severity==="unusual"?"unusual":"notable",a=n.finding;s.push({id:`lot-repeat:${o}`,level:"lot",severity:e,variable:{...a.variable},comparison:{...a.comparison},effect:{direction:a.effect.direction,absoluteDelta:a.effect.absoluteDelta,relativeDelta:a.effect.relativeDelta,effectSize:a.effect.effectSize},stats:{method:"wafer-finding-frequency",sampleSizeLeft:n.waferIndices.length,sampleSizeRight:f.length-n.waferIndices.length},summary:`${a.summary} \u2014 seen on ${n.waferIndices.length}/${f.length} wafers (${Math.round(c*100)}%)`,highlight:{kind:"wafer",waferIndices:n.waferIndices}})}return s}function v(f){const r=f.map(e=>({waferIndex:e.waferIndex,yieldPercent:e.summary.stats.yieldPercent})).filter(e=>e.yieldPercent!==null);if(r.length<3)return[];const s=r.map(e=>e.yieldPercent),o=y(s),n=y(s.map(e=>Math.abs(e-o)));if(!Number.isFinite(n)||n===0)return[];const c=[];for(const e of r){const a=.6745*(e.yieldPercent-o)/n;if(Math.abs(a)<g)continue;const l=e.yieldPercent-o;c.push({id:`inter-wafer:yield:${e.waferIndex}`,level:"inter-wafer",severity:Math.abs(a)>=2?"unusual":"notable",variable:{kind:"yield",label:"Yield"},comparison:{family:"wafer",left:`Wafer ${e.waferIndex+1}`,right:"Lot median"},effect:{direction:l>0?"higher":"lower",absoluteDelta:l,relativeDelta:o===0?void 0:l/o,effectSize:a},stats:{method:"mad-z-score",sampleSizeLeft:1,sampleSizeRight:r.length-1},summary:`Wafer ${e.waferIndex+1} yield is ${Math.abs(l).toFixed(1)} percentage points ${l>0?"higher":"lower"} than the lot median`,highlight:{kind:"wafer",waferIndices:[e.waferIndex]}})}return c}export function analyzeWaferLot(f,r={}){const s=f.map((t,i)=>({waferIndex:i,summary:r.perWaferSummaries?.[i]??h(t,r)})),o=[...w(s),...v(s)].sort((t,i)=>{const d=t.severity==="unusual"?2:t.severity==="notable"?1:0,m=i.severity==="unusual"?2:i.severity==="notable"?1:0;return d!==m?m-d:t.summary.localeCompare(i.summary,"en")}),n=s.map(t=>t.summary.wafer).filter(t=>!!t),c={},e=[];if(n.length>0){const t=new Set;for(const i of n)for(const d of Object.keys(i))t.add(d);t.delete("wafer"),t.delete("waferId");for(const i of t){const d=n.filter(u=>i in u).map(u=>u[i]);d.every(u=>u===d[0])?c[i]=d[0]:e.push(i)}}const a=s.map(({waferIndex:t,summary:i})=>({waferIndex:t,yieldPercent:i.stats.yieldPercent})),l=s.map(({waferIndex:t,summary:i})=>i.stats.perTestStats?.length?{waferIndex:t,tests:i.stats.perTestStats}:null).filter(t=>t!==null);return{level:"lot",hasNotableFindings:o.some(t=>t.severity!=="info")||s.some(t=>t.summary.hasNotableFindings),findings:o,lot:Object.keys(c).length>0?c:void 0,...e.length>0?{mixedIdentityFields:e}:{},stats:{waferCount:f.length},lotYieldSeries:a,perWafer:s,...l.length>0?{perWaferTestStats:l}:{}}}
|
|
@@ -13,10 +13,12 @@ export declare function renderDefinitionList(entries: Array<{
|
|
|
13
13
|
}>, className?: string): string;
|
|
14
14
|
/**
|
|
15
15
|
* The single source of metadata content for every report and panel surface
|
|
16
|
-
* — built from `buildFacetTable` over the actual item(s), never
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* — built from `buildFacetTable` over the actual item(s), never
|
|
17
|
+
* `LotStatsSummary.lot` (which itself now omits — rather than silently
|
|
18
|
+
* picking a winner for — any field the population disagrees on; see its own
|
|
19
|
+
* `mixedIdentityFields`) or a per-file known-key list, both of which can
|
|
20
|
+
* silently drop a field that varies across the population and can drift
|
|
21
|
+
* from what the live Summary panel
|
|
20
22
|
* shows (`buildMetadataInfoSection` in canvas-adapter/summaryPanel.ts,
|
|
21
23
|
* which calls the same `buildFacetTable`). A population of one wafer is
|
|
22
24
|
* just a facet table where every field has exactly one value, so the
|
|
@@ -189,8 +189,21 @@ export interface LotStatsSummary {
|
|
|
189
189
|
level: 'lot';
|
|
190
190
|
hasNotableFindings: boolean;
|
|
191
191
|
findings: StatsFinding[];
|
|
192
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* Free-form lot-level identity fields (lot ID, product, etc. — wafer-specific
|
|
194
|
+
* keys excluded). Only includes a key when every wafer that has identity data
|
|
195
|
+
* agrees on its value — see `mixedIdentityFields` for keys that disagree.
|
|
196
|
+
*/
|
|
193
197
|
lot?: Record<string, unknown>;
|
|
198
|
+
/**
|
|
199
|
+
* Identity keys (lot, product, testProgram, temperature, etc.) where the
|
|
200
|
+
* pooled wafers do NOT all agree — e.g. `items` mixed more than one
|
|
201
|
+
* lot/product/program into one `analyzeWaferLot` call. Omitted from `lot`
|
|
202
|
+
* rather than silently reporting the first wafer's value. Present only when
|
|
203
|
+
* at least one such key exists; check this before treating `lot` as
|
|
204
|
+
* describing the whole batch.
|
|
205
|
+
*/
|
|
206
|
+
mixedIdentityFields?: string[];
|
|
194
207
|
/** Engine-computed analysis stats for this lot. */
|
|
195
208
|
stats: {
|
|
196
209
|
waferCount: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wafertools/wafermap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "Interactive wafer map visualization and yield analysis for semiconductor test data. Hard bins, soft bins, test values, retests, edge exclusion, reticle overlays, spatial statistics, failure clustering, and lot-level trend analysis — pure ES modules, no runtime dependencies, no server required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|