@wafertools/wafermap 0.30.1 → 0.30.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/AGENTS.md +71 -11
  2. package/CHANGELOG.md +464 -8
  3. package/README.md +6 -3
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +48 -0
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +4 -1
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +4 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +81 -7
  12. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  14. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  15. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  18. package/dist/packages/canvas-adapter/charts/sweep.d.ts +31 -0
  19. package/dist/packages/canvas-adapter/charts/sweep.js +1 -0
  20. package/dist/packages/canvas-adapter/charts/trend.d.ts +3 -1
  21. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  22. package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
  23. package/dist/packages/canvas-adapter/chunked.js +1 -0
  24. package/dist/packages/canvas-adapter/dieList.js +2 -2
  25. package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
  26. package/dist/packages/canvas-adapter/drilldown.js +1 -0
  27. package/dist/packages/canvas-adapter/icons.js +1 -1
  28. package/dist/packages/canvas-adapter/insightsTab.d.ts +55 -3
  29. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  30. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +55 -0
  31. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  32. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  33. package/dist/packages/canvas-adapter/summaryPanel.d.ts +55 -9
  34. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  35. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  36. package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
  37. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  38. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  39. package/dist/packages/canvas-adapter/userGuideHtml.js +126 -3
  40. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  41. package/dist/packages/canvas-adapter/version.js +1 -1
  42. package/dist/packages/canvas-adapter/warnings.js +1 -1
  43. package/dist/packages/core/aggregates.js +1 -1
  44. package/dist/packages/core/utils.d.ts +55 -0
  45. package/dist/packages/core/utils.js +1 -1
  46. package/dist/packages/renderer/axisTicks.d.ts +32 -0
  47. package/dist/packages/renderer/axisTicks.js +1 -0
  48. package/dist/packages/renderer/binColors.d.ts +14 -0
  49. package/dist/packages/renderer/binColors.js +1 -1
  50. package/dist/packages/renderer/buildView.d.ts +11 -37
  51. package/dist/packages/renderer/buildView.js +1 -1
  52. package/dist/packages/renderer/buildWaferMap.d.ts +58 -1
  53. package/dist/packages/renderer/buildWaferMap.js +1 -1
  54. package/dist/packages/renderer/deprecate.d.ts +13 -5
  55. package/dist/packages/renderer/deprecate.js +1 -1
  56. package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
  57. package/dist/packages/renderer/derivedTests/apply.js +1 -0
  58. package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
  59. package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
  60. package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
  61. package/dist/packages/renderer/derivedTests/parser.js +2 -0
  62. package/dist/packages/renderer/fmt.d.ts +24 -2
  63. package/dist/packages/renderer/fmt.js +1 -1
  64. package/dist/packages/renderer/index.d.ts +1 -0
  65. package/dist/packages/renderer/index.js +1 -1
  66. package/dist/packages/renderer/spec.d.ts +49 -0
  67. package/dist/packages/renderer/spec.js +1 -0
  68. package/dist/packages/renderer/testLabel.d.ts +154 -0
  69. package/dist/packages/renderer/testLabel.js +1 -0
  70. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  71. package/dist/packages/stats/capability.d.ts +60 -0
  72. package/dist/packages/stats/capability.js +1 -1
  73. package/dist/packages/stats/correlation.d.ts +33 -0
  74. package/dist/packages/stats/correlation.js +1 -1
  75. package/dist/packages/stats/deprecated.d.ts +3 -0
  76. package/dist/packages/stats/deprecated.js +1 -1
  77. package/dist/packages/stats/findingsNarrative.js +1 -1
  78. package/dist/packages/stats/index.d.ts +2 -2
  79. package/dist/packages/stats/index.js +1 -1
  80. package/dist/packages/stats/math.d.ts +27 -2
  81. package/dist/packages/stats/math.js +1 -1
  82. package/dist/packages/stats/mergeTestDefs.js +1 -1
  83. package/dist/packages/stats/renderFindingsReport.js +8 -28
  84. package/dist/packages/stats/renderSummaryReport.js +22 -31
  85. package/dist/packages/stats/reportHtml.d.ts +27 -0
  86. package/dist/packages/stats/reportHtml.js +35 -14
  87. package/dist/packages/stats/sweep.d.ts +178 -0
  88. package/dist/packages/stats/sweep.js +1 -0
  89. package/dist/packages/stats/sweepXFromName.d.ts +27 -0
  90. package/dist/packages/stats/sweepXFromName.js +1 -0
  91. package/dist/packages/stats/testPassRate.js +1 -1
  92. package/dist/packages/stats/types.d.ts +28 -1
  93. package/llms.txt +14 -8
  94. package/package.json +2 -2
