@wafertools/wafermap 0.30.2 → 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 (82) hide show
  1. package/AGENTS.md +16 -5
  2. package/CHANGELOG.md +384 -0
  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/dieList.js +2 -2
  23. package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
  24. package/dist/packages/canvas-adapter/drilldown.js +1 -0
  25. package/dist/packages/canvas-adapter/icons.js +1 -1
  26. package/dist/packages/canvas-adapter/insightsTab.d.ts +55 -3
  27. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  28. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  29. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  30. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  31. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  32. package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
  33. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  34. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  35. package/dist/packages/canvas-adapter/userGuideHtml.js +126 -3
  36. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  37. package/dist/packages/canvas-adapter/version.js +1 -1
  38. package/dist/packages/canvas-adapter/warnings.js +1 -1
  39. package/dist/packages/renderer/axisTicks.d.ts +32 -0
  40. package/dist/packages/renderer/axisTicks.js +1 -0
  41. package/dist/packages/renderer/buildView.d.ts +11 -37
  42. package/dist/packages/renderer/buildView.js +1 -1
  43. package/dist/packages/renderer/buildWaferMap.d.ts +58 -1
  44. package/dist/packages/renderer/buildWaferMap.js +1 -1
  45. package/dist/packages/renderer/deprecate.d.ts +13 -5
  46. package/dist/packages/renderer/deprecate.js +1 -1
  47. package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
  48. package/dist/packages/renderer/derivedTests/apply.js +1 -0
  49. package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
  50. package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
  51. package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
  52. package/dist/packages/renderer/derivedTests/parser.js +2 -0
  53. package/dist/packages/renderer/fmt.d.ts +24 -2
  54. package/dist/packages/renderer/fmt.js +1 -1
  55. package/dist/packages/renderer/index.d.ts +1 -0
  56. package/dist/packages/renderer/index.js +1 -1
  57. package/dist/packages/renderer/spec.d.ts +49 -0
  58. package/dist/packages/renderer/spec.js +1 -0
  59. package/dist/packages/renderer/testLabel.d.ts +154 -0
  60. package/dist/packages/renderer/testLabel.js +1 -0
  61. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  62. package/dist/packages/stats/capability.d.ts +4 -0
  63. package/dist/packages/stats/capability.js +1 -1
  64. package/dist/packages/stats/correlation.d.ts +3 -0
  65. package/dist/packages/stats/correlation.js +1 -1
  66. package/dist/packages/stats/deprecated.d.ts +3 -0
  67. package/dist/packages/stats/deprecated.js +1 -1
  68. package/dist/packages/stats/index.d.ts +2 -2
  69. package/dist/packages/stats/index.js +1 -1
  70. package/dist/packages/stats/mergeTestDefs.js +1 -1
  71. package/dist/packages/stats/renderFindingsReport.js +8 -28
  72. package/dist/packages/stats/renderSummaryReport.js +21 -30
  73. package/dist/packages/stats/reportHtml.d.ts +27 -0
  74. package/dist/packages/stats/reportHtml.js +35 -14
  75. package/dist/packages/stats/sweep.d.ts +178 -0
  76. package/dist/packages/stats/sweep.js +1 -0
  77. package/dist/packages/stats/sweepXFromName.d.ts +27 -0
  78. package/dist/packages/stats/sweepXFromName.js +1 -0
  79. package/dist/packages/stats/testPassRate.js +1 -1
  80. package/dist/packages/stats/types.d.ts +28 -1
  81. package/llms.txt +3 -2
  82. package/package.json +1 -1