@@ -1 +1 @@
1
- import{hasPosition as oe}from"../core/dies.js";import{getReticleCell as re}from"../core/reticle.js";import{metadataCategoricalValue as le,metadataDisplayValue as ce}from"../core/metadata.js";import{affineRotation as pt,affineMirror as Rt,affineCompose as st,affinePoint as G,affineVector as yt,affineSwapsAxes as ue}from"../core/transforms.js";import{contrastTextColor as Q,SPEC_PASS_FILL as gt,SPEC_FAIL_LOW as fe,SPEC_FAIL_HIGH as bt}from"./colorMap.js";import{resolveValueColorFn as de}from"./colorSchemes.js";import{resolveBinColors as he,binColorsCover as me}from"./binColors.js";import{NO_DATA_FILL as z}from"./colorMap.js";import{diePassStatus as pe}from"../core/dies.js";import{getDieTestValue as P,getTestPassStatus as U,isParametricTest as xt}from"./buildWaferMap.js";import{fmt as Mt,fmtColorbarAxis as St,fmtAggregationMethod as Nt}from"./fmt.js";import{metadataValueColor as ye}from"./colorMap.js";import{compareNatural as ge,clamp01 as Ht,escHtml as O}from"../core/utils.js";import{prettyKey as Ot}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function collectMetadataValues(t,e){const a=new Set;for(const n of t){if(n.partial||n.edgeExcluded)continue;const s=ot(n,e);s!==void 0&&a.add(s)}return[...a].sort(ge)}export function findTestDef(t,e){return t?.find(a=>a.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,a)=>e-a)}const be="#d3d6db",At="#e8e9ea",xe="#aab0ba";function ot(t,e){return e?le(t.metadata?.[e]):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function Me(t,e,a){const{x:n,y:s}=t.center,i=pt(t.orientation,n,s),o=st(Rt(e?.x??!1,e?.y??!1,n,s),pt(t.orientation,n,s)),l=st(Rt(a?.flipX??!1,a?.flipY??!1,n,s),pt(a?.rotation??0,n,s));return{bakedToScreen:l,physicalToScreen:st(l,i),gridToScreen:st(l,o)}}function K(t,e=!1){return{points:[t],closed:e}}function Se(t,e){const{center:a,radius:n,notch:s}=t;let i=a.x+n*Math.cos(e),o=a.y+n*Math.sin(e);if(!s)return{x:i,y:o};const l=Math.sqrt(n**2-(s.length/2)**2),d=s.length/2,g=i-a.x,p=o-a.y;return s.type==="bottom"&&p<-l?(o=a.y-l,i=a.x+Math.max(-d,Math.min(d,g))):s.type==="top"&&p>l?(o=a.y+l,i=a.x+Math.max(-d,Math.min(d,g))):s.type==="left"&&g<-l?(i=a.x-l,o=a.y+Math.max(-d,Math.min(d,p))):s.type==="right"&&g>l&&(i=a.x+l,o=a.y+Math.max(-d,Math.min(d,p))),{x:i,y:o}}function Et(t,e){return t.map(e).join(" / ")}function ke(t,e=256){const a=Array.from({length:e},(n,s)=>t(s/(e-1)));return n=>a[Math.min(e-1,Math.max(0,Math.round(n*(e-1))))]}function ve(t,e){const a=Math.max(1,Math.min(t.width,t.height)),n=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(a*.55,n*1.8))))}function $e(t,e,a){if(e?.length){const n=e.flatMap(s=>{const i=s.testNumber;if(!xt(s)){const l=U(t,i,s);return l===void 0?[]:[{key:i,label:s.name,value:l?"Pass":"Fail"}]}const o=P(t,i);return o===void 0?[]:[{key:i,label:s.name,value:Mt(o,s.unit,a),recordedFail:t.testPass?.[i]===!1}]});if(n.length)return n}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:Mt(s,void 0,a)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,a={}){const{testDefs:n,hbinDefs:s,sbinDefs:i,fallbackFormat:o,aggrMethod:l,lotSize:d,activeTest:g,reticleConfig:p,metadataFields:B}=a,D=s?new Map(s.map(u=>[u.bin,u])):null,T=i?new Map(i.map(u=>[u.bin,u])):null,b=[O(`Die (${t.x}, ${t.y})`)];if(p&&oe(t)){const u=re(t,p);b.push(O(`Reticle (${u.column}, ${u.row})`))}if(e==="stackedValues"){const u=P(t,0);if(u!==void 0){const f=n?.[0],h=f?.testNumber,k=f?.name??(h!=null?`Test ${h}`:"Value"),m=l?` (${l})`:"";b.push(O(`${k}${m}: ${Mt(u,f?.unit,o)}`))}}else if(e==="stackedBins"||e==="stackedSoftBins"){const u=P(t,0),f=e==="stackedSoftBins"?t.sbin:t.hbin;if(u!==void 0){const h=e==="stackedSoftBins"?T:D,k=f!==void 0?h?.get(f)?.name?`${f} \xB7 ${h.get(f).name}`:`Bin ${f}`:"Bin";let m;if(l==="percent")m=`${u.toFixed(0)}%`;else{const C=d?` (${(u/d*100).toFixed(0)}% of stacked wafers)`:"";m=`${u}${C}`}const x=l?` [${Nt(l)}]`:"";b.push(O(`${k}: ${m}${x}`))}}else{const u=$e(t,n,o);if(e==="value"&&u.length){const{testNumber:f}=resolveTestNumber(g??0,n),h=findTestDef(n,f),k=P(t,f);let m=u.findIndex(y=>y.key===f);m<0&&(m=0);const x=u[m];let C=`<b>${O(`${x.label}: ${x.value}`)}${x.recordedFail?" <i>(recorded fail)</i>":""}</b>`;if(x.key===f&&h){const y=classifySpec(k,h);(y==="failLow"||y==="failHigh")&&(C+=" <i>(out of spec)</i>")}b.push(C);const R=u.length-1;R>0&&b.push(`<i>+${R} more test${R===1?"":"s"}</i>`)}else if(u.length){const f=u.length;b.push(`<i>${f} test result${f===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const f=[];if(t.hbin!==void 0){const h=D?.get(t.hbin)?.name,k=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);f.push(`HBin: ${k}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,k=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);f.push(`SBin: ${k}`)}b.push(f.map(O).join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&b.push(O(`Retests: ${t.retestCount}`)),t.siteNum!==void 0&&b.push(O(`Site: ${t.siteNum}`)),t.partId!==void 0&&b.push(O(`Part ID: ${t.partId}`)),t.partial&&b.push("<i>partial die</i>"),t.probeIndex!==void 0&&b.push(O(`Probe: #${t.probeIndex}`));const $={...t.metadata??{}};for(const[u,f]of Object.entries($)){const h=ce(f);if(h===void 0)continue;const k=B?.find(m=>m.key===u)?.label??Ot(u);b.push(O(`${k}: ${h}`))}return b.join("<br>")}export function buildMapTitle(t,e="engineering",a){const n=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=i=>{const o=a?.length===1?a[0]:void 0;return o===void 0?`${i} Bin`:o.name?`${i} Bin ${o.bin} \xB7 ${o.name}`:`${i} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:n};case"stackedSoftBins":return{primary:s("Soft"),secondary:n};case"metadata":{const i=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?Ot(t.activeMetadataKey):"Metadata";return{primary:i?.label??o,secondary:""}}case"stackedValues":{const i=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=St(o,i?.name,i?.unit,e),d=l||"Value",g=t.aggrMethod?` \xB7 ${Nt(t.aggrMethod)}`:"";return{primary:`${d}${g}`,secondary:n}}default:{const i=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=St(o,i?.name,i?.unit,e),d=l||i?.name;if(t.passFailDisplay!=="off"){const g=i?.testNumber??t.activeTest,p=d?`${d} \xB7 #${g}`:`Test ${g}`,B=t.passFailDisplay==="spec"?"Spec pass/fail":i!==void 0&&!xt(i)?"Functional pass/fail":"Tester pass/fail";return{primary:p,secondary:B}}return{primary:d??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,a){const{plotMode:n,colorFns:s,normalize:i,activeTest:o,valueRange:l,testDefs:d,fallbackFormat:g,passFailDisplay:p="off",activeMetadataKey:B,metadataColorMap:D,allIntegerValues:T}=a,b=findTestDef(d,o),{tickFmt:$}=St(l[1],b?.name,b?.unit,g),u=T?h=>String(Math.round(h)):$,{testNumber:f}=resolveTestNumber(o,d);return t.flatMap((h,k)=>{let m="",x="#111111";if(n==="value"&&p==="test"){const y=U(h,f,b);if(y===void 0)return[];m=y?"P":"F",x=Q(y?gt:bt)}else if(n==="value"){const y=P(h,f);if(y===void 0)return[];m=Et([y],u),x=Q(s.forValue(i(y)))}else if(n==="hardBin"||n==="softBin"){const y=n==="softBin"?h.sbin:h.hbin;if(y===void 0)return[];m=String(y),x=Q(s.forBin(y))}else if(n==="metadata"){const y=ot(h,B);if(y===void 0)return[];m=y,x=Q(D?.get(m)??z)}else{const y=P(h,0);if(y===void 0)return[];m=Et([y],u),x=Q(s.forValue(i(y)))}const C=e?e[k*2]:h.physX,R=e?e[k*2+1]:h.physY;return[{x:C,y:R,text:m,fontSize:ve(h,m),color:x,align:"center"}]})}function _t(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function Be(t,e,a=720){const{center:n,radius:s,notch:i}=t,o=$=>G(e,$.x,$.y);if(!(i!==void 0&&t.diameter>150)){const $=[];for(let u=0;u<=a;u++){const f=2*Math.PI*u/a;$.push(o(Se(t,f)))}return[{kind:"wafer-boundary",...K($,!0),lineColor:"#888888",lineWidth:1}]}const d=1.25,g=i.length,p=_t(i.type),B=Math.atan2(g,s),D=s-d,T=[];let b=!1;for(let $=0;$<=a;$++){const u=2*Math.PI*$/a;let f=u-p;for(;f>Math.PI;)f-=2*Math.PI;for(;f<-Math.PI;)f+=2*Math.PI;if(Math.abs(f)<=B){if(!b){b=!0;const h={x:n.x+s*Math.cos(p-B),y:n.y+s*Math.sin(p-B)},k={x:n.x+D*Math.cos(p),y:n.y+D*Math.sin(p)},m={x:n.x+s*Math.cos(p+B),y:n.y+s*Math.sin(p+B)};T.push(o(h),o(k),o(m))}continue}T.push(o({x:n.x+s*Math.cos(u),y:n.y+s*Math.sin(u)}))}return[{kind:"wafer-boundary",...K(T,!0),lineColor:"#888888",lineWidth:1}]}function Te(t,e,a,n=360){const s=[],i=Math.max(1,Math.floor(a));for(let o=1;o<i;o++){const l=t.radius*o/i,d=[];for(let g=0;g<=n;g++){const p=2*Math.PI*g/n,B={x:t.center.x+l*Math.cos(p),y:t.center.y+l*Math.sin(p)};d.push(G(e,B.x,B.y))}s.push({kind:"ring-boundary",...K(d,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function Ie(t,e,a,n){const{x:s,y:i}=t.center,o=t.radius,l=(u,f)=>G(e,u,f),d=a-s,g=Math.sqrt(Math.max(0,o*o-d*d)),p=l(a,i-g),B=l(a,i+g),D=n-i,T=Math.sqrt(Math.max(0,o*o-D*D)),b=l(s-T,n),$=l(s+T,n);return[{kind:"quadrant-boundary",...K([p,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...K([b,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function Ve(t,e){return t.map(a=>{const n=a.width/2,s=a.height/2;return{kind:"reticle",points:[[[a.x-n,a.y-s],[a.x+n,a.y-s],[a.x+n,a.y+s],[a.x-n,a.y+s]].map(([o,l])=>G(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function Ce(t){const e=t.filter(a=>a.probeIndex!==void 0).sort((a,n)=>(a.probeIndex??0)-(n.probeIndex??0));return e.length?[{kind:"probe-path",...K(e.map(a=>({x:a.physX,y:a.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function Fe(t,e,a,n,s,i,o,l,d,g,p,B,D,T,b="off",$,u,f){const h=e.width-o,k=e.height-o,m=i?k:h,x=i?h:k,C=S=>s==="softBin"?S.sbin:S.hbin,R=S=>ot(S,$);if(e.partial){t.push({x:a,y:n,width:m,height:x,fill:be,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:a,y:n,width:m,height:x,fill:xe,type:"stacked",metadata:e.metadata});return}if(d!==void 0&&(s==="hardBin"||s==="softBin")&&C(e)!==d){t.push({x:a,y:n,width:m,height:x,fill:At,type:"hardBin",metadata:e.metadata});return}if(f!==void 0&&s==="metadata"&&R(e)!==f){t.push({x:a,y:n,width:m,height:x,fill:At,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(b==="test"){const W=U(e,p,T),rt=W===void 0?z:W?gt:bt;t.push({x:a,y:n,width:m,height:x,fill:rt,type:"value",metadata:e.metadata});return}const S=P(e,p),N=classifySpec(S,T);let w,J;S===void 0?w=z:b==="spec"?w=N==="failLow"?fe:N==="failHigh"?bt:gt:(w=l.forValue(g(S)),(N==="failLow"||N==="failHigh")&&(J=N)),t.push({x:a,y:n,width:m,height:x,fill:w,type:"value",specMark:J,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const S=C(e),N=S!=null?l.forBin(S):z,w=S!=null&&pe(e,D)===!1?!0:void 0;t.push({x:a,y:n,width:m,height:x,fill:N,type:s,binFail:w,metadata:e.metadata});return}if(s==="metadata"){const S=R(e),N=S!==void 0?u?.get(S)??z:z;t.push({x:a,y:n,width:m,height:x,fill:N,type:"metadata",metadata:e.metadata});return}const y=P(e,0),Y=y!==void 0?l.forValue(g(y)):z;t.push({x:a,y:n,width:m,height:x,fill:Y,type:"value",metadata:e.metadata})}function De(t,e,a){const n=t.radius*.15,s=yt(e,n,0),i=yt(e,0,n),o=s.x+i.x>=0?-1:1,l=s.y+i.y>=0?-1:1,d={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},g={x:d.x+s.x,y:d.y+s.y},p={x:d.x+i.x,y:d.y+i.y};return a.push({x:g.x+s.x*.35,y:g.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:p.x+i.x*.35,y:p.y+i.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...K([d,g]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...K([d,p]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,a={},n){const s=a.reticles??[],{plotMode:i="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:d=!1,ringCount:g=4,showRingBoundaries:p=!1,showQuadrantBoundaries:B=!1,showXYIndicator:D=!1,binColorScheme:T="default",valueColorScheme:b="default",reverseValueScheme:$=!1,useDefinedBinColors:u=!0,binColors:f,passBins:h=[1],highlightBin:k,highlightMetadataValue:m,interactiveTransform:x,valueRange:C,metadataValueOrder:R,testDefs:y,activeTest:Y=0,activeMetadataKey:S,fallbackFormat:N="engineering",aggregationMethod:w,lotSize:J,dataAxisFlip:W,isLotStack:rt=!1,logScale:wt,colorbarRangeMode:Wt="spec",passFailDisplay:jt,showPartialDies:zt=!0}=a,kt=jt??"off",lt=n?.hbinDefs,ct=n?.sbinDefs,vt=n?.metadataFields,Kt=vt?.find(c=>c.key===S),qt={x:(W?.x??!1)!==(x?.flipX??!1),y:(W?.y??!1)!==(x?.flipY??!1)},Xt=lt?new Map(lt.map(c=>[c.bin,c])):null,Yt=ct?new Map(ct.map(c=>[c.bin,c])):null,Gt=i==="softBin"?Yt:Xt,Qt=new Set(h),ut=f&&me(f,e)?f:he(e,{passBins:h,binColorScheme:T,hbinDefs:lt,sbinDefs:ct,useDefinedBinColors:u}),Ut=i==="softBin"?ut.soft:ut.hard,$t={forValue:ke(de(b,$)),forBin:c=>Ut.get(c)??z};let{testNumber:A}=resolveTestNumber(Y,y);if(!y?.length&&i==="value"&&!e.some(r=>r.testValues&&A in r.testValues)){const r=e.reduce((M,V)=>{if(!V.testValues)return M;const v=Object.keys(V.testValues).map(Number),L=v.length?Math.min(...v):void 0;return L!==void 0&&(M===void 0||L<M)?L:M},void 0);r!==void 0&&(A=r)}let Z;if(Array.isArray(C))Z=C;else if(C){const{testNumber:c}=resolveTestNumber(C.test,y);Z=c===A?C.range:void 0}const I=findTestDef(y,A),Bt=i==="value"&&I!==void 0&&!xt(I),Jt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let _="off";i==="value"&&(Bt?_="test":kt==="spec"&&Jt?_="spec":kt==="test"&&e.some(c=>U(c,A,I)!==void 0)&&(_="test"));const Tt=_==="spec"?"spec":Wt,ft=i==="stackedValues"||i==="stackedBins"||i==="stackedSoftBins";let H,E,tt=!1;if(Z){[H,E]=Z;let c=!0,r=!1;for(const M of e){const V=ft?P(M,0):P(M,A);if(V!==void 0&&(r=!0,!Number.isInteger(V))){c=!1;break}}tt=r&&c}else{const c=!ft&&i==="value"&&Tt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let r=1/0,M=-1/0,V=!0;for(const v of e){const L=ft?P(v,0):P(v,A);L!==void 0&&(L<r&&(r=L),L>M&&(M=L),Number.isInteger(L)||(V=!1))}isFinite(r)||(V=!1),tt=V,c?(H=I.limitLow!==void 0?I.limitLow:isFinite(r)?r:0,E=I.limitHigh!==void 0?I.limitHigh:isFinite(M)?M:1):(H=isFinite(r)?r:0,E=isFinite(M)?M:1)}H===E&&(E=H+1);const It=Bt?!1:wt??I?.logScale??!1,Vt=It&&H>0&&E>0,Zt=Vt;let et;if(Vt){const c=Math.log10(H),r=Math.log10(E)-c;et=M=>M<=0?0:Ht((Math.log10(M)-c)/r)}else et=c=>Ht((c-H)/(E-H));const F=Me(t,W,x);let Ct=null;if(t.notch){const c=_t(t.notch.type),r=yt(F.physicalToScreen,Math.cos(c)*t.radius,Math.sin(c)*t.radius),M=Math.hypot(r.x,r.y);M>0&&(Ct={x:r.x/M,y:r.y/M})}let q=1/0;for(let c=0;c<e.length;c++){const r=e[c];r.width<q&&(q=r.width),r.height<q&&(q=r.height)}const Ft=1,te=q!==1/0?Math.min(Ft,q*.12):Ft,Dt=[],dt=[],ee=F.bakedToScreen.a===1&&F.bakedToScreen.b===0&&F.bakedToScreen.c===0&&F.bakedToScreen.d===1&&F.bakedToScreen.e===0&&F.bakedToScreen.f===0;let j=null;if(!ee){j=new Float64Array(e.length*2);for(let c=0;c<e.length;c++){const r=G(F.bakedToScreen,e[c].physX,e[c].physY);j[c*2]=r.x,j[c*2+1]=r.y}}const ne=ue(F.gridToScreen),ae=i==="hardBin"||i==="softBin"||i==="stackedBins"||i==="stackedSoftBins",ie=i==="softBin"||i==="stackedSoftBins";let nt=ae?new Map:void 0;const ht=i==="value"&&_==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,mt=i==="value"&&_==="test"?{pass:0,fail:0}:void 0,Pt=i==="metadata";let at=Pt?new Map:void 0,it=null;if(Pt&&S){const c=collectMetadataValues(e,S),r=R?.key===S?R.values:void 0,M=r?[...r,...c.filter(V=>!r.includes(V))]:c;it=new Map(M.map((V,v)=>[V,Kt?.values?.find(L=>L.value===V)?.color??ye(v)]))}for(let c=0;c<e.length;c++){const r=e[c],M=j?j[c*2]:r.physX,V=j?j[c*2+1]:r.physY;if(dt.push({x:M,y:V}),nt&&!r.partial&&!r.edgeExcluded){const v=ie?r.sbin:r.hbin;v!=null&&nt.set(v,(nt.get(v)??0)+1)}if(ht&&!r.partial&&!r.edgeExcluded){const v=classifySpec(P(r,A),I);v&&ht[v]++}if(mt&&!r.partial&&!r.edgeExcluded){const v=U(r,A,I);v!==void 0&&mt[v?"pass":"fail"]++}if(at&&!r.partial&&!r.edgeExcluded){const v=ot(r,S);v!==void 0&&at.set(v,(at.get(v)??0)+1)}r.partial&&!zt||Fe(Dt,r,M,V,i,ne,te,$t,k,et,A,Gt,Qt,I,_,S,it,m)}const Lt=o?generateTextOverlay(e,j,{plotMode:i,colorFns:$t,normalize:et,activeTest:Y,valueRange:[H,E],testDefs:y,fallbackFormat:N,passFailDisplay:_,activeMetadataKey:S,metadataColorMap:it,allIntegerValues:tt}):[],X=Be(t,F.physicalToScreen);p&&X.push(...Te(t,F.physicalToScreen,g)),B&&X.push(...Ie(t,F.physicalToScreen,t.center.x,t.center.y)),l&&X.push(...Ve(s,F.gridToScreen)),d&&X.push(...Ce(e)),D&&X.push(...De(t,F.gridToScreen,Lt));const se=dt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Dt,hoverPoints:dt,texts:Lt,overlays:X,plotMode:i,binColorScheme:T,valueColorScheme:b,reverseValueScheme:$,binColors:ut,metadata:t.metadata??null,dies:e,valueRange:[H,E],allIntegerValues:tt,testDefs:y,activeTest:Y,logScale:Zt,logScaleRequested:It,aggrMethod:w,lotSize:J,axisFlip:qt,dataAxisFlip:{x:W?.x??!1,y:W?.y??!1},gridToScreen:F.gridToScreen,rotation:((t.orientation+(x?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:rt,passFailDisplay:_,colorbarRangeMode:Tt,waferCenter:t.center,waferRadius:t.radius,notchDir:Ct,binCounts:nt,metadataCounts:at,metadataColorMap:it,activeMetadataKey:S,metadataFields:vt,specCounts:ht,passFailCounts:mt,dieBounds:se}}export{getDieKey}from"../core/dies.js";
1
+ import{hasPosition as de}from"../core/dies.js";import{getReticleCell as he}from"../core/reticle.js";import{metadataCategoricalValue as me,metadataDisplayValue as pe}from"../core/metadata.js";import{affineRotation as bt,affineMirror as Ht,affineCompose as st,affinePoint as G,affineVector as xt,affineSwapsAxes as ye}from"../core/transforms.js";import{contrastTextColor as Q,SPEC_PASS_FILL as Mt,SPEC_FAIL_LOW as ge,SPEC_FAIL_HIGH as St}from"./colorMap.js";import{resolveValueColorFn as be}from"./colorSchemes.js";import{resolveBinColors as xe,binColorsCover as Me}from"./binColors.js";import{NO_DATA_FILL as z}from"./colorMap.js";import{diePassStatus as Se}from"../core/dies.js";import{getDieTestValue as P,getTestPassStatus as U,isParametricTest as vt}from"./buildWaferMap.js";import{classifySpec as ot}from"./spec.js";export{ot as classifySpec};import{fmt as kt,fmtColorbarAxis as $t,fmtAggregationMethod as _t}from"./fmt.js";import{metadataValueColor as ve}from"./colorMap.js";import{clamp01 as Kt,compareNatural as ke,escHtml as L,minOf as $e}from"../core/utils.js";import{prettyKey as Wt}from"../core/utils.js";import{testLabel as rt,markedTestLabel as lt,derivedTestNote as jt,isDerivedTest as Be,DERIVED_MARK as Te,DERIVED_KEY as Ie}from"./testLabel.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function collectMetadataValues(t,e){const i=new Set;for(const a of t){if(a.partial||a.edgeExcluded)continue;const s=ct(a,e);s!==void 0&&i.add(s)}return[...i].sort(ke)}export function findTestDef(t,e){return t?.find(i=>i.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,i)=>e-i)}const De="#d3d6db",wt="#e8e9ea",Ve="#aab0ba";function ct(t,e){return e?me(t.metadata?.[e]):void 0}function Ce(t,e,i){const{x:a,y:s}=t.center,n=bt(t.orientation,a,s),o=st(Ht(e?.x??!1,e?.y??!1,a,s),bt(t.orientation,a,s)),l=st(Ht(i?.flipX??!1,i?.flipY??!1,a,s),bt(i?.rotation??0,a,s));return{bakedToScreen:l,physicalToScreen:st(l,n),gridToScreen:st(l,o)}}function q(t,e=!1){return{points:[t],closed:e}}function Fe(t,e){const{center:i,radius:a,notch:s}=t;let n=i.x+a*Math.cos(e),o=i.y+a*Math.sin(e);if(!s)return{x:n,y:o};const l=Math.sqrt(a**2-(s.length/2)**2),d=s.length/2,g=n-i.x,p=o-i.y;return s.type==="bottom"&&p<-l?(o=i.y-l,n=i.x+Math.max(-d,Math.min(d,g))):s.type==="top"&&p>l?(o=i.y+l,n=i.x+Math.max(-d,Math.min(d,g))):s.type==="left"&&g<-l?(n=i.x-l,o=i.y+Math.max(-d,Math.min(d,p))):s.type==="right"&&g>l&&(n=i.x+l,o=i.y+Math.max(-d,Math.min(d,p))),{x:n,y:o}}function zt(t,e){return t.map(e).join(" / ")}function Pe(t,e=256){const i=Array.from({length:e},(a,s)=>t(s/(e-1)));return a=>i[Math.min(e-1,Math.max(0,Math.round(a*(e-1))))]}function Le(t,e){const i=Math.max(1,Math.min(t.width,t.height)),a=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(i*.55,a*1.8))))}function Re(t,e,i){if(e?.length){const a=e.flatMap(s=>{const n=s.testNumber;if(!vt(s)){const l=U(t,n,s);return l===void 0?[]:[{key:n,label:lt(s,n),value:l?"Pass":"Fail"}]}const o=P(t,n);return o===void 0?[]:[{key:n,label:lt(s,n),value:kt(o,s.unit,i),recordedFail:t.testPass?.[n]===!1}]});if(a.length)return a}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([a,s])=>({key:Number(a),label:rt(void 0,Number(a)),value:kt(s,void 0,i)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([a,s])=>({key:Number(a),label:rt(void 0,Number(a)),value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,i={}){const{testDefs:a,hbinDefs:s,sbinDefs:n,fallbackFormat:o,aggrMethod:l,lotSize:d,activeTest:g,reticleConfig:p,metadataFields:B}=i,F=s?new Map(s.map(f=>[f.bin,f])):null,T=n?new Map(n.map(f=>[f.bin,f])):null,b=[L(`Die (${t.x}, ${t.y})`)];if(p&&de(t)){const f=he(t,p);b.push(L(`Reticle (${f.column}, ${f.row})`))}if(e==="stackedValues"){const f=P(t,0);if(f!==void 0){const u=a?.[0],h=u?lt(u,u.testNumber):"Value",v=l?` (${l})`:"";b.push(L(`${h}${v}: ${kt(f,u?.unit,o)}`));const m=jt(u);m&&b.push(`<i>${L(m)}</i>`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const f=P(t,0),u=e==="stackedSoftBins"?t.sbin:t.hbin;if(f!==void 0){const h=e==="stackedSoftBins"?T:F,v=u!==void 0?h?.get(u)?.name?`${u} \xB7 ${h.get(u).name}`:`Bin ${u}`:"Bin";let m;if(l==="percent")m=`${f.toFixed(0)}%`;else{const V=d?` (${(f/d*100).toFixed(0)}% of stacked wafers)`:"";m=`${f}${V}`}const x=l?` [${_t(l)}]`:"";b.push(L(`${v}: ${m}${x}`))}}else{const f=Re(t,a,o);if(e==="value"&&f.length){const{testNumber:u}=resolveTestNumber(g??0,a),h=findTestDef(a,u),v=P(t,u);let m=f.findIndex(A=>A.key===u);m<0&&(m=0);const x=f[m];let V=`<b>${L(`${x.label}: ${x.value}`)}${x.recordedFail?" <i>(recorded fail)</i>":""}</b>`;if(x.key===u&&h){const A=ot(v,h);(A==="failLow"||A==="failHigh")&&(V+=" <i>(out of spec)</i>")}b.push(V);const E=jt(findTestDef(a,x.key));E&&b.push(`<i>${L(E)}</i>`);const y=f.length-1;y>0&&b.push(`<i>+${y} more test${y===1?"":"s"}</i>`)}else if(f.length){const u=f.length;b.push(`<i>${u} test result${u===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const u=[];if(t.hbin!==void 0){const h=F?.get(t.hbin)?.name,v=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);u.push(`HBin: ${v}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,v=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);u.push(`SBin: ${v}`)}b.push(u.map(L).join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&b.push(L(`Retests: ${t.retestCount}`)),t.siteNum!==void 0&&b.push(L(`Site: ${t.siteNum}`)),t.partId!==void 0&&b.push(L(`Part ID: ${t.partId}`)),t.partial&&b.push("<i>partial die</i>"),t.probeIndex!==void 0&&b.push(L(`Probe: #${t.probeIndex}`));const $={...t.metadata??{}};for(const[f,u]of Object.entries($)){const h=pe(u);if(h===void 0)continue;const v=B?.find(m=>m.key===f)?.label??Wt(f);b.push(L(`${v}: ${h}`))}return b.join("<br>")}function Bt(t){return t?.name?lt(t,t.testNumber):void 0}function Tt(t){return Be(t)?{note:`${Te} ${Ie}`}:{}}export function buildMapTitle(t,e="engineering",i){const a=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=n=>{const o=i?.length===1?i[0]:void 0;return o===void 0?`${n} Bin`:o.name?`${n} Bin ${o.bin} \xB7 ${o.name}`:`${n} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:a};case"stackedSoftBins":return{primary:s("Soft"),secondary:a};case"metadata":{const n=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?Wt(t.activeMetadataKey):"Metadata";return{primary:n?.label??o,secondary:""}}case"stackedValues":{const n=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=$t(o,Bt(n),n?.unit,e),d=l||"Value",g=t.aggrMethod?` \xB7 ${_t(t.aggrMethod)}`:"";return{primary:`${d}${g}`,secondary:a,...Tt(n)}}default:{const n=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=$t(o,Bt(n),n?.unit,e),d=l||Bt(n);if(t.passFailDisplay!=="off"){const g=n?.testNumber??t.activeTest,p=d?`${d} \xB7 #${g}`:rt(void 0,g),B=t.passFailDisplay==="spec"?"Spec pass/fail":n!==void 0&&!vt(n)?"Functional pass/fail":"Tester pass/fail";return{primary:p,secondary:B,...Tt(n)}}return{primary:d??rt(void 0,t.activeTest),secondary:"",...Tt(n)}}}}export function generateTextOverlay(t,e,i){const{plotMode:a,colorFns:s,normalize:n,activeTest:o,valueRange:l,testDefs:d,fallbackFormat:g,passFailDisplay:p="off",activeMetadataKey:B,metadataColorMap:F,allIntegerValues:T}=i,b=findTestDef(d,o),{tickFmt:$}=$t(l[1],b?.name,b?.unit,g),f=T?h=>String(Math.round(h)):$,{testNumber:u}=resolveTestNumber(o,d);return t.flatMap((h,v)=>{let m="",x="#111111";if(a==="value"&&p==="test"){const y=U(h,u,b);if(y===void 0)return[];m=y?"P":"F",x=Q(y?Mt:St)}else if(a==="value"){const y=P(h,u);if(y===void 0)return[];m=zt([y],f),x=Q(s.forValue(n(y)))}else if(a==="hardBin"||a==="softBin"){const y=a==="softBin"?h.sbin:h.hbin;if(y===void 0)return[];m=String(y),x=Q(s.forBin(y))}else if(a==="metadata"){const y=ct(h,B);if(y===void 0)return[];m=y,x=Q(F?.get(m)??z)}else{const y=P(h,0);if(y===void 0)return[];m=zt([y],f),x=Q(s.forValue(n(y)))}const V=e?e[v*2]:h.physX,E=e?e[v*2+1]:h.physY;return[{x:V,y:E,text:m,fontSize:Le(h,m),color:x,align:"center"}]})}function qt(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function Ne(t,e,i=720){const{center:a,radius:s,notch:n}=t,o=$=>G(e,$.x,$.y);if(!(n!==void 0&&t.diameter>150)){const $=[];for(let f=0;f<=i;f++){const u=2*Math.PI*f/i;$.push(o(Fe(t,u)))}return[{kind:"wafer-boundary",...q($,!0),lineColor:"#888888",lineWidth:1}]}const d=1.25,g=n.length,p=qt(n.type),B=Math.atan2(g,s),F=s-d,T=[];let b=!1;for(let $=0;$<=i;$++){const f=2*Math.PI*$/i;let u=f-p;for(;u>Math.PI;)u-=2*Math.PI;for(;u<-Math.PI;)u+=2*Math.PI;if(Math.abs(u)<=B){if(!b){b=!0;const h={x:a.x+s*Math.cos(p-B),y:a.y+s*Math.sin(p-B)},v={x:a.x+F*Math.cos(p),y:a.y+F*Math.sin(p)},m={x:a.x+s*Math.cos(p+B),y:a.y+s*Math.sin(p+B)};T.push(o(h),o(v),o(m))}continue}T.push(o({x:a.x+s*Math.cos(f),y:a.y+s*Math.sin(f)}))}return[{kind:"wafer-boundary",...q(T,!0),lineColor:"#888888",lineWidth:1}]}function Oe(t,e,i,a=360){const s=[],n=Math.max(1,Math.floor(i));for(let o=1;o<n;o++){const l=t.radius*o/n,d=[];for(let g=0;g<=a;g++){const p=2*Math.PI*g/a,B={x:t.center.x+l*Math.cos(p),y:t.center.y+l*Math.sin(p)};d.push(G(e,B.x,B.y))}s.push({kind:"ring-boundary",...q(d,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function Ee(t,e,i,a){const{x:s,y:n}=t.center,o=t.radius,l=(f,u)=>G(e,f,u),d=i-s,g=Math.sqrt(Math.max(0,o*o-d*d)),p=l(i,n-g),B=l(i,n+g),F=a-n,T=Math.sqrt(Math.max(0,o*o-F*F)),b=l(s-T,a),$=l(s+T,a);return[{kind:"quadrant-boundary",...q([p,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...q([b,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function Ae(t,e){return t.map(i=>{const a=i.width/2,s=i.height/2;return{kind:"reticle",points:[[[i.x-a,i.y-s],[i.x+a,i.y-s],[i.x+a,i.y+s],[i.x-a,i.y+s]].map(([o,l])=>G(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function He(t){const e=t.filter(i=>i.probeIndex!==void 0).sort((i,a)=>(i.probeIndex??0)-(a.probeIndex??0));return e.length?[{kind:"probe-path",...q(e.map(i=>({x:i.physX,y:i.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function _e(t,e,i,a,s,n,o,l,d,g,p,B,F,T,b="off",$,f,u){const h=e.width-o,v=e.height-o,m=n?v:h,x=n?h:v,V=S=>s==="softBin"?S.sbin:S.hbin,E=S=>ct(S,$);if(e.partial){t.push({x:i,y:a,width:m,height:x,fill:De,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:i,y:a,width:m,height:x,fill:Ve,type:"stacked",metadata:e.metadata});return}if(d!==void 0&&(s==="hardBin"||s==="softBin")&&V(e)!==d){t.push({x:i,y:a,width:m,height:x,fill:wt,type:"hardBin",metadata:e.metadata});return}if(u!==void 0&&s==="metadata"&&E(e)!==u){t.push({x:i,y:a,width:m,height:x,fill:wt,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(b==="test"){const j=U(e,p,T),ut=j===void 0?z:j?Mt:St;t.push({x:i,y:a,width:m,height:x,fill:ut,type:"value",metadata:e.metadata});return}const S=P(e,p),N=ot(S,T);let W,J;S===void 0?W=z:b==="spec"?W=N==="failLow"?ge:N==="failHigh"?St:Mt:(W=l.forValue(g(S)),(N==="failLow"||N==="failHigh")&&(J=N)),t.push({x:i,y:a,width:m,height:x,fill:W,type:"value",specMark:J,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const S=V(e),N=S!=null?l.forBin(S):z,W=S!=null&&Se(e,F)===!1?!0:void 0;t.push({x:i,y:a,width:m,height:x,fill:N,type:s,binFail:W,metadata:e.metadata});return}if(s==="metadata"){const S=E(e),N=S!==void 0?f?.get(S)??z:z;t.push({x:i,y:a,width:m,height:x,fill:N,type:"metadata",metadata:e.metadata});return}const y=P(e,0),A=y!==void 0?l.forValue(g(y)):z;t.push({x:i,y:a,width:m,height:x,fill:A,type:"value",metadata:e.metadata})}function Ke(t,e,i){const a=t.radius*.15,s=xt(e,a,0),n=xt(e,0,a),o=s.x+n.x>=0?-1:1,l=s.y+n.y>=0?-1:1,d={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},g={x:d.x+s.x,y:d.y+s.y},p={x:d.x+n.x,y:d.y+n.y};return i.push({x:g.x+s.x*.35,y:g.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:p.x+n.x*.35,y:p.y+n.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...q([d,g]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...q([d,p]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,i={},a){const s=i.reticles??[],{plotMode:n="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:d=!1,ringCount:g=4,showRingBoundaries:p=!1,showQuadrantBoundaries:B=!1,showXYIndicator:F=!1,binColorScheme:T="default",valueColorScheme:b="default",reverseValueScheme:$=!1,useDefinedBinColors:f=!0,binColors:u,passBins:h=[1],highlightBin:v,highlightMetadataValue:m,interactiveTransform:x,valueRange:V,metadataValueOrder:E,testDefs:y,activeTest:A=0,activeMetadataKey:S,fallbackFormat:N="engineering",aggregationMethod:W,lotSize:J,dataAxisFlip:j,isLotStack:ut=!1,logScale:Xt,colorbarRangeMode:Yt="spec",passFailDisplay:Gt,showPartialDies:Qt=!0}=i,It=Gt??"off",ft=a?.hbinDefs,dt=a?.sbinDefs,Dt=a?.metadataFields,Ut=Dt?.find(c=>c.key===S),Jt={x:(j?.x??!1)!==(x?.flipX??!1),y:(j?.y??!1)!==(x?.flipY??!1)},Zt=ft?new Map(ft.map(c=>[c.bin,c])):null,te=dt?new Map(dt.map(c=>[c.bin,c])):null,ee=n==="softBin"?te:Zt,ne=new Set(h),ht=u&&Me(u,e)?u:xe(e,{passBins:h,binColorScheme:T,hbinDefs:ft,sbinDefs:dt,useDefinedBinColors:f}),ae=n==="softBin"?ht.soft:ht.hard,Vt={forValue:Pe(be(b,$)),forBin:c=>ae.get(c)??z};let{testNumber:H}=resolveTestNumber(A,y);if(!y?.length&&n==="value"&&!e.some(r=>r.testValues&&H in r.testValues)){const r=e.reduce((M,D)=>{if(!D.testValues)return M;const k=Object.keys(D.testValues).map(Number),R=k.length?$e(k):void 0;return R!==void 0&&(M===void 0||R<M)?R:M},void 0);r!==void 0&&(H=r)}let Z;if(Array.isArray(V))Z=V;else if(V){const{testNumber:c}=resolveTestNumber(V.test,y);Z=c===H?V.range:void 0}const I=findTestDef(y,H),Ct=n==="value"&&I!==void 0&&!vt(I),ie=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let K="off";n==="value"&&(Ct?K="test":It==="spec"&&ie?K="spec":It==="test"&&e.some(c=>U(c,H,I)!==void 0)&&(K="test"));const Ft=K==="spec"?"spec":Yt,mt=n==="stackedValues"||n==="stackedBins"||n==="stackedSoftBins";let O,_,tt=!1;if(Z){[O,_]=Z;let c=!0,r=!1;for(const M of e){const D=mt?P(M,0):P(M,H);if(D!==void 0&&(r=!0,!Number.isInteger(D))){c=!1;break}}tt=r&&c}else{const c=!mt&&n==="value"&&Ft==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let r=1/0,M=-1/0,D=!0;for(const k of e){const R=mt?P(k,0):P(k,H);R!==void 0&&(R<r&&(r=R),R>M&&(M=R),Number.isInteger(R)||(D=!1))}isFinite(r)||(D=!1),tt=D,c?(O=I.limitLow!==void 0?I.limitLow:isFinite(r)?r:0,_=I.limitHigh!==void 0?I.limitHigh:isFinite(M)?M:1):(O=isFinite(r)?r:0,_=isFinite(M)?M:1)}O===_&&(_=O+1);const Pt=Ct?!1:Xt??I?.logScale??!1,Lt=Pt&&O>0&&_>0,se=Lt;let et;if(Lt){const c=Math.log10(O),r=Math.log10(_)-c;et=M=>M<=0?0:Kt((Math.log10(M)-c)/r)}else et=c=>Kt((c-O)/(_-O));const C=Ce(t,j,x);let Rt=null;if(t.notch){const c=qt(t.notch.type),r=xt(C.physicalToScreen,Math.cos(c)*t.radius,Math.sin(c)*t.radius),M=Math.hypot(r.x,r.y);M>0&&(Rt={x:r.x/M,y:r.y/M})}let X=1/0;for(let c=0;c<e.length;c++){const r=e[c];r.width<X&&(X=r.width),r.height<X&&(X=r.height)}const Nt=1,oe=X!==1/0?Math.min(Nt,X*.12):Nt,Ot=[],pt=[],re=C.bakedToScreen.a===1&&C.bakedToScreen.b===0&&C.bakedToScreen.c===0&&C.bakedToScreen.d===1&&C.bakedToScreen.e===0&&C.bakedToScreen.f===0;let w=null;if(!re){w=new Float64Array(e.length*2);for(let c=0;c<e.length;c++){const r=G(C.bakedToScreen,e[c].physX,e[c].physY);w[c*2]=r.x,w[c*2+1]=r.y}}const le=ye(C.gridToScreen),ce=n==="hardBin"||n==="softBin"||n==="stackedBins"||n==="stackedSoftBins",ue=n==="softBin"||n==="stackedSoftBins";let nt=ce?new Map:void 0;const yt=n==="value"&&K==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,gt=n==="value"&&K==="test"?{pass:0,fail:0}:void 0,Et=n==="metadata";let at=Et?new Map:void 0,it=null;if(Et&&S){const c=collectMetadataValues(e,S),r=E?.key===S?E.values:void 0,M=r?[...r,...c.filter(D=>!r.includes(D))]:c;it=new Map(M.map((D,k)=>[D,Ut?.values?.find(R=>R.value===D)?.color??ve(k)]))}for(let c=0;c<e.length;c++){const r=e[c],M=w?w[c*2]:r.physX,D=w?w[c*2+1]:r.physY;if(pt.push({x:M,y:D}),nt&&!r.partial&&!r.edgeExcluded){const k=ue?r.sbin:r.hbin;k!=null&&nt.set(k,(nt.get(k)??0)+1)}if(yt&&!r.partial&&!r.edgeExcluded){const k=ot(P(r,H),I);k&&yt[k]++}if(gt&&!r.partial&&!r.edgeExcluded){const k=U(r,H,I);k!==void 0&&gt[k?"pass":"fail"]++}if(at&&!r.partial&&!r.edgeExcluded){const k=ct(r,S);k!==void 0&&at.set(k,(at.get(k)??0)+1)}r.partial&&!Qt||_e(Ot,r,M,D,n,le,oe,Vt,v,et,H,ee,ne,I,K,S,it,m)}const At=o?generateTextOverlay(e,w,{plotMode:n,colorFns:Vt,normalize:et,activeTest:A,valueRange:[O,_],testDefs:y,fallbackFormat:N,passFailDisplay:K,activeMetadataKey:S,metadataColorMap:it,allIntegerValues:tt}):[],Y=Ne(t,C.physicalToScreen);p&&Y.push(...Oe(t,C.physicalToScreen,g)),B&&Y.push(...Ee(t,C.physicalToScreen,t.center.x,t.center.y)),l&&Y.push(...Ae(s,C.gridToScreen)),d&&Y.push(...He(e)),F&&Y.push(...Ke(t,C.gridToScreen,At));const fe=pt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Ot,hoverPoints:pt,texts:At,overlays:Y,plotMode:n,binColorScheme:T,valueColorScheme:b,reverseValueScheme:$,binColors:ht,metadata:t.metadata??null,dies:e,valueRange:[O,_],allIntegerValues:tt,testDefs:y,activeTest:A,logScale:se,logScaleRequested:Pt,aggrMethod:W,lotSize:J,axisFlip:Jt,dataAxisFlip:{x:j?.x??!1,y:j?.y??!1},gridToScreen:C.gridToScreen,rotation:((t.orientation+(x?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:ut,passFailDisplay:K,colorbarRangeMode:Ft,waferCenter:t.center,waferRadius:t.radius,notchDir:Rt,binCounts:nt,metadataCounts:at,metadataColorMap:it,activeMetadataKey:S,metadataFields:Dt,specCounts:yt,passFailCounts:gt,dieBounds:fe}}export{getDieKey}from"../core/dies.js";
@@ -2,6 +2,8 @@ import type { Die } from '../core/dies.js';
2
2
  import type { DieMetadata, WaferMetadata } from '../core/metadata.js';
3
3
  import type { Wafer } from '../core/wafer.js';
4
4
  import type { Reticle } from '../core/reticle.js';
5
+ import { type DerivedTestDef } from './derivedTests/apply.js';
6
+ export type { DerivedTestDef } from './derivedTests/apply.js';
5
7
  import { type View, type ViewOptions, type PlotMode } from './buildView.js';
6
8
  /**
7
9
  * Test result for a single die position, as output by the prober.
@@ -232,6 +234,24 @@ export interface TestDef {
232
234
  * would be meaningless. Default: `'P'`.
233
235
  */
234
236
  testType?: 'P' | 'F';
237
+ /**
238
+ * Set by `buildWaferMap` on a test computed from `derivedTests`, never by the
239
+ * caller. It lives on `TestDef` rather than only on `DerivedTestDef` because
240
+ * `result.testDefs` is one homogeneous `TestDef[]`: every display surface
241
+ * reads its test names from there, and a derived value must be
242
+ * distinguishable from a measured one without a cast — an engineer reading a
243
+ * Cpk table cannot be left to assume a number came off the tester.
244
+ */
245
+ readonly derived?: true;
246
+ /**
247
+ * The expression a `derived` test was computed from, carried through verbatim
248
+ * so a display surface can answer "where did this number come from" without
249
+ * the caller having to hold the definition alongside the result. Absent on a
250
+ * measured test.
251
+ */
252
+ readonly expression?: string;
253
+ /** Named constants the `expression` resolved, for the same reason. */
254
+ readonly constants?: Record<string, number>;
235
255
  }
236
256
  /**
237
257
  * True when `def` describes a parametric (continuous-value) test — i.e. its
@@ -368,6 +388,34 @@ export interface WaferMapInputBase {
368
388
  * Both spaces range 0–32767 and may overlap — define them separately.
369
389
  */
370
390
  sbinDefs?: BinDef[];
391
+ /**
392
+ * Tests computed from other tests on the same die, rather than measured.
393
+ *
394
+ * Each entry is a `TestDef` plus an `expression`. They are evaluated per probe
395
+ * record at build time — before lot stacking and before retest resolution, so
396
+ * every value is derived from one real touchdown — and then behave as ordinary
397
+ * tests: they appear in `testDefs`, in the value plot modes, the colorbar,
398
+ * tooltips, `analyzeWaferMap`, Insights and the report.
399
+ *
400
+ * ```ts
401
+ * derivedTests: [
402
+ * { testNumber: 900001, name: 'Leakage Shift', unit: 'uA',
403
+ * expression: 'abs(t[1020] - t[1010])', limitHigh: 5 },
404
+ * { testNumber: 900002, name: 'Sweep All Pass', testType: 'F',
405
+ * expression: 'all(testPass[1010..1025])' },
406
+ * ]
407
+ * ```
408
+ *
409
+ * Expressions are parsed to a typed tree and walked — there is no `eval`, no
410
+ * `Function`, and no way to reach a host object — so a spec can be shared as
411
+ * plain JSON between teams. An entry that cannot be compiled is DROPPED with a
412
+ * `derived-test-invalid` warning naming the position; it is never partially
413
+ * applied.
414
+ *
415
+ * A boolean expression is a verdict: declare `testType: 'F'` and the result
416
+ * lands in `die.testPass`, never as a 1/0 in `testValues`.
417
+ */
418
+ derivedTests?: DerivedTestDef[];
371
419
  /**
372
420
  * Named definitions for `die.metadata` keys that should be selectable as the
373
421
  * `'metadata'` plot mode — a generic categorical view driven by whatever
@@ -519,6 +567,15 @@ export interface WaferWarning {
519
567
  * were given as text (or verdicts as something other than true/false), as a
520
568
  * CSV parser produces. They are not converted, so those dies are judged and
521
569
  * plotted wrongly — a bin of "1" is not pass bin 1. The message counts them.
570
+ * - `'derived-test-invalid'` — raised by `buildWaferMap`: a `derivedTests` entry
571
+ * could not be compiled — a parse or type error in its `expression`, a
572
+ * `testNumber` that collides with a measured test, a `testType` that
573
+ * disagrees with what the expression produces, or an accessor that is
574
+ * statically impossible (`t[n]` on a functional test, `specPass[n]` on a test
575
+ * with no limits). The message names the test and the position in the
576
+ * expression. That derived test is DROPPED — never partially applied, since a
577
+ * half-working expression plots wrong numbers rather than no numbers. Also
578
+ * used for the non-fatal unit-mismatch advisory, where the value IS computed.
522
579
  * - `'input-field-removed'` — raised by `buildWaferMap`: the input used a name
523
580
  * removed in an earlier release (`data`, `die`, `stack`, `values`,
524
581
  * `TestDef.index`, `dieConfig.origin`, `waferConfig.flat`,
@@ -529,7 +586,7 @@ export interface WaferWarning {
529
586
  * The union is intentionally open to string so future advisory codes can be
530
587
  * added without a breaking change; switch with a `default` branch.
531
588
  */
532
- code: 'partial-coverage' | 'geometry-conflict' | 'non-standard-diameter' | 'diameter-exceeds-die-extent' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | 'analysis-option-corrected' | 'bin-colors-shared' | 'pass-bins-mixed' | 'ring-count-mixed' | 'input-field-removed' | 'input-values-not-numbers' | (string & {});
589
+ code: 'partial-coverage' | 'geometry-conflict' | 'non-standard-diameter' | 'diameter-exceeds-die-extent' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | 'analysis-option-corrected' | 'bin-colors-shared' | 'pass-bins-mixed' | 'ring-count-mixed' | 'input-field-removed' | 'input-values-not-numbers' | 'derived-test-invalid' | (string & {});
533
590
  /** Human-readable explanation, suitable for direct display. */
534
591
  message: string;
535
592
  /**
@@ -1 +1 @@
1
- import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as le,getDieKey as T,hasPosition as A}from"../core/dies.js";import{applyOrientation as H,transformDies as ue,clipDiesToWafer as he}from"../core/transforms.js";import{generateDies as me}from"../core/dies.js";import{affineRotation as ge,affineMirror as pe,affineCompose as ye,affinePoint as be}from"../core/transforms.js";import{inferWaferFromXY as we}from"../core/inference/wafer.js";import{resolveGridPitch as xe}from"../core/inference/pitch.js";import{assignGridIndices as De}from"../core/inference/grid.js";import{generateReticleGrid as Me}from"../core/reticle.js";import{buildView as U}from"./buildView.js";import{modeOf as Ce}from"../core/utils.js";import{aggregateValues as J,aggregateBinCounts as ve}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function Oe(t){const n=(l,s)=>({ringCount:l,ringCountWarning:{code:"analysis-option-corrected",severity:"warning",message:`ringCount=${String(t)} ${s} (using ${l}).`}});if(t===void 0)return{ringCount:4,ringCountWarning:void 0};if(typeof t!="number"||!Number.isFinite(t))return n(4,"is not a finite number");const e=Math.max(1,Math.round(t));return e===t?{ringCount:t,ringCountWarning:void 0}:n(e,"is not a whole number of at least 1")}function Q(t){const n=Array.isArray(t)?{}:t,e=Array.isArray(n.lotStack?.results)?n.lotStack.results:[],l=(Array.isArray(t)?[t]:[n.results,...e]).filter(a=>Array.isArray(a)),s={hbin:0,sbin:0,testValues:0,testPass:0};let r;const h=(a,i)=>{s[a]++,r??=`${a} ${JSON.stringify(i)}`};for(const a of l){const i=new Set,f=new Set;for(let p=0;p<a.length;p+=p<50?1:100){const m=a[p];if(!(m===null||typeof m!="object")){if(m.testValues&&typeof m.testValues=="object")for(const D of Object.keys(m.testValues))i.add(D);if(m.testPass&&typeof m.testPass=="object")for(const D of Object.keys(m.testPass))f.add(D)}}const u=[...i],d=[...f];for(const p of a){if(p===null||typeof p!="object")continue;const m=p;m.hbin!=null&&typeof m.hbin!="number"&&h("hbin",m.hbin),m.sbin!=null&&typeof m.sbin!="number"&&h("sbin",m.sbin);const D=m.testValues;if(D&&typeof D=="object")for(let y=0;y<u.length;y++){const b=D[u[y]];b!=null&&typeof b!="number"&&h("testValues",b)}const C=m.testPass;if(C&&typeof C=="object")for(let y=0;y<d.length;y++){const b=C[d[y]];b!=null&&typeof b!="boolean"&&h("testPass",b)}}}const o=[];if(s.hbin&&o.push(`${s.hbin} hard bin${s.hbin===1?"":"s"}`),s.sbin&&o.push(`${s.sbin} soft bin${s.sbin===1?"":"s"}`),s.testValues&&o.push(`${s.testValues} test value${s.testValues===1?"":"s"}`),s.testPass&&o.push(`${s.testPass} pass/fail verdict${s.testPass===1?"":"s"}`),!o.length)return;const g=`buildWaferMap received ${o.join(", ")} of the wrong type (for example ${r}). Bins and test values must be numbers and verdicts true/false \u2014 a bin of "1" is not pass bin 1, so those dies count as fails and yield is wrong, and a test value given as text is not plotted or analysed correctly. Convert them with Number() (and verdicts to booleans) and rebuild.`;return console.warn(`[wafermap] ${g}`),{code:"input-values-not-numbers",message:g,severity:"error"}}function Z(t){const n=(a,i)=>a!==null&&typeof a=="object"&&a[i]!==void 0,e=Array.isArray(t)?{}:t,l=[],s=(a,i,f)=>{a&&l.push(`\`${i}\` (now \`${f}\`)`)};s(n(e,"data"),"data","results"),s(n(e,"die"),"die","dieConfig"),s(n(e,"stack"),"stack","lotStack"),s(n(e.dieConfig,"origin"),"dieConfig.origin","dieConfig.coordinateOrigin"),s(n(e.waferConfig,"flat"),"waferConfig.flat","waferConfig.notch"),s(n(e.reticleConfig,"anchor"),"reticleConfig.anchor","reticleConfig.anchorDie"),s(n(e.lotStack,"aggr"),"lotStack.aggr","lotStack.method"),s(Array.isArray(e.testDefs)&&e.testDefs.some(a=>n(a,"index")&&!n(a,"testNumber")),"TestDef.index","TestDef.testNumber");const r=n(e.lotStack,"results")?e.lotStack.results:[],h=(Array.isArray(t)?[t]:[e.results,...Array.isArray(r)?r:[]]).filter(a=>Array.isArray(a));if(s(h.some(a=>a.slice(0,100).some(i=>n(i,"values"))),"values","testValues"),!l.length)return;const o=l.length>1,g=`buildWaferMap ignored ${l.join(", ")}: ${o?"these names were":"that name was"} removed in an earlier release, so what ${o?"they":"it"} described is not on this map. Rename and rebuild.`;return console.warn(`[wafermap] ${g}`),{code:"input-field-removed",message:g,severity:"error"}}function Se(t){if(t.results!==void 0||t.lotStack!==void 0)throw new Error("buildWaferMap: pass `layout`, `results` or `lotStack` \u2014 only one.");const n=t.waferConfig?.diameter,e=t.dieConfig?.width,l=t.dieConfig?.height;if(!(n>0&&e>0&&l>0))throw new Error("buildWaferMap: `layout` needs waferConfig.diameter and dieConfig.width and height, all positive.");const s=j({diameter:n,notch:t.waferConfig?.notch,orientation:0}),r=he(me(s,{width:e,height:l}),s,{width:e,height:l}).filter(i=>!i.partial),{flipX:h,flipY:o}=ee(t.dieConfig,{type:"center"}),{layout:g,...a}=t;return{...a,results:r.map(i=>({x:h?-i.x:i.x,y:o?-i.y:i.y})),waferConfig:{...t.waferConfig,center:{x:0,y:0}},dieConfig:{...t.dieConfig,coordinateOrigin:{type:"center"}}}}function ke(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],ringCount:4,ringCountWarning:void 0,removedFieldWarning:Z(t),nonNumericWarning:Q(t),standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],...Oe(t.ringCount),removedFieldWarning:Z(t),nonNumericWarning:Q(t),standardDiameters:t.standardDiameters,testDefs:t.testDefs?.length?t.testDefs:void 0,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Ae(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Re(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let l=1/0,s=-1/0,r=1/0,h=-1/0;for(const o of t)o.x<l&&(l=o.x),o.x>s&&(s=o.x),o.y<r&&(r=o.y),o.y>h&&(h=o.y);return{offsetX:Math.round((s+l)/2),offsetY:Math.round((h+r)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function $e(t,n,e,l,s,r){if(r)return{colMidX:(Math.round(r.x)-n)*l,colMidY:(Math.round(r.y)-e)*s,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let h=1/0,o=-1/0,g=1/0,a=-1/0;for(const i of t){const f=Math.round(i.x)-n,u=Math.round(i.y)-e;f<h&&(h=f),f>o&&(o=f),u<g&&(g=u),u>a&&(a=u)}return{colMidX:(h+o)/2*l,colMidY:(g+a)/2*s,anchored:!1}}function We(t,n,e){let l=1/0,s=-1/0,r=1/0,h=-1/0,o=0,g=0;for(const f of t){const u=Math.round(f.x)-n,d=Math.round(f.y)-e;o+=u,g+=d,u<l&&(l=u),u>s&&(s=u),d<r&&(r=d),d>h&&(h=d)}const a=t.length,i=(f,u,d)=>{const p=(d-u)/2;return p<=0?0:Math.abs(f/a-(u+d)/2)/p};return i(o,l,s)>.11||i(g,r,h)>.11}function ee(t,n){let e=t?.xAxisDirection==="left",l=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(l=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:l}}function Ee(t,n){const{method:e,targetBin:l}=t,s=t.results.map(r=>r.filter(A));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const r=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),h=new Set;for(const i of s)for(const f of i)if(f.testValues)for(const u of Object.keys(f.testValues)){const d=Number(u);r.has(d)||h.add(d)}if(h.size===0)return J(s,e);const o=[...h],g=o.map(i=>J(s,e,i)),a=new Map;for(let i=0;i<o.length;i++){const f=o[i];for(const u of g[i]){const d=T(u);a.has(d)||a.set(d,{template:u,testValues:{}});const p=a.get(d),m=u.testValues?.[0];m!==void 0&&(p.testValues[f]=m)}}return[...a.values()].map(({template:i,testValues:f})=>({...i,testValues:Object.keys(f).length>0?f:void 0}))}if(e==="countBin"||e==="percent"){if(l===void 0)return[];const r=ve(s,l,"hard");if(e==="countBin")return r;const h=s.length;return r.map(o=>({...o,testValues:{0:h>0?(o.testValues?.[0]??0)/h*100:0}}))}if(e==="mode"){const r=new Map;for(const o of s)for(const g of o){const a=T(g);r.has(a)||r.set(a,[]),r.get(a).push(g)}const h=[];for(const[o,g]of r){const a=o.split(","),i=Number(a[0]),f=Number(a[1]),u=g.map(p=>p.hbin).filter(p=>p!==void 0),d=Ce(u);d!==null&&h.push({x:i,y:f,hbin:d})}return h}return[]}function te(t){const n=t.filter(A),e=n.length,l=n.filter(r=>r.edgeExcluded).length,s=n.filter(r=>dieHasTestData(r)||r.hbin!==void 0||r.sbin!==void 0).length;return{filledDies:s,totalDies:e,edgeExcludedDies:l,unpositionedDies:t.length-n.length,ratio:e>0?s/e:0}}function ne(t,n,e="exclude"){const l=new Set(n),r=t.filter(d=>!d.partial).filter(d=>d.edgeExcluded).length,h=t.filter(d=>d.partial).length;let o=0,g=0,a=!1;for(const d of t){if(!le(d))continue;const p=d.hbin??d.sbin;p!==void 0&&(a=!0,l.has(p)?o++:g++)}const i=o+g,f=a&&i>0?o/i*100:null;let u=null;if(e==="denominator-only"){const d=i+r;u=a&&d>0?o/d*100:null}return{passDies:o,failDies:g,edgeExcludedDies:r,partialDies:h,totalDies:i,yieldPercent:f,yieldPercentGross:u}}function se(t,n,e,l,s,r=0,h=0,o=0,g=0,a=0,i=!1,f=!1){if(!t)return[];const u=t.anchorDie??{x:0,y:0},d=Me(n,{width:t.width,height:t.height,diePitchX:l,diePitchY:s,anchorDie:{x:u.x-r,y:u.y-h},gridOrigin:{x:-o,y:-g}}),p=ye(pe(i,f,n.center.x,n.center.y),ge(a,n.center.x,n.center.y));return d.filter(m=>{const D=m.width/2,C=m.height/2,y=[[m.x-D,m.y-C],[m.x+D,m.y-C],[m.x+D,m.y+C],[m.x-D,m.y+C]].map(([w,B])=>be(p,w,B)),b=Math.min(...y.map(w=>w.x)),R=Math.max(...y.map(w=>w.x)),$=Math.min(...y.map(w=>w.y)),F=Math.max(...y.map(w=>w.y));return e.some(w=>w.physX>=b&&w.physX<R&&w.physY>=$&&w.physY<F)})}function Fe(t,n,e){const s=Math.max(0,n.radius-e)**2;return t.map(r=>{const h=r.physX-n.center.x,o=r.physY-n.center.y;return h*h+o*o>s?{...r,edgeExcluded:!0}:r})}function Pe(t,n,e){const l=t.filter(A),s=t.filter(i=>!A(i)),r=new Map;for(const i of l){let f=r.get(i.x);f||(f=new Map,r.set(i.x,f)),f.set(i.y,(f.get(i.y)??0)+1)}const h=new Set(e);function o(i,f){const u=i.hbin,d=f.hbin;if(u===void 0||d===void 0)return!1;const p=h.has(u),m=h.has(d);return n==="best"?m!==p?m:d<u:m!==p?p:d>u}const g=new Map;for(const i of l){const f=T(i),u=g.get(f);n==="first"&&u||(n==="best"||n==="worst")&&u&&!o(u,i)||g.set(f,i)}return[...Array.from(g.values()).map(i=>{const u=r.get(i.x)?.get(i.y)??1;return u>1?{...i,retestCount:u}:i}),...s]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const l=t.testPass?.[n];if(l!==void 0)return l;if(e!==void 0&&!isParametricTest(e)){const s=t.testValues?.[n];if(s===0||s===1)return s===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,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 ie(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function oe(t,n){return t.map(({code:e,message:l})=>({code:e,message:l,severity:e==="non-standard-diameter"||e==="diameter-exceeds-die-extent"?"warning":"error",confidence:n.wafer.confidence}))}const Ie="do not fit inside the supplied",Ne="Inferred wafer diameter",Ve="The probed dies reach only",Te=.75,_e=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const Be=.02;function re(t,n){return n.reduce((e,l)=>Math.abs(l-t)<Math.abs(e-t)?l:e)}function Ye(t,n){if(!n.length)return!0;const e=re(t,n);return Math.abs(e-t)/e<=Be}function Le(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=ke(!Array.isArray(t)&&t.layout?Se(t):t),{debug:l,...s}=n??{},r=e.lotStackOpts?Ee(e.lotStackOpts,e.testDefs):e.results;for(let c=0;c<Math.min(5,r.length);c++){const x=r[c];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 c of r)if(c.x==null!=(c.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(c.x)}, y=${String(c.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const h=Pe(r,e.retestPolicy,e.passBins),o={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}},g=[];if(e.explicitDies){let c=e.explicitDies;const x=h.filter(A),S=h.filter(V=>!A(V));if(x.length>0){const V=new Map(x.map(k=>[T(k),k]));c=c.map(k=>{const q=V.get(T(k));return q?_(k,q):k})}const N=e.waferOpts?.diameter??300,E=j({diameter:N,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});E.orientation!==0&&(c=H(c,E));const G=se(e.reticleOpts,E,c,1,1,0,0,0,0,E.orientation),fe=s.showReticle??e.reticleOpts!==void 0,L=U(E,c,{...s,reticles:G,showReticle:fe,plotMode:ie(h,s),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ce=S.map((V,k)=>_({id:`unpositioned_${k}`,width:c[0]?.width??1,height:c[0]?.height??1},V)),X=[...c,...ce];return{wafer:E,dies:X,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:o,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...oe(g,o),...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:te(X),passBins:e.passBins,ringCount:e.ringCount,yield:ne(X,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const a=h.filter(A),i=h.filter(c=>!A(c)),f=a.map(c=>({x:c.x,y:c.y})),u=xe(f,e.dieOpts,e.waferOpts?.diameter);o.diePitch={confidence:u.confidence,units:u.units};const{pitchX:d,pitchY:p}=u,m=u.units,D=Ae(a,e.dieOpts),C=De(f);o.grid={confidence:C.confidence};const{offsetX:y,offsetY:b}=Re(f,D,C),{flipX:R,flipY:$}=ee(e.dieOpts,D),{colMidX:F,colMidY:w,anchored:B}=$e(f,y,b,d,p,e.waferOpts?.center);let M=e.waferOpts?.diameter;const P=f.map(c=>({x:(Math.round(c.x)-y)*d-F,y:(Math.round(c.y)-b)*p-w})),W=P.length>0?Math.max(...P.map(({x:c,y:x})=>Math.hypot(Math.abs(c)+d/2,Math.abs(x)+p/2))):0;if(M===void 0)if(f.length>0)if(m==="mm"){const c=we(P,{minRadius:W});M=c.diameter,o.wafer={confidence:c.confidence,method:c.method};const x=Le(e.standardDiameters);if(!Ye(M,x)){const S=re(M,x);g.push({code:"non-standard-diameter",message:`${Ne} ${M.toFixed(1)} mm is not a standard wafer size (nearest is ${S} mm). The diameter was derived from the die extent, which assumes the probed grid reaches the wafer edge \u2014 a non-standard result usually means it does not, because the outer dies were never probed. Every die is then placed against a wafer that is too small, which moves dies between rings and changes ring and edge findings. Supply waferConfig.diameter.`})}}else M=W*2,o.wafer={confidence:u.confidence*.8,method:"extent"};else M=m==="mm"?300:30,o.wafer={confidence:0,method:"default"};if(!B&&f.length>0&&We(f,y,b)&&(o.wafer.method="inferred-partial",g.push({code:"partial-coverage",message:"Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly."})),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&W>0&&f.length>=_e){const c=W/(M/2);c<Te&&g.push({code:"diameter-exceeds-die-extent",message:`${Ve} ${(c*100).toFixed(0)}% of the ${M} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`})}if(e.waferOpts?.diameter!==void 0){const c=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(c&&W>M/2+1e-9){const x=P.filter(({x:S,y:N})=>Math.hypot(Math.abs(S)+d/2,Math.abs(N)+p/2)>M/2+1e-9).length;g.push({code:"geometry-conflict",message:`${x} of ${P.length} probed die positions ${Ie} ${M} mm wafer at the supplied die pitch of ${d} \xD7 ${p} 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 ${(W*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`})}}const O=j({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let v=a.map(c=>{const x=Math.round(c.x)-y,S=Math.round(c.y)-b,N={id:`${x}_${S}`,x,y:S,physX:x*d-F,physY:S*p-w,width:d,height:p,insideWafer:!0,partial:!1};return _(N,c)});(y!==0||b!==0)&&(v=v.map(c=>({...c,x:c.x+y,y:c.y+b,id:`${c.x+y}_${c.y+b}`}))),v=H(v,O),(R||$)&&(v=ue(v,{flipX:R,flipY:$},O.center));const K=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(v=Fe(v,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&K.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 z=se(e.reticleOpts,O,v,d,p,y,b,F,w,O.orientation,R,$),ae=s.showReticle??e.reticleOpts!==void 0,I=U(O,v,{...s,reticles:z,showReticle:ae,plotMode:ie(h,s),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,dataAxisFlip:{x:R,y:$},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),de=i.map((c,x)=>_({id:`unpositioned_${x}`,width:d,height:p},c)),Y=[...v,...de];return{wafer:O,dies:Y,view:I,reticleConfig:e.reticleOpts,units:m,inference:o,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...oe(g,o),...K,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:I.plotMode,metadata:I.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:te(Y),passBins:e.passBins,ringCount:e.ringCount,yield:ne(Y,e.passBins,e.edgeDieYieldMode),reticles:z,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:I.aggrMethod,lotSize:I.lotSize}}
1
+ import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as ge,getDieKey as V,hasPosition as R}from"../core/dies.js";import{applyOrientation as U,transformDies as pe,clipDiesToWafer as ye}from"../core/transforms.js";import{generateDies as be}from"../core/dies.js";import{affineRotation as we,affineMirror as xe,affineCompose as De,affinePoint as Me}from"../core/transforms.js";import{inferWaferFromXY as ve}from"../core/inference/wafer.js";import{resolveGridPitch as Ce}from"../core/inference/pitch.js";import{assignGridIndices as Oe}from"../core/inference/grid.js";import{generateReticleGrid as Se}from"../core/reticle.js";import{applyDerivedTests as J}from"./derivedTests/apply.js";import{buildView as Q}from"./buildView.js";import{maxOf as K,minOf as Z,modeOf as ke}from"../core/utils.js";import{aggregateValues as ee,aggregateBinCounts as Ae}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function Re(t){const n=(c,i)=>({ringCount:c,ringCountWarning:{code:"analysis-option-corrected",severity:"warning",message:`ringCount=${String(t)} ${i} (using ${c}).`}});if(t===void 0)return{ringCount:4,ringCountWarning:void 0};if(typeof t!="number"||!Number.isFinite(t))return n(4,"is not a finite number");const e=Math.max(1,Math.round(t));return e===t?{ringCount:t,ringCountWarning:void 0}:n(e,"is not a whole number of at least 1")}function te(t){const n=Array.isArray(t)?{}:t,e=Array.isArray(n.lotStack?.results)?n.lotStack.results:[],c=(Array.isArray(t)?[t]:[n.results,...e]).filter(a=>Array.isArray(a)),i={hbin:0,sbin:0,testValues:0,testPass:0};let o;const u=(a,s)=>{i[a]++,o??=`${a} ${JSON.stringify(s)}`};for(const a of c){const s=new Set,m=new Set;for(let p=0;p<a.length;p+=p<50?1:100){const h=a[p];if(!(h===null||typeof h!="object")){if(h.testValues&&typeof h.testValues=="object")for(const x of Object.keys(h.testValues))s.add(x);if(h.testPass&&typeof h.testPass=="object")for(const x of Object.keys(h.testPass))m.add(x)}}const f=[...s],l=[...m];for(const p of a){if(p===null||typeof p!="object")continue;const h=p;h.hbin!=null&&typeof h.hbin!="number"&&u("hbin",h.hbin),h.sbin!=null&&typeof h.sbin!="number"&&u("sbin",h.sbin);const x=h.testValues;if(x&&typeof x=="object")for(let D=0;D<f.length;D++){const b=x[f[D]];b!=null&&typeof b!="number"&&u("testValues",b)}const C=h.testPass;if(C&&typeof C=="object")for(let D=0;D<l.length;D++){const b=C[l[D]];b!=null&&typeof b!="boolean"&&u("testPass",b)}}}const r=[];if(i.hbin&&r.push(`${i.hbin} hard bin${i.hbin===1?"":"s"}`),i.sbin&&r.push(`${i.sbin} soft bin${i.sbin===1?"":"s"}`),i.testValues&&r.push(`${i.testValues} test value${i.testValues===1?"":"s"}`),i.testPass&&r.push(`${i.testPass} pass/fail verdict${i.testPass===1?"":"s"}`),!r.length)return;const g=`buildWaferMap received ${r.join(", ")} of the wrong type (for example ${o}). Bins and test values must be numbers and verdicts true/false \u2014 a bin of "1" is not pass bin 1, so those dies count as fails and yield is wrong, and a test value given as text is not plotted or analysed correctly. Convert them with Number() (and verdicts to booleans) and rebuild.`;return console.warn(`[wafermap] ${g}`),{code:"input-values-not-numbers",message:g,severity:"error"}}function ne(t){const n=(a,s)=>a!==null&&typeof a=="object"&&a[s]!==void 0,e=Array.isArray(t)?{}:t,c=[],i=(a,s,m)=>{a&&c.push(`\`${s}\` (now \`${m}\`)`)};i(n(e,"data"),"data","results"),i(n(e,"die"),"die","dieConfig"),i(n(e,"stack"),"stack","lotStack"),i(n(e.dieConfig,"origin"),"dieConfig.origin","dieConfig.coordinateOrigin"),i(n(e.waferConfig,"flat"),"waferConfig.flat","waferConfig.notch"),i(n(e.reticleConfig,"anchor"),"reticleConfig.anchor","reticleConfig.anchorDie"),i(n(e.lotStack,"aggr"),"lotStack.aggr","lotStack.method"),i(Array.isArray(e.testDefs)&&e.testDefs.some(a=>n(a,"index")&&!n(a,"testNumber")),"TestDef.index","TestDef.testNumber");const o=n(e.lotStack,"results")?e.lotStack.results:[],u=(Array.isArray(t)?[t]:[e.results,...Array.isArray(o)?o:[]]).filter(a=>Array.isArray(a));if(i(u.some(a=>a.slice(0,100).some(s=>n(s,"values"))),"values","testValues"),!c.length)return;const r=c.length>1,g=`buildWaferMap ignored ${c.join(", ")}: ${r?"these names were":"that name was"} removed in an earlier release, so what ${r?"they":"it"} described is not on this map. Rename and rebuild.`;return console.warn(`[wafermap] ${g}`),{code:"input-field-removed",message:g,severity:"error"}}function We(t){if(t.results!==void 0||t.lotStack!==void 0)throw new Error("buildWaferMap: pass `layout`, `results` or `lotStack` \u2014 only one.");const n=t.waferConfig?.diameter,e=t.dieConfig?.width,c=t.dieConfig?.height;if(!(n>0&&e>0&&c>0))throw new Error("buildWaferMap: `layout` needs waferConfig.diameter and dieConfig.width and height, all positive.");const i=j({diameter:n,notch:t.waferConfig?.notch,orientation:0}),o=ye(be(i,{width:e,height:c}),i,{width:e,height:c}).filter(s=>!s.partial),{flipX:u,flipY:r}=se(t.dieConfig,{type:"center"}),{layout:g,...a}=t;return{...a,results:o.map(s=>({x:u?-s.x:s.x,y:r?-s.y:s.y})),waferConfig:{...t.waferConfig,center:{x:0,y:0}},dieConfig:{...t.dieConfig,coordinateOrigin:{type:"center"}}}}function $e(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],ringCount:4,ringCountWarning:void 0,removedFieldWarning:ne(t),nonNumericWarning:te(t),standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,derivedTests: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],...Re(t.ringCount),removedFieldWarning:ne(t),nonNumericWarning:te(t),standardDiameters:t.standardDiameters,testDefs:t.testDefs?.length?t.testDefs:void 0,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,derivedTests:t.derivedTests?.length?t.derivedTests:void 0,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Ee(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Fe(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let c=1/0,i=-1/0,o=1/0,u=-1/0;for(const r of t)r.x<c&&(c=r.x),r.x>i&&(i=r.x),r.y<o&&(o=r.y),r.y>u&&(u=r.y);return{offsetX:Math.round((i+c)/2),offsetY:Math.round((u+o)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Pe(t,n,e,c,i,o){if(o)return{colMidX:(Math.round(o.x)-n)*c,colMidY:(Math.round(o.y)-e)*i,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let u=1/0,r=-1/0,g=1/0,a=-1/0;for(const s of t){const m=Math.round(s.x)-n,f=Math.round(s.y)-e;m<u&&(u=m),m>r&&(r=m),f<g&&(g=f),f>a&&(a=f)}return{colMidX:(u+r)/2*c,colMidY:(g+a)/2*i,anchored:!1}}function Te(t,n,e){let c=1/0,i=-1/0,o=1/0,u=-1/0,r=0,g=0;for(const m of t){const f=Math.round(m.x)-n,l=Math.round(m.y)-e;r+=f,g+=l,f<c&&(c=f),f>i&&(i=f),l<o&&(o=l),l>u&&(u=l)}const a=t.length,s=(m,f,l)=>{const p=(l-f)/2;return p<=0?0:Math.abs(m/a-(f+l)/2)/p};return s(r,c,i)>.11||s(g,o,u)>.11}function se(t,n){let e=t?.xAxisDirection==="left",c=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(c=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:c}}function Ie(t,n){const{method:e,targetBin:c}=t,i=t.results.map(o=>o.filter(R));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const o=new Set((n??[]).filter(s=>!isParametricTest(s)).map(s=>s.testNumber)),u=new Set;for(const s of i)for(const m of s)if(m.testValues)for(const f of Object.keys(m.testValues)){const l=Number(f);o.has(l)||u.add(l)}if(u.size===0)return ee(i,e);const r=[...u],g=r.map(s=>ee(i,e,s)),a=new Map;for(let s=0;s<r.length;s++){const m=r[s];for(const f of g[s]){const l=V(f);a.has(l)||a.set(l,{template:f,testValues:{}});const p=a.get(l),h=f.testValues?.[0];h!==void 0&&(p.testValues[m]=h)}}return[...a.values()].map(({template:s,testValues:m})=>({...s,testValues:Object.keys(m).length>0?m:void 0}))}if(e==="countBin"||e==="percent"){if(c===void 0)return[];const o=Ae(i,c,"hard");if(e==="countBin")return o;const u=i.length;return o.map(r=>({...r,testValues:{0:u>0?(r.testValues?.[0]??0)/u*100:0}}))}if(e==="mode"){const o=new Map;for(const r of i)for(const g of r){const a=V(g);o.has(a)||o.set(a,[]),o.get(a).push(g)}const u=[];for(const[r,g]of o){const a=r.split(","),s=Number(a[0]),m=Number(a[1]),f=g.map(p=>p.hbin).filter(p=>p!==void 0),l=ke(f);l!==null&&u.push({x:s,y:m,hbin:l})}return u}return[]}function ie(t){const n=t.filter(R),e=n.length,c=n.filter(o=>o.edgeExcluded).length,i=n.filter(o=>dieHasTestData(o)||o.hbin!==void 0||o.sbin!==void 0).length;return{filledDies:i,totalDies:e,edgeExcludedDies:c,unpositionedDies:t.length-n.length,ratio:e>0?i/e:0}}function re(t,n,e="exclude"){const c=new Set(n),o=t.filter(l=>!l.partial).filter(l=>l.edgeExcluded).length,u=t.filter(l=>l.partial).length;let r=0,g=0,a=!1;for(const l of t){if(!ge(l))continue;const p=l.hbin??l.sbin;p!==void 0&&(a=!0,c.has(p)?r++:g++)}const s=r+g,m=a&&s>0?r/s*100:null;let f=null;if(e==="denominator-only"){const l=s+o;f=a&&l>0?r/l*100:null}return{passDies:r,failDies:g,edgeExcludedDies:o,partialDies:u,totalDies:s,yieldPercent:m,yieldPercentGross:f}}function oe(t,n,e,c,i,o=0,u=0,r=0,g=0,a=0,s=!1,m=!1){if(!t)return[];const f=t.anchorDie??{x:0,y:0},l=Se(n,{width:t.width,height:t.height,diePitchX:c,diePitchY:i,anchorDie:{x:f.x-o,y:f.y-u},gridOrigin:{x:-r,y:-g}}),p=De(xe(s,m,n.center.x,n.center.y),we(a,n.center.x,n.center.y));return l.filter(h=>{const x=h.width/2,C=h.height/2,D=[[h.x-x,h.y-C],[h.x+x,h.y-C],[h.x+x,h.y+C],[h.x-x,h.y+C]].map(([w,P])=>Me(p,w,P)),b=Z(D.map(w=>w.x)),S=K(D.map(w=>w.x)),W=Z(D.map(w=>w.y)),$=K(D.map(w=>w.y));return e.some(w=>w.physX>=b&&w.physX<S&&w.physY>=W&&w.physY<$)})}function Ne(t,n,e){const i=Math.max(0,n.radius-e)**2;return t.map(o=>{const u=o.physX-n.center.x,r=o.physY-n.center.y;return u*u+r*r>i?{...o,edgeExcluded:!0}:o})}function Ve(t,n,e){const c=t.filter(R),i=t.filter(s=>!R(s)),o=new Map;for(const s of c){let m=o.get(s.x);m||(m=new Map,o.set(s.x,m)),m.set(s.y,(m.get(s.y)??0)+1)}const u=new Set(e);function r(s,m){const f=s.hbin,l=m.hbin;if(f===void 0||l===void 0)return!1;const p=u.has(f),h=u.has(l);return n==="best"?h!==p?h:l<f:h!==p?p:l>f}const g=new Map;for(const s of c){const m=V(s),f=g.get(m);n==="first"&&f||(n==="best"||n==="worst")&&f&&!r(f,s)||g.set(m,s)}return[...Array.from(g.values()).map(s=>{const f=o.get(s.x)?.get(s.y)??1;return f>1?{...s,retestCount:f}:s}),...i]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const c=t.testPass?.[n];if(c!==void 0)return c;if(e!==void 0&&!isParametricTest(e)){const i=t.testValues?.[n];if(i===0||i===1)return i===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,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 ae(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function de(t,n){return t.map(({code:e,message:c})=>({code:e,message:c,severity:e==="non-standard-diameter"||e==="diameter-exceeds-die-extent"?"warning":"error",confidence:n.wafer.confidence}))}const Be="do not fit inside the supplied",_e="Inferred wafer diameter",Ye="The probed dies reach only",Le=.75,Xe=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const je=.02;function fe(t,n){return n.reduce((e,c)=>Math.abs(c-t)<Math.abs(e-t)?c:e)}function Ke(t,n){if(!n.length)return!0;const e=fe(t,n);return Math.abs(e-t)/e<=je}function ze(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=$e(!Array.isArray(t)&&t.layout?We(t):t),{debug:c,...i}=n??{},o=[];if(e.derivedTests){const d=new Set(e.passBins);if(e.lotStackOpts){const y=e.lotStackOpts.results.map(v=>J(v,e.testDefs,e.derivedTests,d));e.lotStackOpts={...e.lotStackOpts,results:y.map(v=>v.results)},e.testDefs=y[0]?.testDefs??e.testDefs,o.push(...y[0]?.warnings??[])}else{const y=J(e.results,e.testDefs,e.derivedTests,d);e.results=y.results,e.testDefs=y.testDefs,o.push(...y.warnings)}}const u=e.lotStackOpts?Ie(e.lotStackOpts,e.testDefs):e.results;for(let d=0;d<Math.min(5,u.length);d++){const y=u[d];if(typeof y.x=="string"||typeof y.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const d of u)if(d.x==null!=(d.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(d.x)}, y=${String(d.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 r=Ve(u,e.retestPolicy,e.passBins),g={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}},a=[];if(e.explicitDies){let d=e.explicitDies;const y=r.filter(R),v=r.filter(N=>!R(N));if(y.length>0){const N=new Map(y.map(A=>[V(A),A]));d=d.map(A=>{const H=N.get(V(A));return H?_(A,H):A})}const I=e.waferOpts?.diameter??300,F=j({diameter:I,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});F.orientation!==0&&(d=U(d,F));const q=oe(e.reticleOpts,F,d,1,1,0,0,0,0,F.orientation),he=i.showReticle??e.reticleOpts!==void 0,L=Q(F,d,{...i,reticles:q,showReticle:he,plotMode:ae(r,i),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),me=v.map((N,A)=>_({id:`unpositioned_${A}`,width:d[0]?.width??1,height:d[0]?.height??1},N)),X=[...d,...me];return{wafer:F,dies:X,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:g,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...de(a,g),...o,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:ie(X),passBins:e.passBins,ringCount:e.ringCount,yield:re(X,e.passBins,e.edgeDieYieldMode),reticles:q,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const s=r.filter(R),m=r.filter(d=>!R(d)),f=s.map(d=>({x:d.x,y:d.y})),l=Ce(f,e.dieOpts,e.waferOpts?.diameter);g.diePitch={confidence:l.confidence,units:l.units};const{pitchX:p,pitchY:h}=l,x=l.units,C=Ee(s,e.dieOpts),D=Oe(f);g.grid={confidence:D.confidence};const{offsetX:b,offsetY:S}=Fe(f,C,D),{flipX:W,flipY:$}=se(e.dieOpts,C),{colMidX:w,colMidY:P,anchored:ce}=Pe(f,b,S,p,h,e.waferOpts?.center);let M=e.waferOpts?.diameter;const B=f.map(d=>({x:(Math.round(d.x)-b)*p-w,y:(Math.round(d.y)-S)*h-P})),E=K(B.map(({x:d,y})=>Math.hypot(Math.abs(d)+p/2,Math.abs(y)+h/2)));if(M===void 0)if(f.length>0)if(x==="mm"){const d=ve(B,{minRadius:E});M=d.diameter,g.wafer={confidence:d.confidence,method:d.method};const y=ze(e.standardDiameters);if(!Ke(M,y)){const v=fe(M,y);a.push({code:"non-standard-diameter",message:`${_e} ${M.toFixed(1)} mm is not a standard wafer size (nearest is ${v} mm). The diameter was derived from the die extent, which assumes the probed grid reaches the wafer edge \u2014 a non-standard result usually means it does not, because the outer dies were never probed. Every die is then placed against a wafer that is too small, which moves dies between rings and changes ring and edge findings. Supply waferConfig.diameter.`})}}else M=E*2,g.wafer={confidence:l.confidence*.8,method:"extent"};else M=x==="mm"?300:30,g.wafer={confidence:0,method:"default"};if(!ce&&f.length>0&&Te(f,b,S)&&(g.wafer.method="inferred-partial",a.push({code:"partial-coverage",message:"Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly."})),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&E>0&&f.length>=Xe){const d=E/(M/2);d<Le&&a.push({code:"diameter-exceeds-die-extent",message:`${Ye} ${(d*100).toFixed(0)}% of the ${M} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`})}if(e.waferOpts?.diameter!==void 0){const d=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(d&&E>M/2+1e-9){const y=B.filter(({x:v,y:I})=>Math.hypot(Math.abs(v)+p/2,Math.abs(I)+h/2)>M/2+1e-9).length;a.push({code:"geometry-conflict",message:`${y} of ${B.length} probed die positions ${Be} ${M} mm wafer at the supplied die pitch of ${p} \xD7 ${h} 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 ${(E*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`})}}const k=j({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let O=s.map(d=>{const y=Math.round(d.x)-b,v=Math.round(d.y)-S,I={id:`${y}_${v}`,x:y,y:v,physX:y*p-w,physY:v*h-P,width:p,height:h,insideWafer:!0,partial:!1};return _(I,d)});(b!==0||S!==0)&&(O=O.map(d=>({...d,x:d.x+b,y:d.y+S,id:`${d.x+b}_${d.y+S}`}))),O=U(O,k),(W||$)&&(O=pe(O,{flipX:W,flipY:$},k.center));const z=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(O=Ne(O,k,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>k.radius&&z.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${k.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const G=oe(e.reticleOpts,k,O,p,h,b,S,w,P,k.orientation,W,$),le=i.showReticle??e.reticleOpts!==void 0,T=Q(k,O,{...i,reticles:G,showReticle:le,plotMode:ae(r,i),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,dataAxisFlip:{x:W,y:$},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ue=m.map((d,y)=>_({id:`unpositioned_${y}`,width:p,height:h},d)),Y=[...O,...ue];return{wafer:k,dies:Y,view:T,reticleConfig:e.reticleOpts,units:x,inference:g,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...de(a,g),...z,...o,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:T.plotMode,metadata:T.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:ie(Y),passBins:e.passBins,ringCount:e.ringCount,yield:re(Y,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:T.aggrMethod,lotSize:T.lotSize}}
@@ -1,9 +1,17 @@
1
1
  /**
2
- * The release that deletes every export deprecated through this helper. Named in
3
- * each notice and each `@deprecated` tag, and held to by tests/deprecations.test.mjs,
4
- * which fails once the changelog or package.json reaches it with them still here.
2
+ * The release that deletes an export deprecated through this helper, unless its
3
+ * deprecation names a later one. Named in each notice and each `@deprecated` tag,
4
+ * and held to by tests/deprecations.test.mjs, which fails once the changelog or
5
+ * package.json reaches an export's removal release with it still here.
5
6
  */
6
7
  export declare const DEPRECATED_REMOVAL_VERSION = "0.31.0";
8
+ /**
9
+ * @internal The release that removes each deprecated name. A name deprecated
10
+ * after the default removal release was scheduled gets a later one — a removal
11
+ * must follow a release in which the name shipped deprecated, never coincide
12
+ * with it.
13
+ */
14
+ export declare const DEPRECATED_REMOVALS: Map<string, string>;
7
15
  /**
8
16
  * @internal Every name deprecated through this module, and whether it is a function
9
17
  * (which logs a notice) or a value (which cannot). tests/deprecations.test.mjs pins
@@ -22,10 +30,10 @@ export declare function noticeOnce(key: string, message: string): void;
22
30
  * The wrapper has exactly `fn`'s type — generics and overloads included — so a
23
31
  * deprecated export's signature does not change until it is removed.
24
32
  */
25
- export declare function deprecated<F extends (...args: never[]) => unknown>(fn: F, name: string, advice: string): F;
33
+ export declare function deprecated<F extends (...args: never[]) => unknown>(fn: F, name: string, advice: string, removal?: string): F;
26
34
  /**
27
35
  * @internal Register a deprecated constant. A value cannot log on use, so its
28
36
  * `@deprecated` tag and the changelog are the only announcement.
29
37
  */
30
- export declare function deprecatedValue<T>(value: T, name: string): T;
38
+ export declare function deprecatedValue<T>(value: T, name: string, removal?: string): T;
31
39
  //# sourceMappingURL=deprecate.d.ts.map
@@ -1 +1 @@
1
- export const DEPRECATED_REMOVAL_VERSION="0.31.0",DEPRECATED_EXPORTS=new Map;const n=new Set;export function noticeOnce(t,e){n.has(t)||(n.add(t),console.warn(`[wafermap] ${e}`))}export function deprecated(t,e,r){DEPRECATED_EXPORTS.set(e,"function");const o=t;return((...c)=>(noticeOnce(e,`${e} is deprecated and will be removed in ${DEPRECATED_REMOVAL_VERSION}. ${r}`),o(...c)))}export function deprecatedValue(t,e){return DEPRECATED_EXPORTS.set(e,"value"),t}
1
+ export const DEPRECATED_REMOVAL_VERSION="0.31.0",DEPRECATED_REMOVALS=new Map,DEPRECATED_EXPORTS=new Map;const r=new Set;export function noticeOnce(t,e){r.has(t)||(r.add(t),console.warn(`[wafermap] ${e}`))}export function deprecated(t,e,n,o=DEPRECATED_REMOVAL_VERSION){DEPRECATED_EXPORTS.set(e,"function"),DEPRECATED_REMOVALS.set(e,o);const c=t;return((...E)=>(noticeOnce(e,`${e} is deprecated and will be removed in ${o}. ${n}`),c(...E)))}export function deprecatedValue(t,e,n=DEPRECATED_REMOVAL_VERSION){return DEPRECATED_EXPORTS.set(e,"value"),DEPRECATED_REMOVALS.set(e,n),t}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Build-time application of derived tests.
3
+ *
4
+ * Runs over raw `DieResult` records **before** `collapseLotStack` and before
5
+ * retest resolution, and that ordering is load-bearing:
6
+ *
7
+ * - **Before stacking.** `abs(t[1020] - t[1010])` computed from the two raw
8
+ * values on one probe record is a real measurement of one die. The same
9
+ * expression computed from two independently aggregated stacks is a difference
10
+ * of two averages over possibly different die populations — a number with no
11
+ * physical meaning that would nonetheless plot happily.
12
+ * - **Before retest collapse.** Every accessor then reads the same probe record,
13
+ * so a derived value can never mix a value from one touchdown with a verdict
14
+ * from another. `retestPolicy` afterwards selects among records that already
15
+ * carry their own derived value.
16
+ *
17
+ * A derived test is an ordinary test from here on: it lands in `testValues` (or
18
+ * `testPass`, if its expression is boolean) and its def joins `testDefs`, so
19
+ * every downstream surface — value plot modes, the colorbar, tooltips,
20
+ * `analyzeWaferMap`, Insights, the report, gallery reconciliation — picks it up
21
+ * with no further change.
22
+ */
23
+ import type { DieResult, TestDef, WaferWarning } from '../buildWaferMap.js';
24
+ /**
25
+ * A test whose values are computed from other tests on the same die, rather than
26
+ * measured by the tester.
27
+ *
28
+ * It is a `TestDef` with an expression, not a parallel concept: `unit`,
29
+ * `limitLow`/`limitHigh` and `logScale` already mean exactly the right thing for
30
+ * a derived quantity, and `result.testDefs` stays one homogeneous array.
31
+ *
32
+ * ```ts
33
+ * { testNumber: 900001, name: 'Leakage Shift', unit: 'uA',
34
+ * expression: 'abs(t[1020] - t[1010])', limitHigh: 5 }
35
+ *
36
+ * { testNumber: 900002, name: 'Sweep All Pass', testType: 'F',
37
+ * expression: 'all(testPass[1010..1025])' }
38
+ * ```
39
+ */
40
+ export interface DerivedTestDef extends TestDef {
41
+ /**
42
+ * Expression over other tests on the same die.
43
+ *
44
+ * Reads: `t[n]` (measured value, parametric tests only), `testPass[n]` (the
45
+ * tester's recorded verdict), `specPass[n]` (spec-limit judgement, needs
46
+ * limits), `diePass()` (the die's bin verdict). A range — `t[1010..1015]` —
47
+ * produces a set that must be reduced: `mean`, `sum`, `min`, `max`, `all`,
48
+ * `any`, `none`, `countTrue`, `countFalse`, `countKnown`.
49
+ *
50
+ * Operators `+ - * / % ^`, comparisons, `and`/`or`/`not`, and
51
+ * `if(cond, a, b)`. Functions `abs sqrt ln log10 exp floor ceil round sign
52
+ * pow min max`.
53
+ *
54
+ * Evaluated with no JavaScript engine of any kind: it is parsed to a typed
55
+ * tree at build time and walked per die. There is no `eval`, no `Function`,
56
+ * no member access and no way to name a host object.
57
+ */
58
+ expression: string;
59
+ /** Named constants usable in `expression`. The only resolvable identifiers
60
+ * besides function names. */
61
+ constants?: Record<string, number>;
62
+ /**
63
+ * Marks this def as derived. Set by `buildWaferMap`, never by the caller.
64
+ *
65
+ * It is a display flag: every surface that names the test reads it, through
66
+ * `renderer/testLabel.ts`, to show that the value was not measured. It does
67
+ * NOT exclude derived tests from anything — a derived test is an ordinary test
68
+ * from the build onwards, and appears in `functionalYield`, the per-test
69
+ * statistics, capability, findings and Insights exactly like a measured one.
70
+ */
71
+ readonly derived?: true;
72
+ }
73
+ export interface ApplyResult {
74
+ results: DieResult[];
75
+ testDefs: TestDef[] | undefined;
76
+ warnings: WaferWarning[];
77
+ }
78
+ /**
79
+ * Apply `derivedTests` to `results`, returning new records that carry the
80
+ * derived values and a `testDefs` array extended with the derived defs.
81
+ *
82
+ * Input records are not mutated — the caller's array is its own.
83
+ */
84
+ export declare function applyDerivedTests(results: DieResult[], testDefs: TestDef[] | undefined, derivedTests: DerivedTestDef[] | undefined, passBins: ReadonlySet<number>): ApplyResult;
85
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1 @@
1
+ import{diePassStatus as P}from"../../core/dies.js";import{getTestPassStatus as D,isParametricTest as S}from"../buildWaferMap.js";import{classifySpec as V}from"../spec.js";import{parseExpression as k}from"./parser.js";import{evaluate as O}from"./evaluate.js";function p(m){return{code:"derived-test-invalid",message:m,severity:"warning"}}function M(m,l){const w=[],o=n=>{switch(n.k){case"acc":{if(n.kind!=="t")return;const i=l.get(n.test)?.unit;return i?new Set([i]):void 0}case"unary":return n.op==="-"?o(n.arg):void 0;case"binary":{const i=o(n.left),a=o(n.right);if(n.op==="+"||n.op==="-"){if(i&&a){const d=new Set([...i,...a]);return d.size>1&&w.push(`${[...d].join(" and ")} are combined by "${n.op}"`),d}return i??a}return}case"call":{if(n.name==="min"||n.name==="max"||n.name==="mean"||n.name==="sum"){const i=n.args[0];if(i?.k==="vec"&&i.kind==="t"){const a=new Set(i.tests.map(d=>l.get(d)?.unit).filter(d=>!!d));return a.size>1&&w.push(`${[...a].join(" and ")} are combined by "${n.name}"`),a}}return}default:return}};return o(m),w}function T(m,l,w){const o=[],n=new Map((l??[]).map(e=>[e.testNumber,e])),i=e=>`${JSON.stringify(e.name??"")} (test ${e.testNumber})`,a=[],d=new Set;for(const e of m){const t=i(e);if(!Number.isInteger(e.testNumber)){o.push(p(`Derived test ${t} was dropped: \`testNumber\` must be an integer.`));continue}if(w.has(e.testNumber)||n.has(e.testNumber)){o.push(p(`Derived test ${t} was dropped: test number ${e.testNumber} already exists in the data. Measured values are never replaced by a derived test \u2014 give it an unused number.`));continue}if(d.has(e.testNumber)){o.push(p(`Derived test ${t} was dropped: test number ${e.testNumber} is used by an earlier derived test.`));continue}if(typeof e.expression!="string"||e.expression.trim()===""){o.push(p(`Derived test ${t} was dropped: \`expression\` is empty.`));continue}d.add(e.testNumber),a.push(e)}const y=new Map(n);for(const e of a){const{expression:t,constants:u,...c}=e;y.set(e.testNumber,{...c,testType:e.testType??"P"})}const s=new Map,r=new Set;for(const e of a){const t=i(e),u={testDefs:y,constants:e.constants??{}},c=k(e.expression,u);if(!c.ok){o.push(p(`Derived test ${t} was dropped: ${c.message}`)),r.add(e.testNumber);continue}const f=c.type==="boolean",g=!S(e);if(f!==g){o.push(p(f?`Derived test ${t} was dropped: its expression produces a true/false verdict, so it must declare \`testType: 'F'\`.`:`Derived test ${t} was dropped: it declares \`testType: 'F'\` but its expression produces a number. Functional tests carry a verdict, not a measurement.`)),r.add(e.testNumber);continue}if(f&&(e.limitLow!==void 0||e.limitHigh!==void 0)&&o.push(p(`Derived test ${t}: spec limits are ignored on a true/false derived test.`)),e.testNumber===void 0)continue;const N=e.testNumber,x=[...new Set(c.reads.map(h=>h.test))].filter(h=>h!==N&&d.has(h));if(c.reads.some(h=>h.test===N)){o.push(p(`Derived test ${t} was dropped: its expression reads itself.`)),r.add(N);continue}for(const h of M(c.ast,y))o.push(p(`Derived test ${t} combines different units \u2014 ${h}. The result is computed as written; set \`unit\` on the derived test to say what it is.`));s.set(N,{raw:e,compiled:{def:{...e,testType:f?"F":"P",derived:!0},ast:c.ast,boolean:f},deps:x})}const b=[],v=new Map,$=(e,t)=>{if(v.get(e)==="done")return!r.has(e);if(v.get(e)==="visiting"){const f=[...t.slice(t.indexOf(e)),e].join(" \u2192 ");for(const g of t.slice(t.indexOf(e)))r.has(g)||(r.add(g),o.push(p(`Derived test ${i(s.get(g).raw)} was dropped: its expression depends on itself through ${f}.`)));return!1}const u=s.get(e);if(u===void 0)return!1;v.set(e,"visiting");let c=!0;for(const f of u.deps)if(!$(f,[...t,e])){if(!r.has(e)){r.add(e);const g=s.get(f)?.raw.name??`test ${f}`;o.push(p(`Derived test ${i(u.raw)} was dropped: it reads ${JSON.stringify(g)} (test ${f}), which was itself dropped.`))}c=!1;break}return v.set(e,"done"),!c||r.has(e)?!1:(b.push(u.compiled),!0)};for(const e of a)$(e.testNumber,[]);return{compiled:b,warnings:o}}export function applyDerivedTests(m,l,w,o){if(w===void 0||w.length===0)return{results:m,testDefs:l,warnings:[]};const n=new Set;for(const s of m){for(const r of Object.keys(s.testValues??{}))n.add(Number(r));for(const r of Object.keys(s.testPass??{}))n.add(Number(r))}const{compiled:i,warnings:a}=T(w,l,n);if(i.length===0)return{results:m,testDefs:l,warnings:a};const d=new Map((l??[]).map(s=>[s.testNumber,s]));for(const s of i)d.set(s.def.testNumber,s.def);return{results:m.map(s=>{let r,b;const v=t=>r?.[t]??s.testValues?.[t],e={value:v,testPass:t=>b?.[t]??D({testValues:r??s.testValues,testPass:b??s.testPass},t,d.get(t)),specPass:t=>{const u=V(v(t),d.get(t));return u===null?void 0:u==="pass"},diePass:()=>P(s,o)};for(const t of i){const u=O(t.ast,e);u!==void 0&&(t.boolean?(b??={...s.testPass},b[t.def.testNumber]=u):(r??={...s.testValues},r[t.def.testNumber]=u))}return r===void 0&&b===void 0?s:{...s,...r!==void 0?{testValues:r}:{},...b!==void 0?{testPass:b}:{}}}),testDefs:[...l??[],...i.map(s=>s.def)],warnings:a}}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Evaluator for a parsed derived-test expression.
3
+ *
4
+ * Two rules govern every operation here, and they are deliberately different
5
+ * from each other:
6
+ *
7
+ * 1. **Scalars propagate "unknown" strictly.** If any operand is `undefined` —
8
+ * a test not run on this die, a verdict never recorded, a value outside its
9
+ * limits' knowability — the whole expression is `undefined`, which becomes an
10
+ * absent entry and renders as no-data grey. Never `?? 0`, never `?? false`.
11
+ * A missing measurement is not a zero and a missing verdict is not a fail.
12
+ *
13
+ * 2. **Reducers skip unknowns and count only what is known.** This is the one
14
+ * intentional break from (1), and it is why `countKnown` exists: a die binned
15
+ * out early legitimately has no verdict for the tests that never ran, and
16
+ * under strict propagation `all(testPass[1010..1025])` would grey out most of
17
+ * a bad wafer instead of saying what it was asked. A reducer over an entirely
18
+ * unknown vector still returns `undefined` — there is nothing to reduce.
19
+ *
20
+ * A non-finite arithmetic result (`0/0`, `ln(-1)`, overflow) is `undefined` too,
21
+ * for the same reason: `NaN` must reach the map as no-data, not as a value that
22
+ * drags a mean toward zero or classifies as in-spec.
23
+ */
24
+ import type { ExprNode } from './parser.js';
25
+ /** Per-die data the evaluator is allowed to reach. Deliberately a closed set of
26
+ * functions rather than the die object, so the AST has no path to anything
27
+ * else on the die or in the host. */
28
+ export interface EvalContext {
29
+ /** Measured value of a parametric test, or `undefined` if not recorded. */
30
+ value(test: number): number | undefined;
31
+ /** Tester's recorded verdict, via `getTestPassStatus`. */
32
+ testPass(test: number): boolean | undefined;
33
+ /** Spec-limit judgement, via `classifySpec`. */
34
+ specPass(test: number): boolean | undefined;
35
+ /** The die's bin-based pass verdict, via `diePassStatus`. */
36
+ diePass(): boolean | undefined;
37
+ }
38
+ type Scalar = number | boolean | undefined;
39
+ /**
40
+ * Evaluate `node` for one die. Returns `undefined` when no answer is knowable.
41
+ *
42
+ * The AST is a plain data tree built by `parseExpression` and already statically
43
+ * type-checked, so this walk performs no validation and cannot reach any host
44
+ * value not exposed through `ctx`.
45
+ */
46
+ export declare function evaluate(node: ExprNode, ctx: EvalContext): Scalar;
47
+ export {};
48
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1 @@
1
+ import{minOf as c,maxOf as f}from"../../core/utils.js";function s(e){return Number.isFinite(e)?e:void 0}function l(e,t){const n=r=>e.kind==="t"?t.value(r):e.kind==="testPass"?t.testPass(r):t.specPass(r);return e.tests.map(n)}function o(e,t){const n=t.filter(r=>r!==void 0);if(e==="countKnown")return n.length;if(n.length!==0)switch(e){case"all":return n.every(Boolean);case"any":return n.some(Boolean);case"none":return!n.some(Boolean);case"countTrue":return n.filter(Boolean).length;case"countFalse":return n.filter(r=>!r).length;case"min":return c(n);case"max":return f(n);case"sum":return s(n.reduce((r,a)=>r+a,0));case"mean":return s(n.reduce((r,a)=>r+a,0)/n.length);default:return}}const d={abs:e=>Math.abs(e[0]),sqrt:e=>Math.sqrt(e[0]),ln:e=>Math.log(e[0]),log10:e=>Math.log10(e[0]),exp:e=>Math.exp(e[0]),floor:e=>Math.floor(e[0]),ceil:e=>Math.ceil(e[0]),round:e=>Math.round(e[0]),sign:e=>Math.sign(e[0]),pow:e=>Math.pow(e[0],e[1]),min:e=>c(e),max:e=>f(e)};export function evaluate(e,t){switch(e.k){case"num":return e.value;case"acc":return e.kind==="t"?t.value(e.test):e.kind==="testPass"?t.testPass(e.test):t.specPass(e.test);case"diePass":return t.diePass();case"vec":return;case"unary":{const n=evaluate(e.arg,t);return n===void 0?void 0:e.op==="-"?s(-n):!n}case"binary":{const n=evaluate(e.left,t);if(n===void 0)return;const r=evaluate(e.right,t);if(r===void 0)return;switch(e.op){case"+":return s(n+r);case"-":return s(n-r);case"*":return s(n*r);case"/":return s(n/r);case"%":return s(n%r);case"^":return s(Math.pow(n,r));case"<":return n<r;case"<=":return n<=r;case">":return n>r;case">=":return n>=r;case"==":return n===r;case"!=":return n!==r;case"and":return n&&r;case"or":return n||r}return}case"call":{if(e.name==="if"){const u=evaluate(e.args[0],t);return u===void 0?void 0:evaluate(e.args[u?1:2],t)}const n=e.args[0];if(e.args.length===1&&n!==void 0&&n.k==="vec")return o(e.name,l(n,t));const r=d[e.name];if(r===void 0)return;const a=[];for(const u of e.args){const i=evaluate(u,t);if(i===void 0)return;a.push(i)}return s(r(a))}}}