@@ -1 +1 @@
1
- import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as he,getDieKey as V,hasPosition as A}from"../core/dies.js";import{applyOrientation as U,transformDies as me,clipDiesToWafer as ge}from"../core/transforms.js";import{generateDies as pe}from"../core/dies.js";import{affineRotation as ye,affineMirror as be,affineCompose as we,affinePoint as xe}from"../core/transforms.js";import{inferWaferFromXY as De}from"../core/inference/wafer.js";import{resolveGridPitch as Me}from"../core/inference/pitch.js";import{assignGridIndices as Ce}from"../core/inference/grid.js";import{generateReticleGrid as ve}from"../core/reticle.js";import{buildView as J}from"./buildView.js";import{maxOf as K,minOf as Q,modeOf as Oe}from"../core/utils.js";import{aggregateValues as Z,aggregateBinCounts as Se}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function ke(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 ee(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 te(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 Ae(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=ge(pe(s,{width:e,height:l}),s,{width:e,height:l}).filter(i=>!i.partial),{flipX:h,flipY:o}=ne(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 Re(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:te(t),nonNumericWarning:ee(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],...ke(t.ringCount),removedFieldWarning:te(t),nonNumericWarning:ee(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 $e(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function We(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 Ee(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 Fe(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 ne(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 Pe(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 Z(s,e);const o=[...h],g=o.map(i=>Z(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=V(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=Se(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=V(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=Oe(u);d!==null&&h.push({x:i,y:f,hbin:d})}return h}return[]}function se(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 ie(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(!he(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 oe(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=ve(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=we(be(i,f,n.center.x,n.center.y),ye(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])=>xe(p,w,B)),b=Q(y.map(w=>w.x)),R=K(y.map(w=>w.x)),$=Q(y.map(w=>w.y)),F=K(y.map(w=>w.y));return e.some(w=>w.physX>=b&&w.physX<R&&w.physY>=$&&w.physY<F)})}function Ie(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 Ne(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=V(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 re(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function ae(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 Ve="do not fit inside the supplied",Te="Inferred wafer diameter",_e="The probed dies reach only",Be=.75,Ye=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const Le=.02;function de(t,n){return n.reduce((e,l)=>Math.abs(l-t)<Math.abs(e-t)?l:e)}function Xe(t,n){if(!n.length)return!0;const e=de(t,n);return Math.abs(e-t)/e<=Le}function je(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=Re(!Array.isArray(t)&&t.layout?Ae(t):t),{debug:l,...s}=n??{},r=e.lotStackOpts?Pe(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=Ne(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(N=>!A(N));if(x.length>0){const N=new Map(x.map(k=>[V(k),k]));c=c.map(k=>{const H=N.get(V(k));return H?_(k,H):k})}const I=e.waferOpts?.diameter??300,E=j({diameter:I,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});E.orientation!==0&&(c=U(c,E));const q=oe(e.reticleOpts,E,c,1,1,0,0,0,0,E.orientation),le=s.showReticle??e.reticleOpts!==void 0,L=J(E,c,{...s,reticles:q,showReticle:le,plotMode:re(h,s),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ue=S.map((N,k)=>_({id:`unpositioned_${k}`,width:c[0]?.width??1,height:c[0]?.height??1},N)),X=[...c,...ue];return{wafer:E,dies:X,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:o,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...ae(g,o),...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:se(X),passBins:e.passBins,ringCount:e.ringCount,yield:ie(X,e.passBins,e.edgeDieYieldMode),reticles:q,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=Me(f,e.dieOpts,e.waferOpts?.diameter);o.diePitch={confidence:u.confidence,units:u.units};const{pitchX:d,pitchY:p}=u,m=u.units,D=$e(a,e.dieOpts),C=Ce(f);o.grid={confidence:C.confidence};const{offsetX:y,offsetY:b}=We(f,D,C),{flipX:R,flipY:$}=ne(e.dieOpts,D),{colMidX:F,colMidY:w,anchored:B}=Ee(f,y,b,d,p,e.waferOpts?.center);let M=e.waferOpts?.diameter;const T=f.map(c=>({x:(Math.round(c.x)-y)*d-F,y:(Math.round(c.y)-b)*p-w})),W=K(T.map(({x:c,y:x})=>Math.hypot(Math.abs(c)+d/2,Math.abs(x)+p/2)));if(M===void 0)if(f.length>0)if(m==="mm"){const c=De(T,{minRadius:W});M=c.diameter,o.wafer={confidence:c.confidence,method:c.method};const x=je(e.standardDiameters);if(!Xe(M,x)){const S=de(M,x);g.push({code:"non-standard-diameter",message:`${Te} ${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&&Fe(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>=Ye){const c=W/(M/2);c<Be&&g.push({code:"diameter-exceeds-die-extent",message:`${_e} ${(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=T.filter(({x:S,y:I})=>Math.hypot(Math.abs(S)+d/2,Math.abs(I)+p/2)>M/2+1e-9).length;g.push({code:"geometry-conflict",message:`${x} of ${T.length} probed die positions ${Ve} ${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,I={id:`${x}_${S}`,x,y:S,physX:x*d-F,physY:S*p-w,width:d,height:p,insideWafer:!0,partial:!1};return _(I,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=U(v,O),(R||$)&&(v=me(v,{flipX:R,flipY:$},O.center));const z=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(v=Ie(v,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&z.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${O.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const G=oe(e.reticleOpts,O,v,d,p,y,b,F,w,O.orientation,R,$),fe=s.showReticle??e.reticleOpts!==void 0,P=J(O,v,{...s,reticles:G,showReticle:fe,plotMode:re(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}),ce=i.map((c,x)=>_({id:`unpositioned_${x}`,width:d,height:p},c)),Y=[...v,...ce];return{wafer:O,dies:Y,view:P,reticleConfig:e.reticleOpts,units:m,inference:o,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...ae(g,o),...z,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:P.plotMode,metadata:P.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:se(Y),passBins:e.passBins,ringCount:e.ringCount,yield:ie(Y,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:P.aggrMethod,lotSize:P.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))}}}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Expression parser for derived tests.
3
+ *
4
+ * Deliberately hand-written and dependency-free. A general-purpose expression
5
+ * library (mathjs, expr-eval) would be the only security boundary between a
6
+ * shared `.json` template and the host application, and would have to be trusted
7
+ * to contain no path back to `Function`/`eval`. This grammar is closed: the only
8
+ * identifiers that resolve are the caller's own declared constants, the only data
9
+ * reachable is the die being evaluated, and there is no member access, no
10
+ * assignment, no indexing by anything but an integer literal, and no way to name
11
+ * a host object. Nothing in the AST can express a call to something not in
12
+ * `FUNCTIONS` below.
13
+ *
14
+ * Grammar (recursive descent, standard precedence):
15
+ *
16
+ * ```
17
+ * expr := or
18
+ * or := and ('or' and)*
19
+ * and := not ('and' not)*
20
+ * not := 'not' not | compare
21
+ * compare := add (('<' | '<=' | '>' | '>=' | '==' | '!=') add)?
22
+ * add := mul (('+' | '-') mul)*
23
+ * mul := pow (('*' | '/' | '%') pow)*
24
+ * pow := unary ('^' pow)? // right-associative
25
+ * unary := '-' unary | primary
26
+ * primary := NUMBER | IDENT | call | accessor | '(' expr ')'
27
+ * accessor := ('t' | 'testPass' | 'specPass') '[' INT ('..' INT)? ']'
28
+ * call := IDENT '(' (expr (',' expr)*)? ')'
29
+ * ```
30
+ *
31
+ * The one non-scalar path: a range accessor (`t[1010..1015]`) produces a VECTOR,
32
+ * and a vector may ONLY be consumed by a reducer. There is no vector arithmetic,
33
+ * no elementwise operator and no vector-valued constant — which is what stops
34
+ * this grammar drifting into an array language.
35
+ */
36
+ import type { TestDef } from '../buildWaferMap.js';
37
+ /** Static type of an expression node. Checked at parse time, so a type error is
38
+ * a build warning with a position rather than a runtime surprise on one die. */
39
+ export type ExprType = 'number' | 'boolean' | 'numberVec' | 'boolVec';
40
+ /** Which per-die quantity an accessor reads. Mirrors `passFailDisplay`'s
41
+ * vocabulary exactly: `'test'` is the tester's recorded verdict, `'spec'` is the
42
+ * spec-limit judgement. `t` is the measured value itself. */
43
+ export type AccessorKind = 't' | 'testPass' | 'specPass';
44
+ export type ExprNode = {
45
+ k: 'num';
46
+ value: number;
47
+ } | {
48
+ k: 'acc';
49
+ kind: AccessorKind;
50
+ test: number;
51
+ } | {
52
+ k: 'vec';
53
+ kind: AccessorKind;
54
+ tests: number[];
55
+ } | {
56
+ k: 'diePass';
57
+ } | {
58
+ k: 'unary';
59
+ op: '-' | 'not';
60
+ arg: ExprNode;
61
+ } | {
62
+ k: 'binary';
63
+ op: BinaryOp;
64
+ left: ExprNode;
65
+ right: ExprNode;
66
+ } | {
67
+ k: 'call';
68
+ name: string;
69
+ args: ExprNode[];
70
+ };
71
+ export type BinaryOp = '+' | '-' | '*' | '/' | '%' | '^' | '<' | '<=' | '>' | '>=' | '==' | '!=' | 'and' | 'or';
72
+ export interface ParseOk {
73
+ ok: true;
74
+ ast: ExprNode;
75
+ type: ExprType;
76
+ /** Every test number the expression reads, for dependency and validity checks. */
77
+ reads: {
78
+ kind: AccessorKind;
79
+ test: number;
80
+ }[];
81
+ /** True when the expression consults the die's bin-based pass verdict. */
82
+ usesDiePass: boolean;
83
+ }
84
+ export interface ParseErr {
85
+ ok: false;
86
+ /** Human-readable, already naming the position. */
87
+ message: string;
88
+ /** 0-based character offset in the source expression. */
89
+ position: number;
90
+ }
91
+ export type ParseResult = ParseOk | ParseErr;
92
+ export interface ParseContext {
93
+ /** Tests the expression may reference — the *source* defs only. A derived test
94
+ * referencing another derived test is rejected: chaining needs a dependency
95
+ * sort and a cycle check, which is a later feature, not a first one. */
96
+ testDefs: Map<number, TestDef>;
97
+ /** Caller-declared named constants. These are the ONLY resolvable identifiers
98
+ * besides function names, and they are substituted at parse time. */
99
+ constants: Record<string, number>;
100
+ }
101
+ /** Argument spec for a whitelisted function. `vec` accepts either vector type
102
+ * unless narrowed by `vecOf`. */
103
+ interface FnSpec {
104
+ /** Fixed scalar arity, when the function takes numbers. */
105
+ arity?: number;
106
+ /** Variadic scalar form, e.g. `min(a, b, c)`. */
107
+ variadic?: boolean;
108
+ /** Reducer form: takes exactly one vector of this type. */
109
+ vecOf?: 'numberVec' | 'boolVec' | 'any';
110
+ /** Result type. */
111
+ returns: ExprType;
112
+ }
113
+ /**
114
+ * The complete set of callable names. Scalar maths plus the vector reducers.
115
+ *
116
+ * `min`/`max` are the only overloaded names: `min(a, b)` over scalars and
117
+ * `min(t[1010..1015])` over a vector. Both mean the same thing, so one name is
118
+ * right; the parser picks the form from the argument types.
119
+ */
120
+ export declare const FUNCTIONS: Record<string, FnSpec[]>;
121
+ /**
122
+ * Parse and statically type-check one derived-test expression.
123
+ *
124
+ * Never throws: a malformed expression comes back as `{ ok: false }` with a
125
+ * message and a character position, which `buildWaferMap` turns into a
126
+ * `derived-test-invalid` warning. The derived test is then dropped rather than
127
+ * evaluated — a half-working expression would silently plot wrong numbers.
128
+ */
129
+ export declare function parseExpression(source: string, ctx: ParseContext): ParseResult;
130
+ export type TestReferenceResult = {
131
+ ok: true;
132
+ tests: number[];
133
+ range: boolean;
134
+ } | ParseErr;
135
+ /**
136
+ * Parse a standalone test reference — `"1020"` or `"1010..1030"` — with exactly
137
+ * the lexer and range rule an expression's `t[…]` uses. This is how a sweep's
138
+ * `tests` accepts ranges: the syntax is shared, never re-implemented, so the
139
+ * same text can never mean different tests in the two places.
140
+ *
141
+ * Never throws.
142
+ */
143
+ export declare function parseTestReference(source: string, testDefs: Map<number, TestDef>): TestReferenceResult;
144
+ /** Bound on source length. A shared template is untrusted input; an expression
145
+ * longer than this is a mistake or an attack, not a measurement. */
146
+ export declare const MAX_EXPRESSION_LENGTH = 2000;
147
+ export {};
148
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{isParametricTest as c}from"../buildWaferMap.js";import{hasSpecLimits as l}from"../spec.js";export const FUNCTIONS={abs:[{arity:1,returns:"number"}],sqrt:[{arity:1,returns:"number"}],ln:[{arity:1,returns:"number"}],log10:[{arity:1,returns:"number"}],exp:[{arity:1,returns:"number"}],floor:[{arity:1,returns:"number"}],ceil:[{arity:1,returns:"number"}],round:[{arity:1,returns:"number"}],sign:[{arity:1,returns:"number"}],pow:[{arity:2,returns:"number"}],min:[{vecOf:"numberVec",returns:"number"},{variadic:!0,returns:"number"}],max:[{vecOf:"numberVec",returns:"number"},{variadic:!0,returns:"number"}],if:[{arity:3,returns:"number"}],all:[{vecOf:"boolVec",returns:"boolean"}],any:[{vecOf:"boolVec",returns:"boolean"}],none:[{vecOf:"boolVec",returns:"boolean"}],countTrue:[{vecOf:"boolVec",returns:"number"}],countFalse:[{vecOf:"boolVec",returns:"number"}],mean:[{vecOf:"numberVec",returns:"number"}],sum:[{vecOf:"numberVec",returns:"number"}],countKnown:[{vecOf:"any",returns:"number"}]};const d=["t","testPass","specPass"],m=["<=",">=","==","!=","..","+","-","*","/","%","^","<",">"],b=["(",")","[","]",","];class o extends Error{position;constructor(t,e){super(t),this.position=e}}function h(r){const t=[];let e=0;for(;e<r.length;){const n=r[e];if(n===" "||n===" "||n===`
2
+ `||n==="\r"){e++;continue}if(n>="0"&&n<="9"){const s=e;for(;e<r.length&&r[e]>="0"&&r[e]<="9";)e++;if(r[e]==="."&&r[e+1]!==".")for(e++;e<r.length&&r[e]>="0"&&r[e]<="9";)e++;if(r[e]==="e"||r[e]==="E"){const a=e;if(e++,(r[e]==="+"||r[e]==="-")&&e++,r[e]>="0"&&r[e]<="9")for(;e<r.length&&r[e]>="0"&&r[e]<="9";)e++;else e=a}const u=r.slice(s,e);t.push({kind:"num",text:u,pos:s,value:Number(u)});continue}if(/[A-Za-z_]/.test(n)){const s=e;for(;e<r.length&&/[A-Za-z0-9_]/.test(r[e]);)e++;t.push({kind:"ident",text:r.slice(s,e),pos:s});continue}const i=m.find(s=>r.startsWith(s,e));if(i){t.push({kind:"op",text:i,pos:e}),e+=i.length;continue}if(b.includes(n)){t.push({kind:"punc",text:n,pos:e}),e++;continue}throw new o(`Unexpected character ${JSON.stringify(n)} at position ${e}`,e)}return t.push({kind:"eof",text:"",pos:r.length}),t}class f{toks;ctx;p=0;reads=[];usesDiePass=!1;constructor(t,e){this.toks=t,this.ctx=e}peek(){return this.toks[this.p]}next(){return this.toks[this.p++]}at(t){const e=this.peek();return(e.kind==="op"||e.kind==="punc"||e.kind==="ident")&&e.text===t}eat(t){return this.at(t)?(this.p++,!0):!1}expect(t){if(!this.at(t)){const e=this.peek();throw new o(`Expected ${JSON.stringify(t)} but found ${e.kind==="eof"?"end of expression":JSON.stringify(e.text)} at position ${e.pos}`,e.pos)}return this.next()}parse(){const t=this.parseOr();return this.expectEnd(),t}parseTestRefOnly(){const t=this.parseTestRef(this.peek().pos);return this.expectEnd(),t}expectEnd(){const t=this.peek();if(t.kind!=="eof")throw new o(`Unexpected ${JSON.stringify(t.text)} at position ${t.pos}`,t.pos)}requireScalar(t,e,n){if(t.type==="numberVec"||t.type==="boolVec")throw new o(`${e} cannot be applied to a test range at position ${n} \u2014 a range must be reduced first, e.g. mean(t[1010..1015]) or all(testPass[1010..1015])`,n);return t.type}parseOr(){let t=this.parseAnd();for(;this.at("or");){const e=this.next().pos,n=this.parseAnd();this.requireBool(t,"or",e),this.requireBool(n,"or",e),t={node:{k:"binary",op:"or",left:t.node,right:n.node},type:"boolean"}}return t}parseAnd(){let t=this.parseNot();for(;this.at("and");){const e=this.next().pos,n=this.parseNot();this.requireBool(t,"and",e),this.requireBool(n,"and",e),t={node:{k:"binary",op:"and",left:t.node,right:n.node},type:"boolean"}}return t}requireBool(t,e,n){if(this.requireScalar(t,`Operator ${JSON.stringify(e)}`,n),t.type!=="boolean")throw new o(`Operator ${JSON.stringify(e)} needs a true/false operand at position ${n}, but got a number \u2014 compare it first, e.g. t[1020] > 5`,n)}parseNot(){if(this.at("not")){const t=this.next().pos,e=this.parseNot();return this.requireBool(e,"not",t),{node:{k:"unary",op:"not",arg:e.node},type:"boolean"}}return this.parseCompare()}parseCompare(){const t=this.parseAdd(),e=this.peek();if(e.kind==="op"&&["<","<=",">",">=","==","!="].includes(e.text)){this.next();const n=this.parseAdd(),i=this.requireScalar(t,`Operator ${JSON.stringify(e.text)}`,e.pos),s=this.requireScalar(n,`Operator ${JSON.stringify(e.text)}`,e.pos);if(i!==s)throw new o(`Cannot compare a ${i} with a ${s} at position ${e.pos}`,e.pos);if(i==="boolean"&&e.text!=="=="&&e.text!=="!=")throw new o(`Operator ${JSON.stringify(e.text)} cannot order true/false values at position ${e.pos}`,e.pos);return{node:{k:"binary",op:e.text,left:t.node,right:n.node},type:"boolean"}}return t}parseAdd(){let t=this.parseMul();for(;;){const e=this.peek();if(e.kind!=="op"||e.text!=="+"&&e.text!=="-")return t;this.next();const n=this.parseMul();this.requireNum(t,e.text,e.pos),this.requireNum(n,e.text,e.pos),t={node:{k:"binary",op:e.text,left:t.node,right:n.node},type:"number"}}}parseMul(){let t=this.parsePow();for(;;){const e=this.peek();if(e.kind!=="op"||!["*","/","%"].includes(e.text))return t;this.next();const n=this.parsePow();this.requireNum(t,e.text,e.pos),this.requireNum(n,e.text,e.pos),t={node:{k:"binary",op:e.text,left:t.node,right:n.node},type:"number"}}}parsePow(){const t=this.parseUnary(),e=this.peek();if(e.kind==="op"&&e.text==="^"){this.next();const n=this.parsePow();return this.requireNum(t,"^",e.pos),this.requireNum(n,"^",e.pos),{node:{k:"binary",op:"^",left:t.node,right:n.node},type:"number"}}return t}requireNum(t,e,n){if(this.requireScalar(t,`Operator ${JSON.stringify(e)}`,n),t.type!=="number")throw new o(`Operator ${JSON.stringify(e)} needs a numeric operand at position ${n}, but got a true/false value`,n)}parseUnary(){const t=this.peek();if(t.kind==="op"&&t.text==="-"){this.next();const e=this.parseUnary();return this.requireNum(e,"-",t.pos),{node:{k:"unary",op:"-",arg:e.node},type:"number"}}return this.parsePrimary()}parsePrimary(){const t=this.next();if(t.kind==="num")return{node:{k:"num",value:t.value},type:"number"};if(t.kind==="punc"&&t.text==="("){const e=this.parseOr();return this.expect(")"),e}if(t.kind==="ident"){if(d.includes(t.text))return this.parseAccessor(t.text,t.pos);if(t.text==="diePass")return this.expect("("),this.expect(")"),this.usesDiePass=!0,{node:{k:"diePass"},type:"boolean"};if(this.at("("))return this.parseCall(t.text,t.pos);if(Object.prototype.hasOwnProperty.call(this.ctx.constants,t.text)){const e=this.ctx.constants[t.text];if(typeof e!="number"||!Number.isFinite(e))throw new o(`Constant ${JSON.stringify(t.text)} at position ${t.pos} is not a finite number`,t.pos);return{node:{k:"num",value:e},type:"number"}}throw new o(`Unknown name ${JSON.stringify(t.text)} at position ${t.pos}. Test values are read as t[1020], verdicts as testPass[1020] or specPass[1020]; other names must be declared in \`constants\`.`,t.pos)}throw new o(`Unexpected ${t.kind==="eof"?"end of expression":JSON.stringify(t.text)} at position ${t.pos}`,t.pos)}parseAccessor(t,e){this.expect("[");const{tests:n,range:i}=this.parseTestRef(e);this.expect("]");for(const u of n)this.checkAccessible(t,u,e);for(const u of n)this.reads.push({kind:t,test:u});const s=t==="t"?"number":"boolean";return i?{node:{k:"vec",kind:t,tests:n},type:s==="number"?"numberVec":"boolVec"}:{node:{k:"acc",kind:t,test:n[0]},type:s}}checkAccessible(t,e,n){const i=this.ctx.testDefs.get(e);if(i===void 0)throw new o(`Test ${e} at position ${n} is not declared in \`testDefs\``,n);if(t==="t"&&!c(i))throw new o(`t[${e}] at position ${n} reads a measured value, but test ${e} is functional (testType 'F') and has no measured value \u2014 use testPass[${e}] for its verdict`,n);if(t==="specPass"){if(!c(i))throw new o(`specPass[${e}] at position ${n} needs spec limits, but test ${e} is functional (testType 'F') \u2014 use testPass[${e}] for its recorded verdict`,n);if(!l(i))throw new o(`specPass[${e}] at position ${n} needs \`limitLow\` or \`limitHigh\` on test ${e}, which declares neither`,n)}}parseTestRef(t){const e=this.expectInt();if(!this.eat(".."))return{tests:[e],range:!1};const n=this.expectInt();if(n<e)throw new o(`Test range ${e}..${n} at position ${t} runs backwards`,t);const i=[...this.ctx.testDefs.keys()].filter(s=>s>=e&&s<=n).sort((s,u)=>s-u);if(i.length===0)throw new o(`Test range ${e}..${n} at position ${t} matches no declared test`,t);return{tests:i,range:!0}}expectInt(){const t=this.peek();if(t.kind!=="num"||!Number.isInteger(t.value))throw new o(`Expected a test number at position ${t.pos}`,t.pos);return this.next(),t.value}parseCall(t,e){const n=FUNCTIONS[t];if(n===void 0)throw new o(`Unknown function ${JSON.stringify(t)} at position ${e}. Available: ${Object.keys(FUNCTIONS).sort().join(", ")}`,e);this.expect("(");const i=[];if(!this.at(")"))do i.push(this.parseOr());while(this.eat(","));this.expect(")");const s=i.map(a=>a.type),u={k:"call",name:t,args:i.map(a=>a.node)};for(const a of n){if(a.vecOf!==void 0){if(s.length!==1)continue;const p=s[0];if(!(p==="numberVec"||p==="boolVec")||a.vecOf!=="any"&&a.vecOf!==p)continue;return{node:u,type:a.returns}}if(a.variadic){if(s.length<2||!s.every(p=>p==="number"))continue;return{node:u,type:a.returns}}if(a.arity!==void 0){if(s.length!==a.arity)continue;if(t==="if"){if(s[0]!=="boolean"||s[1]!=="number"||s[2]!=="number")continue;return{node:u,type:"number"}}if(!s.every(p=>p==="number"))continue;return{node:u,type:a.returns}}}throw new o(`${t}(${s.join(", ")}) at position ${e} is not a valid call \u2014 ${y(t,n)}`,e)}}function y(r,t){return`expected ${t.map(n=>n.vecOf==="boolVec"?`${r}(<true/false test range>)`:n.vecOf==="numberVec"?`${r}(<value test range>)`:n.vecOf==="any"?`${r}(<test range>)`:n.variadic?`${r}(number, number, \u2026)`:r==="if"?"if(<true/false>, number, number)":`${r}(${Array.from({length:n.arity??0},()=>"number").join(", ")})`).join(" or ")}`}export function parseExpression(r,t){try{if(r.length>MAX_EXPRESSION_LENGTH)return{ok:!1,message:`Expression is ${r.length} characters, over the ${MAX_EXPRESSION_LENGTH} limit`,position:MAX_EXPRESSION_LENGTH};const e=new f(h(r),t),{node:n,type:i}=e.parse();return i==="numberVec"||i==="boolVec"?{ok:!1,message:"Expression results in a test range, not a single value \u2014 reduce it, e.g. mean(t[1010..1015]) or all(testPass[1010..1015])",position:0}:{ok:!0,ast:n,type:i,reads:e.reads,usesDiePass:e.usesDiePass}}catch(e){if(e instanceof o)return{ok:!1,message:e.message,position:e.position};throw e}}export function parseTestReference(r,t){try{if(r.length>MAX_EXPRESSION_LENGTH)return{ok:!1,message:`Test reference is ${r.length} characters, over the ${MAX_EXPRESSION_LENGTH} limit`,position:MAX_EXPRESSION_LENGTH};const{tests:e,range:n}=new f(h(r),{testDefs:t,constants:{}}).parseTestRefOnly();return{ok:!0,tests:e,range:n}}catch(e){if(e instanceof o)return{ok:!1,message:e.message,position:e.position};throw e}}export const MAX_EXPRESSION_LENGTH=2e3;
@@ -1,3 +1,8 @@
1
+ /** Multiplier per SI prefix letter. Exported for the one place that PARSES a
2
+ * prefixed number (`stats/sweepXFromName.ts`), so reading and printing values
3
+ * agree on every prefix. Internal — not re-exported from the package. */
4
+ export declare const SI_PREFIX_SCALE: Record<string, number>;
5
+ export declare const KNOWN_BASE_UNITS: string[];
1
6
  /**
2
7
  * Plain-language labels for lot-stack aggregation methods. Maps internal API terms to the words
3
8
  * shown to the user (e.g. `countBin` → "occurrence count"). Shared by the canvas map title and the
@@ -23,8 +28,25 @@ export declare function plainBinTerms(text: string): string;
23
28
  * engineering notation (E±N, multiples of 3) outside that range.
24
29
  */
25
30
  export declare function fmt(v: number, unit?: string, fallbackFormat?: 'si' | 'engineering'): string;
26
- export declare function fmtColorbarAxis(vRef: number, name: string | null | undefined, unit: string | undefined, fallbackFormat?: 'si' | 'engineering'): {
31
+ export interface AxisFormat {
32
+ /** A tick label: a bare number in the axis's shared scale. */
27
33
  tickFmt: (v: number) => string;
34
+ /** A data value at an axis end (not on the tick grid) — see `makeEdgeFormatter`. */
35
+ edgeFmt: (v: number) => string;
36
+ /** Name plus scaled unit, shown once for the axis — e.g. `"Idsat (mA)"`. */
28
37
  axisLabel: string;
29
- };
38
+ /** The scaled unit alone (`"mA"`), for an axis that prints it on every tick; `''` when none. */
39
+ scaledUnit: string;
40
+ }
41
+ /**
42
+ * The formatting for one numeric axis: a shared SI scale chosen from `vRef`
43
+ * (typically the largest-magnitude value), applied to every tick so the axis
44
+ * reads consistently, with the scaled unit returned once.
45
+ *
46
+ * Pass `step` — the spacing of the ticks, from `fitTicks` (`axisTicks.ts`) —
47
+ * for a tick grid. Omit it for lone values, which keep size-based decimals.
48
+ * Without a unit, values in [0.1, 9999] are unscaled and, with no step, use
49
+ * `fmt()` as before.
50
+ */
51
+ export declare function fmtColorbarAxis(vRef: number, name: string | null | undefined, unit: string | undefined, fallbackFormat?: 'si' | 'engineering', step?: number): AxisFormat;
30
52
  //# sourceMappingURL=fmt.d.ts.map
@@ -1 +1 @@
1
- const p=[[1e12,"T"],[1e9,"G"],[1e6,"M"],[1e3,"k"],[1,""],[.001,"m"],[1e-6,"\xB5"],[1e-9,"n"],[1e-12,"p"],[1e-15,"f"]],g={T:1e12,G:1e9,M:1e6,k:1e3,m:.001,\u00B5:1e-6,u:1e-6,n:1e-9,p:1e-12,f:1e-15},E=["Hz","V","A","W","F","\u03A9","Ohm","S","H","J","C","s"];function m(t){for(const e of E)if(t.length===e.length+1&&t.endsWith(e)){const s=t[0];if(s in g)return{preScale:g[s],base:e}}return{preScale:1,base:t}}function F(t,e){const{preScale:s,base:n}=m(e),o=t*s,i=Math.abs(o),[r,u]=p.find(([f])=>i>=f*.9999)??[1e-15,"f"],a=o/r,l=Math.abs(a),c=l>=100?0:l>=10?1:2;return`${a.toFixed(c)} ${u}${n}`}function A(t){const e=Math.abs(t),s=Math.floor(Math.log10(e)/3)*3,n=Math.max(-15,Math.min(12,s)),o=t/Math.pow(10,n),i=Math.abs(o),r=i>=100?0:i>=10?1:2,u=n===0?"":`E${n>0?"+":""}${n}`;return`${o.toFixed(r)}${u}`}export const AGGREGATION_METHOD_LABELS={mean:"mean",median:"median",stddev:"std dev",min:"min",max:"max",count:"count",countBin:"occurrence count",percent:"occurrence %"};export function fmtAggregationMethod(t){return AGGREGATION_METHOD_LABELS[t??""]??t??"aggregated"}export function plainBinTerms(t){return t.replace(/\bHBin\b/g,"hard bin").replace(/\bSBin\b/g,"soft bin")}export function fmt(t,e,s){if(!isFinite(t))return String(t);const n=Math.abs(t);return n===0?e?`0 ${e}`:"0":e!==void 0?F(t,e):s==="si"?F(t,""):n>=.1&&n<1e4?n>=1e3?t.toFixed(0):n>=100?t.toFixed(1):n>=10?t.toFixed(2):t.toFixed(3):A(t)}function h(t){return e=>{if(!isFinite(e))return String(e);if(e===0)return"0";const s=e/t,n=Math.abs(s),o=n>=100?0:n>=10?1:2;return s.toFixed(o)}}export function fmtColorbarAxis(t,e,s,n="engineering"){const o=Math.abs(t);if(s){const{preScale:c,base:f}=m(s),d=o*c,[b,x]=d===0?[1,""]:p.find(([S])=>d>=S*.9999)??[1e-15,"f"],M=`${x}${f}`,$=e?`${e} (${M})`:M;return{tickFmt:h(b/c),axisLabel:$}}if(o===0||o>=.1&&o<1e4)return{tickFmt:c=>fmt(c,void 0,n),axisLabel:e??""};if(n==="si"){const[c]=p.find(([x])=>o>=x*.9999)??[1e-15,"f"],f=Math.round(Math.log10(c)),d=f===0?"":`\xD710E${f}`,b=e?d?`${e} (${d})`:e:d;return{tickFmt:h(c),axisLabel:b}}const i=Math.floor(Math.log10(o)/3)*3,r=Math.max(-15,Math.min(12,i)),u=Math.pow(10,r),a=r===0?"":`\xD710E${r}`,l=e?a?`${e} (${a})`:e:a;return{tickFmt:h(u),axisLabel:l}}
1
+ import{stepDecimals as m}from"./axisTicks.js";const g=[[1e12,"T"],[1e9,"G"],[1e6,"M"],[1e3,"k"],[1,""],[.001,"m"],[1e-6,"\xB5"],[1e-9,"n"],[1e-12,"p"],[1e-15,"f"]];export const SI_PREFIX_SCALE={T:1e12,G:1e9,M:1e6,k:1e3,m:.001,\u00B5:1e-6,u:1e-6,n:1e-9,p:1e-12,f:1e-15},KNOWN_BASE_UNITS=["Hz","V","A","W","F","\u03A9","Ohm","ohm","S","H","J","C","s"];function M(e){for(const t of KNOWN_BASE_UNITS)if(e.length===t.length+1&&e.endsWith(t)){const i=e[0];if(i in SI_PREFIX_SCALE)return{preScale:SI_PREFIX_SCALE[i],base:t}}return{preScale:1,base:e}}function F(e,t){const{preScale:i,base:n}=M(t),r=e*i,o=Math.abs(r),[c,b]=g.find(([s])=>o>=s*.9999)??[1e-15,"f"],f=r/c,h=Math.abs(f),d=h>=100?0:h>=10?1:2;return`${f.toFixed(d)} ${b}${n}`}function S(e){const t=Math.abs(e),i=Math.floor(Math.log10(t)/3)*3,n=Math.max(-15,Math.min(12,i)),r=e/Math.pow(10,n),o=Math.abs(r),c=o>=100?0:o>=10?1:2,b=n===0?"":`E${n>0?"+":""}${n}`;return`${r.toFixed(c)}${b}`}export const AGGREGATION_METHOD_LABELS={mean:"mean",median:"median",stddev:"std dev",min:"min",max:"max",count:"count",countBin:"occurrence count",percent:"occurrence %"};export function fmtAggregationMethod(e){return AGGREGATION_METHOD_LABELS[e??""]??e??"aggregated"}export function plainBinTerms(e){return e.replace(/\bHBin\b/g,"hard bin").replace(/\bSBin\b/g,"soft bin")}export function fmt(e,t,i){if(!isFinite(e))return String(e);const n=Math.abs(e);return n===0?t?`0 ${t}`:"0":t!==void 0?F(e,t):i==="si"?F(e,""):n>=.1&&n<1e4?n>=1e3?e.toFixed(0):n>=100?e.toFixed(1):n>=10?e.toFixed(2):e.toFixed(3):S(e)}function E(e,t){const i=t!==void 0&&t>0?m(t/e):void 0;return n=>{if(!isFinite(n))return String(n);if(n===0)return"0";const r=n/e;if(i!==void 0)return r.toFixed(i);const o=Math.abs(r),c=o>=100?0:o>=10?1:2;return r.toFixed(c)}}function A(e,t,i){if(t===void 0||!(t>0))return i;const n=m(t/e)+1;return r=>{if(!isFinite(r))return String(r);if(r===0)return"0";const o=(r/e).toFixed(n);return o.endsWith("0")?o.slice(0,n===1?-2:-1):o}}export function fmtColorbarAxis(e,t,i,n="engineering",r){const o=Math.abs(e),c=(s,u,a,x)=>{const l=x&&r===void 0?x:E(s,r);return{tickFmt:l,edgeFmt:A(s,r,l),axisLabel:u,scaledUnit:a}};if(i){const{preScale:s,base:u}=M(i),a=o*s,[x,l]=a===0?[1,""]:g.find(([$])=>a>=$*.9999)??[1e-15,"f"],p=`${l}${u}`;return c(x/s,t?`${t} (${p})`:p,p)}if(o===0||o>=.1&&o<1e4)return c(1,t??"","",s=>fmt(s,void 0,n));if(n==="si"){const[s]=g.find(([x])=>o>=x*.9999)??[1e-15,"f"],u=Math.round(Math.log10(s)),a=u===0?"":`\xD710E${u}`;return c(s,t?a?`${t} (${a})`:t:a,a)}const b=Math.floor(Math.log10(o)/3)*3,f=Math.max(-15,Math.min(12,b)),h=Math.pow(10,f),d=f===0?"":`\xD710E${f}`;return c(h,t?d?`${t} (${d})`:t:d,d)}
@@ -5,5 +5,6 @@ export type { WaferMetadata, DieMetadata } from '../core/metadata.js';
5
5
  export * from './colorSchemes.js';
6
6
  export type { BinColors, BinColorOptions, BinColorSource, MapBinColorOptions } from './binColors.js';
7
7
  export { binColorsForMaps } from './binColors.js';
8
+ export { DERIVED_MARK, DERIVED_KEY } from './testLabel.js';
8
9
  export { valueToViridis, valueToGreyscale, getValueColorScheme, buildView, buildHoverText, buildMapTitle, resolveBinColors, getBinColorScheme, contrastTextColor, dieHasTestData, isParametricTest, getDieTestValue, STANDARD_WAFER_DIAMETERS_MM, } from './deprecated.js';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- export{getDieKey}from"./buildView.js";export*from"./buildWaferMap.js";export*from"./colorSchemes.js";export{binColorsForMaps}from"./binColors.js";export{valueToViridis,valueToGreyscale,getValueColorScheme,buildView,buildHoverText,buildMapTitle,resolveBinColors,getBinColorScheme,contrastTextColor,dieHasTestData,isParametricTest,getDieTestValue,STANDARD_WAFER_DIAMETERS_MM}from"./deprecated.js";
1
+ export{getDieKey}from"./buildView.js";export*from"./buildWaferMap.js";export*from"./colorSchemes.js";export{binColorsForMaps}from"./binColors.js";export{DERIVED_MARK,DERIVED_KEY}from"./testLabel.js";export{valueToViridis,valueToGreyscale,getValueColorScheme,buildView,buildHoverText,buildMapTitle,resolveBinColors,getBinColorScheme,contrastTextColor,dieHasTestData,isParametricTest,getDieTestValue,STANDARD_WAFER_DIAMETERS_MM}from"./deprecated.js";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The single spec-limit judgement in the library: "is this measured value within
3
+ * the test's limits?"
4
+ *
5
+ * Every surface that shows or counts an out-of-spec die reads this — value-mode
6
+ * die fills and ▽/△ markers (`buildView`), the legend's spec tally, the spec
7
+ * pass-rate tables (`stats/testPassRate.ts`), and the `specPass[n]` accessor in
8
+ * derived-test expressions. It previously existed twice, once here and once in
9
+ * `testPassRate.ts`, and the two disagreed on both edge cases below.
10
+ *
11
+ * Two rules, and both are the *strict* reading:
12
+ *
13
+ * 1. **No limits defined ⇒ `null` (no judgement).** A test with no limits has not
14
+ * been given a spec, so there is nothing to be inside of. Reporting `'pass'`
15
+ * would manufacture an in-spec verdict out of an absence, and it would inflate
16
+ * a spec pass-rate to 100% for every unlimited test.
17
+ * 2. **A non-finite value ⇒ `null`.** `NaN` is not in spec. Comparison operators
18
+ * are false for `NaN`, so a naive `v < low` / `v > high` pair falls through to
19
+ * "pass" — which is exactly the "never drawn as plain in-spec" failure this
20
+ * judgement exists to prevent.
21
+ *
22
+ * `null` means no verdict and must be treated as no-data by every caller, never
23
+ * as a pass and never as a fail.
24
+ *
25
+ * Out-of-spec *classification* depends ONLY on whether limits are defined — never on
26
+ * `colorbarRangeMode`. An out-of-spec die is always flagged when limits exist, regardless
27
+ * of how the colorbar is scaled. The *form* of the indication, decided in
28
+ * `pushDieRectangles`, depends only on the effective `passFailDisplay`: under `'spec'` the
29
+ * die gets a solid green/blue/red categorical fill; in normal value/gradient mode it keeps
30
+ * the value gradient fill (so the distribution stays readable and out-of-spec colours don't
31
+ * collide with the scheme) and is flagged with a ▽/△ marker (`ViewRect.specMark`).
32
+ */
33
+ export type SpecCategory = 'pass' | 'failHigh' | 'failLow';
34
+ /** Just the fields of a `TestDef` this judgement reads — kept structural so
35
+ * `core/` and `stats/` can call it without importing the full def. */
36
+ export interface SpecLimits {
37
+ limitLow?: number;
38
+ limitHigh?: number;
39
+ }
40
+ /**
41
+ * Classify `value` against `limits`. Returns `null` when no verdict is possible:
42
+ * no value, a non-finite value, or a test with neither limit defined.
43
+ */
44
+ export declare function classifySpec(value: number | undefined, limits: SpecLimits | undefined): SpecCategory | null;
45
+ /** True when `limits` can produce a verdict at all — i.e. at least one limit is
46
+ * defined. Used to reject `specPass[n]` at parse time rather than silently
47
+ * yielding no-data for every die. */
48
+ export declare function hasSpecLimits(limits: SpecLimits | undefined): boolean;
49
+ //# sourceMappingURL=spec.d.ts.map
@@ -0,0 +1 @@
1
+ export function classifySpec(n,i){return n===void 0||!Number.isFinite(n)||i===void 0||i.limitLow===void 0&&i.limitHigh===void 0?null:i.limitLow!==void 0&&n<i.limitLow?"failLow":i.limitHigh!==void 0&&n>i.limitHigh?"failHigh":"pass"}export function hasSpecLimits(n){return n!==void 0&&(n.limitLow!==void 0||n.limitHigh!==void 0)}