@wafertools/wafermap 0.30.1 → 0.30.2

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 (38) hide show
  1. package/AGENTS.md +59 -10
  2. package/CHANGELOG.md +80 -8
  3. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  4. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  6. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  8. package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
  9. package/dist/packages/canvas-adapter/chunked.js +1 -0
  10. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  11. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +55 -0
  12. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  13. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  14. package/dist/packages/canvas-adapter/summaryPanel.d.ts +55 -9
  15. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  16. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  17. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  18. package/dist/packages/canvas-adapter/userGuideHtml.js +1 -1
  19. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  20. package/dist/packages/canvas-adapter/version.js +1 -1
  21. package/dist/packages/core/aggregates.js +1 -1
  22. package/dist/packages/core/utils.d.ts +55 -0
  23. package/dist/packages/core/utils.js +1 -1
  24. package/dist/packages/renderer/binColors.d.ts +14 -0
  25. package/dist/packages/renderer/binColors.js +1 -1
  26. package/dist/packages/renderer/buildView.js +1 -1
  27. package/dist/packages/renderer/buildWaferMap.js +1 -1
  28. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  29. package/dist/packages/stats/capability.d.ts +56 -0
  30. package/dist/packages/stats/capability.js +1 -1
  31. package/dist/packages/stats/correlation.d.ts +30 -0
  32. package/dist/packages/stats/correlation.js +1 -1
  33. package/dist/packages/stats/findingsNarrative.js +1 -1
  34. package/dist/packages/stats/math.d.ts +27 -2
  35. package/dist/packages/stats/math.js +1 -1
  36. package/dist/packages/stats/renderSummaryReport.js +4 -4
  37. package/llms.txt +12 -7
  38. package/package.json +2 -2
@@ -40,7 +40,37 @@ export declare function pearsonOfPairs(pairs: ArrayLike<{
40
40
  export interface CorrelationMatrix {
41
41
  tests: CorrelationTestInfo[];
42
42
  cells: CorrelationCell[];
43
+ /**
44
+ * Present only when the matrix was computed from a sample of the dies rather
45
+ * than all of them — see `CORRELATION_DIE_BUDGET`. `of` is the population that
46
+ * carried test values, `used` how many were read.
47
+ *
48
+ * **Any surface showing this matrix must say so.** An `r` from a sample is a
49
+ * perfectly good estimate, but an unlabelled one is a number the reader will
50
+ * take for the whole population — the exact class of quietly-wrong figure this
51
+ * library exists to prevent. `correlationSampleNote()` is the wording.
52
+ */
53
+ sample?: {
54
+ of: number;
55
+ used: number;
56
+ };
43
57
  }
58
+ /**
59
+ * Dies read before `buildCorrelationMatrix` starts sampling.
60
+ *
61
+ * Correlation is the only computation here that is quadratic in tests *and*
62
+ * linear in dies: a 400k-die, 50-test lot is 1,225 pairs per die — 490 million
63
+ * pair updates, each touching six accumulators — which in a browser presents as
64
+ * a panel that never renders (tsmap WMAP_ISSUES #61).
65
+ *
66
+ * 25,000 is far past the point where more dies change a Pearson coefficient: the
67
+ * standard error of r is about `(1 - r²)/sqrt(n)`, so at n = 25,000 it is under
68
+ * 0.007 even for r = 0. Sixteen times more dies would halve an error that is
69
+ * already invisible at two decimal places.
70
+ */
71
+ export declare const CORRELATION_DIE_BUDGET = 25000;
72
+ /** The sentence a panel puts next to a sampled matrix. */
73
+ export declare function correlationSampleNote(sample: CorrelationMatrix['sample']): string | undefined;
44
74
  /**
45
75
  * Pearson correlation matrix for every test in `testDefs` across every die
46
76
  * in `dies`.
@@ -1 +1 @@
1
- import{isParametricTest as A}from"../renderer/buildWaferMap.js";export function pearsonFromSums(o,l,u,i,y,f){if(o<3)return null;const d=l/o,c=u/o,r=f/o-d*c,m=i/o-d*d,p=y/o-c*c,h=Math.sqrt(m*p);return h===0?null:Math.max(-1,Math.min(1,r/h))}export function pearsonOfPairs(o){let l=0,u=0,i=0,y=0,f=0,d=0;for(let c=0;c<o.length;c++){const{x:r,y:m}=o[c];!Number.isFinite(r)||!Number.isFinite(m)||(l++,u+=r,i+=m,y+=r*r,f+=m*m,d+=r*m)}return{r:pearsonFromSums(l,u,i,y,f,d),n:l}}function N(o){return o.filter(l=>l.testNumber!==void 0&&A(l)).map(l=>({testNumber:l.testNumber,label:l.name,unit:l.unit}))}export function buildCorrelationMatrix(o,l){const u=N(l);if(u.length<2)return{tests:u,cells:[]};const i=u.length,y=u.map(t=>t.testNumber),f=i*(i-1)/2,d=new Float64Array(f),c=new Float64Array(i),r=new Float64Array(f),m=new Float64Array(f),p=new Float64Array(f),h=new Float64Array(f),g=new Float64Array(f);function w(t,n){return t*i-(t*(t+1)>>1)+(n-t-1)}for(const t of o){if(!t.testValues)continue;const n=new Float64Array(i),x=new Uint8Array(i);for(let s=0;s<i;s++){const a=t.testValues[y[s]];a!==void 0&&Number.isFinite(a)&&(n[s]=a,x[s]=1)}for(let s=0;s<i;s++){if(!x[s])continue;c[s]++;const a=n[s];for(let b=s+1;b<i;b++){if(!x[b])continue;const M=n[b],I=w(s,b);d[I]++,r[I]+=a,m[I]+=M,p[I]+=a*a,h[I]+=M*M,g[I]+=a*M}}}const F=t=>pearsonFromSums(d[t],r[t],m[t],p[t],h[t],g[t]),e=[];for(let t=0;t<i;t++)for(let n=0;n<i;n++){if(n===t){e.push({xIndex:n,yIndex:t,r:1,n:c[n]});continue}const x=Math.min(n,t),s=Math.max(n,t),a=w(x,s);e.push({xIndex:n,yIndex:t,r:F(a),n:d[a]})}return{tests:u,cells:e}}export function filterCorrelationMatrix(o,{threshold:l=.3,minTests:u=6,maxTests:i=20}={}){const y=[];let f=0,d=0,c=null;for(const e of o.cells){if(e.xIndex>=e.yIndex||e.r===null)continue;const t=Math.abs(e.r);y.push({xi:e.xIndex,yi:e.yIndex,absR:t}),(c===null||t>Math.abs(c.r))&&(c={xLabel:o.tests[e.xIndex].label,yLabel:o.tests[e.yIndex].label,r:e.r})}y.sort((e,t)=>t.absR-e.absR);const r=new Set;for(const{xi:e,yi:t,absR:n}of y){const x=n<l;r.size>=i||x&&r.size>=u||(r.add(e),r.size<i&&r.add(t))}if(r.size<Math.min(u,o.tests.length))for(let e=0;e<o.tests.length&&r.size<Math.min(i,u,o.tests.length);e++)r.add(e);const m=(()=>{const e=Array.from(r),t=new Map,n=new Map;for(const{xi:s,yi:a,absR:b}of y)!r.has(s)||!r.has(a)||(t.set(s,(t.get(s)??0)+b),t.set(a,(t.get(a)??0)+b),n.set(s,(n.get(s)??0)+1),n.set(a,(n.get(a)??0)+1));const x=s=>(n.get(s)??0)>0?t.get(s)/n.get(s):0;return e.sort((s,a)=>x(a)-x(s))})(),p=m.map(e=>o.tests[e]),h=new Map(m.map((e,t)=>[e,t])),g=new Set(p.map(e=>e.testNumber)),w=o.cells.filter(e=>g.has(o.tests[e.xIndex].testNumber)&&g.has(o.tests[e.yIndex].testNumber)).map(e=>({xIndex:h.get(e.xIndex),yIndex:h.get(e.yIndex),r:e.r,n:e.n}));let F=0;for(const{xi:e,yi:t,absR:n}of y)r.has(e)&&r.has(t)?n>=.7?f++:n>=.4&&d++:n<l&&F++;return{matrix:{tests:p,cells:w},strongPairs:f,moderatePairs:d,hiddenWeakPairs:F,strongestPair:c}}
1
+ import{isParametricTest as X}from"../renderer/buildWaferMap.js";export function pearsonFromSums(e,i,a,r,c,d){if(e<3)return null;const m=i/e,f=a/e,s=d/e-m*f,h=r/e-m*m,w=c/e-f*f,I=Math.sqrt(h*w);return I===0?null:Math.max(-1,Math.min(1,s/I))}export function pearsonOfPairs(e){let i=0,a=0,r=0,c=0,d=0,m=0;for(let f=0;f<e.length;f++){const{x:s,y:h}=e[f];!Number.isFinite(s)||!Number.isFinite(h)||(i++,a+=s,r+=h,c+=s*s,d+=h*h,m+=s*h)}return{r:pearsonFromSums(i,a,r,c,d,m),n:i}}export const CORRELATION_DIE_BUDGET=25e3;function Y(e,i){const a=[],r=e/i;for(let c=0;c<i;c++)a.push(Math.floor(c*r));return a}export function correlationSampleNote(e){if(e)return`From a ${e.used.toLocaleString()}-die sample of ${e.of.toLocaleString()}, spread evenly across the lot`}function v(e){return e.filter(i=>i.testNumber!==void 0&&X(i)).map(i=>({testNumber:i.testNumber,label:i.name,unit:i.unit}))}export function buildCorrelationMatrix(e,i){const a=v(i);if(a.length<2)return{tests:a,cells:[]};const r=a.length,c=a.map(n=>n.testNumber),d=r*(r-1)/2,m=new Float64Array(d),f=new Float64Array(r),s=new Float64Array(d),h=new Float64Array(d),w=new Float64Array(d),I=new Float64Array(d),A=new Float64Array(d);function R(n,o){return n*r-(n*(n+1)>>1)+(o-n-1)}const g=e.filter(n=>n.testValues!==void 0),t=g.length>CORRELATION_DIE_BUDGET,l=t?Y(g.length,CORRELATION_DIE_BUDGET).map(n=>g[n]):g,u=new Float64Array(r),p=new Uint8Array(r);for(const n of l){p.fill(0);for(let o=0;o<r;o++){const b=n.testValues[c[o]];b!==void 0&&Number.isFinite(b)&&(u[o]=b,p[o]=1)}for(let o=0;o<r;o++){if(!p[o])continue;f[o]++;const b=u[o];for(let F=o+1;F<r;F++){if(!p[F])continue;const M=u[F],N=R(o,F);m[N]++,s[N]+=b,h[N]+=M,w[N]+=b*b,I[N]+=M*M,A[N]+=b*M}}}const y=n=>pearsonFromSums(m[n],s[n],h[n],w[n],I[n],A[n]),x=[];for(let n=0;n<r;n++)for(let o=0;o<r;o++){if(o===n){x.push({xIndex:o,yIndex:n,r:1,n:f[o]});continue}const b=Math.min(o,n),F=Math.max(o,n),M=R(b,F);x.push({xIndex:o,yIndex:n,r:y(M),n:m[M]})}return t?{tests:a,cells:x,sample:{of:g.length,used:l.length}}:{tests:a,cells:x}}export function filterCorrelationMatrix(e,{threshold:i=.3,minTests:a=6,maxTests:r=20}={}){const c=[];let d=0,m=0,f=null;for(const t of e.cells){if(t.xIndex>=t.yIndex||t.r===null)continue;const l=Math.abs(t.r);c.push({xi:t.xIndex,yi:t.yIndex,absR:l}),(f===null||l>Math.abs(f.r))&&(f={xLabel:e.tests[t.xIndex].label,yLabel:e.tests[t.yIndex].label,r:t.r})}c.sort((t,l)=>l.absR-t.absR);const s=new Set;for(const{xi:t,yi:l,absR:u}of c){const p=u<i;s.size>=r||p&&s.size>=a||(s.add(t),s.size<r&&s.add(l))}if(s.size<Math.min(a,e.tests.length))for(let t=0;t<e.tests.length&&s.size<Math.min(r,a,e.tests.length);t++)s.add(t);const h=(()=>{const t=Array.from(s),l=new Map,u=new Map;for(const{xi:y,yi:x,absR:n}of c)!s.has(y)||!s.has(x)||(l.set(y,(l.get(y)??0)+n),l.set(x,(l.get(x)??0)+n),u.set(y,(u.get(y)??0)+1),u.set(x,(u.get(x)??0)+1));const p=y=>(u.get(y)??0)>0?l.get(y)/u.get(y):0;return t.sort((y,x)=>p(x)-p(y))})(),w=h.map(t=>e.tests[t]),I=new Map(h.map((t,l)=>[t,l])),A=new Set(w.map(t=>t.testNumber)),R=e.cells.filter(t=>A.has(e.tests[t.xIndex].testNumber)&&A.has(e.tests[t.yIndex].testNumber)).map(t=>({xIndex:I.get(t.xIndex),yIndex:I.get(t.yIndex),r:t.r,n:t.n}));let g=0;for(const{xi:t,yi:l,absR:u}of c)s.has(t)&&s.has(l)?u>=.7?d++:u>=.4&&m++:u<i&&g++;return{matrix:{tests:w,cells:R},strongPairs:d,moderatePairs:m,hiddenWeakPairs:g,strongestPair:f}}
@@ -1 +1 @@
1
- const y={unusual:0,notable:1,info:2},S={E:0,ENE:22.5,NE:45,NNE:67.5,N:90,NNW:112.5,NW:135,WNW:157.5,W:180,WSW:202.5,SW:225,SSW:247.5,S:270,SSE:292.5,SE:315,ESE:337.5};function m(e){return e.stats.adjustedPValue??e.stats.pValue??1}function b(e){return[...e].sort((n,t)=>{const o=y[n.severity]-y[t.severity];return o!==0?o:m(n)-m(t)})[0]}function w(e){return e.some(t=>t.variable.kind==="yield")?e.filter(t=>t.variable.kind!=="hardBin"&&t.variable.kind!=="softBin"):e}function $(e){return e.replace(/^(Rings?|Quadrants?|Sectors?)\s+/i,"").split(/[,&]|–|\band\b/).map(n=>n.trim()).filter(Boolean)}function A(e,n){const t=new Set($(e));return $(n).some(o=>t.has(o))}function v(e){return e.variable.kind==="yield"?"yield":e.variable.kind==="hardBin"?`HBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.kind==="softBin"?`SBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.label}function h(e){let n=0,t=0;for(const o of e)o.effect.direction==="higher"?n++:o.effect.direction==="lower"&&t++;return n>t?"higher":t>n?"lower":"mixed"}function d(e){return e==="higher"?"elevated":e==="lower"?"reduced":"shifted"}function p(e){const n=new Set,t=[];for(const o of e){const r=v(o);n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:t.length===3?`${t[0]}, ${t[1]}, and ${t[2]}`:`${t[0]}, ${t[1]}, and ${t.length-2} more`}function E(e){const n=new Set,t=[];for(const o of e){const r=o.comparison.left;n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`}function R(e){return e.length!==1?!1:!/^(Rings|Quadrants|Sectors)\b/.test(e[0].comparison.left)}function B(e){const n=h(e),t=e.every(c=>c.comparison.left.includes("(edge)")),o=e.every(c=>c.comparison.left.includes("(core)"));let r,a;return o?(r="The core ring",a=!0):t&&e.length>1?(r="The edge rings",a=!1):(r=E(e),a=R(e)),`${r} show${a?"s":""} ${d(n)} ${p(e)}.`}function I(e){const n=h(e),t=s=>s.highlight.kind==="region"&&s.highlight.regionKeys.length>1||s.comparison.left.startsWith("Quadrants"),o=e.filter(s=>t(s)),r=e.filter(s=>!t(s)),a=[];r.length===1?a.push(`the ${r[0].comparison.left} quadrant`):r.length>1&&a.push(`the ${E(r)} quadrants`);for(const s of o)a.push(s.comparison.left.replace(/^Quadrants/,"quadrants"));const c=a.join(" and "),u=r.length===1&&o.length===0;return`${c.charAt(0).toUpperCase()+c.slice(1)} show${u?"s":""} ${d(n)} ${p(e)}.`}function P(e){const n=h(e),t=[],o=new Set;for(const u of e){const i=u.comparison.left.replace(/^Sectors? /,"");o.has(i)||(o.add(i),t.push(i))}let r,a;return t.length===1?(r=`the ${t[0]} ${e[0].comparison.left.startsWith("Sectors")?"sectors":"region"}`,a=!e[0].comparison.left.startsWith("Sectors")):(r=`the ${t.length===2?t.join(" and "):`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`} sectors`,a=!1),`${r.charAt(0).toUpperCase()+r.slice(1)} show${a?"s":""} ${d(n)} ${p(e)}.`}function x(e){if(e.length===1)return`A failure cluster at ${e[0].comparison.left.replace(/^Cluster at /,"")}.`;const t=e.reduce((o,r)=>(o.stats.sampleSizeLeft??0)>=(r.stats.sampleSizeLeft??0)?o:r).comparison.left.replace(/^Cluster at /,"");return`${e.length} failure clusters identified; the largest at ${t}.`}function C(e){const n=e[0].comparison.left.replace(/^Edge arc ~/,"");return e.length===1?`An edge arc near ${n} shows localised failures.`:`${e.length} edge arcs detected, including near ${n}.`}function q(e){const n=h(e);return e.length===1?`Reticle-position variation: ${e[0].comparison.left} shows ${d(n)} ${p(e)}.`:`Reticle-position variation across ${e.length} cells (${d(n)} ${p(e)}).`}function F(e,n){const t=e.stats.method==="geometry"?` (${e.severity==="unusual"?"high":e.severity==="notable"?"medium":"low"} confidence)`:"",o=new Set(e.relatedIds??[]),r=n.find(c=>o.has(c.id)&&(c.comparison.family==="edge-arc"||c.comparison.family==="cluster"));let a="";return r&&(r.comparison.family==="edge-arc"?a=`; failures concentrated at the wafer edge near ${r.comparison.left.replace(/^Edge arc ~/,"")}`:a=`; failures concentrated near ${r.comparison.left.replace(/^Cluster at /,"")}`),`${e.comparison.left} failure pattern detected${t}${a}.`}function Y(e){const n=e.replace(/\.$/,"");return n.charAt(0).toLowerCase()+n.slice(1)}function M(e){const n=e.filter(t=>t.variable.kind==="yield");return n.length>0?h(n):h(e)}function k(e,n){const t=new Map;for(const i of n){const s=t.get(i.comparison.left)??[];s.push(i),t.set(i.comparison.left,s)}const o=[],r=[];for(const i of t.values()){const s=M(i);s==="higher"?o.push(...i.filter(l=>l.effect.direction==="higher")):s==="lower"?r.push(...i.filter(l=>l.effect.direction==="lower")):o.push(...i)}const a=o.map(i=>i.comparison.left),c=r.filter(i=>!a.some(s=>A(s,i.comparison.left)));if(o.length>0&&c.length>0){const i=n.some(f=>f.variable.kind==="yield"),[s,l]=i?[c,o]:[o,c];return`${e(s).replace(/\.$/,"")} while ${Y(e(l))}.`}if(o.length>0&&r.length>0&&c.length===0)return e(o);const u=M(n);if(u!=="mixed"&&n.some(i=>i.variable.kind==="yield")){const i=n.filter(s=>s.effect.direction===u);if(i.length>0)return e(i)}return e(n)}function N(e){let n=0,t=0,o=0;for(const r of e)for(const a of $(r.comparison.left)){const c=S[a];if(c===void 0)continue;const u=c*Math.PI/180;n+=Math.cos(u),t+=Math.sin(u),o++}return o===0||n===0&&t===0?null:(Math.atan2(t,n)*180/Math.PI+360)%360}function W(e){let n="E",t=360;for(const[o,r]of Object.entries(S)){const a=Math.abs((e-r+540)%360-180);a<t&&(t=a,n=o)}return n}function D(e){const n=e.filter(u=>u.effect.direction==="higher"),t=e.filter(u=>u.effect.direction==="lower");if(n.length===0||t.length===0)return null;const o=new Set(e.map(v));if(o.size!==1)return null;const r=N(n),a=N(t);return r===null||a===null||Math.abs((r-a+540)%360-180)<120?null:`${[...o][0]} increases from ${W(a)} toward ${W(r)} across the wafer.`}function O(e){const n=w(e);if(n.length===0)return"";const t=["sector","quadrant","ring"],o=new Map;for(const i of n){const s=o.get(i.comparison.family)??[];s.push(i),o.set(i.comparison.family,s)}const r=[...o.keys()].sort((i,s)=>{const l=Math.min(...o.get(i).map(m)),f=Math.min(...o.get(s).map(m));return l!==f?l-f:t.indexOf(i)-t.indexOf(s)})[0],a=o.get(r),c=D(a);return c||k(r==="ring"?B:r==="quadrant"?I:P,a)}function j(e,n){switch(e){case"cluster":return x(n);case"edge-arc":return C(n);case"reticle-position":return k(q,n);default:return""}}const L=new Set(["ring","quadrant","sector"]);export function buildFindingsNarrative(e){if(e.length===0)return"";const n=e.filter(s=>s.severity!=="info");if(n.length===0)return"Minor spatial variation detected; no strongly significant patterns.";const t=[],o=n.filter(s=>s.comparison.family==="spatial-pattern"),r=o.length?b(o):null,a=new Set(r?.relatedIds??[]);r&&t.push(F(r,n));const c=n.filter(s=>s!==r&&s.comparison.family!=="spatial-pattern"&&!a.has(s.id));let u=c.filter(s=>L.has(s.comparison.family));if(r&&u.length){const s=w(u);s.length>0&&s.every(f=>f.variable.kind==="yield"&&f.effect.direction==="higher")&&(u=[])}const i=new Set;if(u.length){const s=O(u);if(s){t.push(s);for(const l of L)i.add(l)}}else if(!r){const s=b(c);if(s){const l=s.comparison.family,f=j(l,c.filter(g=>g.comparison.family===l));f&&(t.push(f),i.add(l))}}if(t.length<3){const s=c.filter(l=>(l.comparison.family==="cluster"||l.comparison.family==="edge-arc")&&!i.has(l.comparison.family));if(s.length){const l=s[0].comparison.family,f=j(l,s.filter(g=>g.comparison.family===l));f&&t.push(f)}}return t.slice(0,3).join(" ")}
1
+ import{minOf as y}from"../core/utils.js";const S={unusual:0,notable:1,info:2},b={E:0,ENE:22.5,NE:45,NNE:67.5,N:90,NNW:112.5,NW:135,WNW:157.5,W:180,WSW:202.5,SW:225,SSW:247.5,S:270,SSE:292.5,SE:315,ESE:337.5};function m(e){return e.stats.adjustedPValue??e.stats.pValue??1}function w(e){return[...e].sort((n,t)=>{const o=S[n.severity]-S[t.severity];return o!==0?o:m(n)-m(t)})[0]}function v(e){return e.some(t=>t.variable.kind==="yield")?e.filter(t=>t.variable.kind!=="hardBin"&&t.variable.kind!=="softBin"):e}function $(e){return e.replace(/^(Rings?|Quadrants?|Sectors?)\s+/i,"").split(/[,&]|–|\band\b/).map(n=>n.trim()).filter(Boolean)}function R(e,n){const t=new Set($(e));return $(n).some(o=>t.has(o))}function E(e){return e.variable.kind==="yield"?"yield":e.variable.kind==="hardBin"?`HBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.kind==="softBin"?`SBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.label}function h(e){let n=0,t=0;for(const o of e)o.effect.direction==="higher"?n++:o.effect.direction==="lower"&&t++;return n>t?"higher":t>n?"lower":"mixed"}function d(e){return e==="higher"?"elevated":e==="lower"?"reduced":"shifted"}function p(e){const n=new Set,t=[];for(const o of e){const r=E(o);n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:t.length===3?`${t[0]}, ${t[1]}, and ${t[2]}`:`${t[0]}, ${t[1]}, and ${t.length-2} more`}function k(e){const n=new Set,t=[];for(const o of e){const r=o.comparison.left;n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`}function B(e){return e.length!==1?!1:!/^(Rings|Quadrants|Sectors)\b/.test(e[0].comparison.left)}function I(e){const n=h(e),t=e.every(c=>c.comparison.left.includes("(edge)")),o=e.every(c=>c.comparison.left.includes("(core)"));let r,a;return o?(r="The core ring",a=!0):t&&e.length>1?(r="The edge rings",a=!1):(r=k(e),a=B(e)),`${r} show${a?"s":""} ${d(n)} ${p(e)}.`}function P(e){const n=h(e),t=s=>s.highlight.kind==="region"&&s.highlight.regionKeys.length>1||s.comparison.left.startsWith("Quadrants"),o=e.filter(s=>t(s)),r=e.filter(s=>!t(s)),a=[];r.length===1?a.push(`the ${r[0].comparison.left} quadrant`):r.length>1&&a.push(`the ${k(r)} quadrants`);for(const s of o)a.push(s.comparison.left.replace(/^Quadrants/,"quadrants"));const c=a.join(" and "),u=r.length===1&&o.length===0;return`${c.charAt(0).toUpperCase()+c.slice(1)} show${u?"s":""} ${d(n)} ${p(e)}.`}function x(e){const n=h(e),t=[],o=new Set;for(const u of e){const i=u.comparison.left.replace(/^Sectors? /,"");o.has(i)||(o.add(i),t.push(i))}let r,a;return t.length===1?(r=`the ${t[0]} ${e[0].comparison.left.startsWith("Sectors")?"sectors":"region"}`,a=!e[0].comparison.left.startsWith("Sectors")):(r=`the ${t.length===2?t.join(" and "):`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`} sectors`,a=!1),`${r.charAt(0).toUpperCase()+r.slice(1)} show${a?"s":""} ${d(n)} ${p(e)}.`}function C(e){if(e.length===1)return`A failure cluster at ${e[0].comparison.left.replace(/^Cluster at /,"")}.`;const t=e.reduce((o,r)=>(o.stats.sampleSizeLeft??0)>=(r.stats.sampleSizeLeft??0)?o:r).comparison.left.replace(/^Cluster at /,"");return`${e.length} failure clusters identified; the largest at ${t}.`}function q(e){const n=e[0].comparison.left.replace(/^Edge arc ~/,"");return e.length===1?`An edge arc near ${n} shows localised failures.`:`${e.length} edge arcs detected, including near ${n}.`}function F(e){const n=h(e);return e.length===1?`Reticle-position variation: ${e[0].comparison.left} shows ${d(n)} ${p(e)}.`:`Reticle-position variation across ${e.length} cells (${d(n)} ${p(e)}).`}function O(e,n){const t=e.stats.method==="geometry"?` (${e.severity==="unusual"?"high":e.severity==="notable"?"medium":"low"} confidence)`:"",o=new Set(e.relatedIds??[]),r=n.find(c=>o.has(c.id)&&(c.comparison.family==="edge-arc"||c.comparison.family==="cluster"));let a="";return r&&(r.comparison.family==="edge-arc"?a=`; failures concentrated at the wafer edge near ${r.comparison.left.replace(/^Edge arc ~/,"")}`:a=`; failures concentrated near ${r.comparison.left.replace(/^Cluster at /,"")}`),`${e.comparison.left} failure pattern detected${t}${a}.`}function Y(e){const n=e.replace(/\.$/,"");return n.charAt(0).toLowerCase()+n.slice(1)}function N(e){const n=e.filter(t=>t.variable.kind==="yield");return n.length>0?h(n):h(e)}function M(e,n){const t=new Map;for(const i of n){const s=t.get(i.comparison.left)??[];s.push(i),t.set(i.comparison.left,s)}const o=[],r=[];for(const i of t.values()){const s=N(i);s==="higher"?o.push(...i.filter(l=>l.effect.direction==="higher")):s==="lower"?r.push(...i.filter(l=>l.effect.direction==="lower")):o.push(...i)}const a=o.map(i=>i.comparison.left),c=r.filter(i=>!a.some(s=>R(s,i.comparison.left)));if(o.length>0&&c.length>0){const i=n.some(f=>f.variable.kind==="yield"),[s,l]=i?[c,o]:[o,c];return`${e(s).replace(/\.$/,"")} while ${Y(e(l))}.`}if(o.length>0&&r.length>0&&c.length===0)return e(o);const u=N(n);if(u!=="mixed"&&n.some(i=>i.variable.kind==="yield")){const i=n.filter(s=>s.effect.direction===u);if(i.length>0)return e(i)}return e(n)}function W(e){let n=0,t=0,o=0;for(const r of e)for(const a of $(r.comparison.left)){const c=b[a];if(c===void 0)continue;const u=c*Math.PI/180;n+=Math.cos(u),t+=Math.sin(u),o++}return o===0||n===0&&t===0?null:(Math.atan2(t,n)*180/Math.PI+360)%360}function j(e){let n="E",t=360;for(const[o,r]of Object.entries(b)){const a=Math.abs((e-r+540)%360-180);a<t&&(t=a,n=o)}return n}function D(e){const n=e.filter(u=>u.effect.direction==="higher"),t=e.filter(u=>u.effect.direction==="lower");if(n.length===0||t.length===0)return null;const o=new Set(e.map(E));if(o.size!==1)return null;const r=W(n),a=W(t);return r===null||a===null||Math.abs((r-a+540)%360-180)<120?null:`${[...o][0]} increases from ${j(a)} toward ${j(r)} across the wafer.`}function T(e){const n=v(e);if(n.length===0)return"";const t=["sector","quadrant","ring"],o=new Map;for(const i of n){const s=o.get(i.comparison.family)??[];s.push(i),o.set(i.comparison.family,s)}const r=[...o.keys()].sort((i,s)=>{const l=y(o.get(i).map(m)),f=y(o.get(s).map(m));return l!==f?l-f:t.indexOf(i)-t.indexOf(s)})[0],a=o.get(r),c=D(a);return c||M(r==="ring"?I:r==="quadrant"?P:x,a)}function L(e,n){switch(e){case"cluster":return C(n);case"edge-arc":return q(n);case"reticle-position":return M(F,n);default:return""}}const A=new Set(["ring","quadrant","sector"]);export function buildFindingsNarrative(e){if(e.length===0)return"";const n=e.filter(s=>s.severity!=="info");if(n.length===0)return"Minor spatial variation detected; no strongly significant patterns.";const t=[],o=n.filter(s=>s.comparison.family==="spatial-pattern"),r=o.length?w(o):null,a=new Set(r?.relatedIds??[]);r&&t.push(O(r,n));const c=n.filter(s=>s!==r&&s.comparison.family!=="spatial-pattern"&&!a.has(s.id));let u=c.filter(s=>A.has(s.comparison.family));if(r&&u.length){const s=v(u);s.length>0&&s.every(f=>f.variable.kind==="yield"&&f.effect.direction==="higher")&&(u=[])}const i=new Set;if(u.length){const s=T(u);if(s){t.push(s);for(const l of A)i.add(l)}}else if(!r){const s=w(c);if(s){const l=s.comparison.family,f=L(l,c.filter(g=>g.comparison.family===l));f&&(t.push(f),i.add(l))}}if(t.length<3){const s=c.filter(l=>(l.comparison.family==="cluster"||l.comparison.family==="edge-arc")&&!i.has(l.comparison.family));if(s.length){const l=s[0].comparison.family,f=L(l,s.filter(g=>g.comparison.family===l));f&&t.push(f)}}return t.slice(0,3).join(" ")}
@@ -5,6 +5,31 @@
5
5
  export declare function errorFunction(value: number): number;
6
6
  /** Standard normal cumulative distribution function Φ(value). */
7
7
  export declare function normalCdf(value: number): number;
8
- /** Linear-interpolation quantile of a pre-sorted array (`q` in [0, 1]). */
9
- export declare function quantile(sorted: number[], q: number): number;
8
+ /** Linear-interpolation quantile of a pre-sorted array (`q` in [0, 1]).
9
+ *
10
+ * `ArrayLike<number>`, not `number[]`, so a caller holding its values in a
11
+ * `Float64Array` does not have to copy them back into a plain array to be
12
+ * read — see `pooledTestStatsSteps`, which sorts in one. */
13
+ export declare function quantile(sorted: ArrayLike<number>, q: number): number;
14
+ /** Descriptive statistics of one population of values — see {@link describeSorted}. */
15
+ export interface DescriptiveStats {
16
+ min: number;
17
+ max: number;
18
+ mean: number;
19
+ count: number;
20
+ /** Population standard deviation (divide by n) — see {@link describeSorted}. */
21
+ stddev: number;
22
+ median: number;
23
+ q1: number;
24
+ q3: number;
25
+ }
26
+ /** Min/max/mean/count/σ and the quartiles of a pre-sorted array — the one copy
27
+ * of the descriptive-statistics formulas behind the Test Values table.
28
+ *
29
+ * σ is the POPULATION standard deviation (divide by n), which is what this
30
+ * table has always shown; the capability indices next to it deliberately use
31
+ * the sample form (n−1) and say so. Two-pass, not `Σx² − n·x̄²`: the moments
32
+ * form loses most of its significant digits when the variance is small beside
33
+ * the mean, which is the normal shape of a passing parametric test. */
34
+ export declare function describeSorted(sorted: ArrayLike<number>): DescriptiveStats;
10
35
  //# sourceMappingURL=math.d.ts.map
@@ -1 +1 @@
1
- export function errorFunction(n){const a=n<0?-1:1,t=Math.abs(n),o=.254829592,r=-.284496736,s=1.421413741,i=-1.453152027,e=1.061405429,c=1/(1+.3275911*t),h=1-((((e*c+i)*c+s)*c+r)*c+o)*c*Math.exp(-t*t);return a*h}export function normalCdf(n){return .5*(1+errorFunction(n/Math.sqrt(2)))}export function quantile(n,a){if(n.length===0)return NaN;const t=a*(n.length-1),o=Math.floor(t),r=Math.ceil(t);return n[o]+(n[r]-n[o])*(t-o)}
1
+ export function errorFunction(n){const t=n<0?-1:1,c=Math.abs(n),a=.254829592,i=-.284496736,o=1.421413741,r=-1.453152027,s=1.061405429,e=1/(1+.3275911*c),u=1-((((s*e+r)*e+o)*e+i)*e+a)*e*Math.exp(-c*c);return t*u}export function normalCdf(n){return .5*(1+errorFunction(n/Math.sqrt(2)))}export function quantile(n,t){if(n.length===0)return NaN;const c=t*(n.length-1),a=Math.floor(c),i=Math.ceil(c);return n[a]+(n[i]-n[a])*(c-a)}export function describeSorted(n){const t=n.length;let c=0;for(let o=0;o<t;o++)c+=n[o];const a=c/t;let i=0;for(let o=0;o<t;o++)i+=(n[o]-a)**2;return{min:n[0],max:n[t-1],mean:a,count:t,stddev:Math.sqrt(i/t),median:quantile(n,.5),q1:quantile(n,.25),q3:quantile(n,.75)}}
@@ -1,4 +1,4 @@
1
- import{waferDisplayLabel as L}from"../core/waferLabel.js";import{binPassSets as we,binPassSetsByWafer as $e,mergeBinDefs as I}from"../renderer/binColors.js";import{itemPassBins as R,passBinsLabel as xe}from"../core/passBins.js";import{isParametricTest as De}from"../renderer/buildWaferMap.js";import{buildRingRegions as j,buildQuadrantRegions as O,buildRegionYieldData as ve}from"./regions.js";import{openHtmlReport as Be}from"./renderFindingsReport.js";import{analyzeWaferLot as Te}from"./analyzeWaferLot.js";import{computeFunctionalYield as Fe,analyzeWaferMap as Pe}from"./analyzeWaferMap.js";import{mergeTestDefs as Ye}from"./mergeTestDefs.js";import{sortBinsForDisplay as z}from"./binPareto.js";import{buildFacetTable as We,facetValueOf as Me,FACET_NONE_VALUE as Ce}from"./facets.js";import{visibleFindings as A}from"./filterFindings.js";import{buildYieldDataCombined as Re}from"./yield.js";import{describeWaferPopulation as G,populationLabel as Ne}from"./population.js";import{buildTestPassRateData as Ee,hasJudgeableTests as ke,poolFunctionalYield as He}from"./testPassRate.js";import{buildCapabilityData as Le}from"./capability.js";import{fmt as F}from"../renderer/fmt.js";import{getDieKey as Ie,isPositionedDie as je,diePassStatus as U}from"../core/dies.js";import{formatFindingDelta as Oe,formatFindingCoverage as ze,formatFindingTooltip as Ae,renderMetadataSection as V,renderMetricGrid as _,renderSection as x,renderSeverityBadge as Ge,renderTable as B,reportStyles as N}from"./reportHtml.js";import{escHtml as h}from"../core/utils.js";function C(t,o){return o===0?"\u2014":`${(t/o*100).toFixed(1)}%`}function Ue(t){if(!t)return"";const o=t.lot??t.lotId,s=t.wafer??t.waferId,n=[o,s].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(h).join(" \xB7 ")}`:""}function K(t,o,s,n,r=[1]){const e=new Map;if(n)for(const[a,c]of Object.entries(n))e.set(Number(a),c);else for(const a of t){if(a.partial||a.edgeExcluded)continue;const c=s==="hard"?a.hbin:a.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const i=[...e.values()].reduce((a,c)=>a+c,0),l=o?new Map(o.map(a=>[a.bin,a])):null,d=z(e.entries(),we(t,r)[s]).map(([a,c])=>{const p=l?.get(a)?.name??"\u2014";return[String(a),p,String(c),C(c,i)]});return x(s==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",B(["Bin","Name","Count","%"],d,{className:"compact"}))}function Q(t,o,s,n){const r=new Set(n),e=new Map(s.map(d=>[Ie(d),d]));if(!s.some(d=>d.hbin!=null||d.sbin!=null)||!o.length)return"";const l=[];for(const d of o){let f=0,a=0;for(const c of d.dieKeys){const b=e.get(c);if(!b||b.partial||b.edgeExcluded)continue;const p=U(b,r);p!==void 0&&(a++,p&&f++)}a&&l.push([d.label,String(f),String(a),C(f,a)])}return l.length?x(t,B(["Region","Pass","Total","Yield"],l,{className:"compact"})):""}function q(t,o,s){if(!o.length)return"";const n=t.filter(i=>!i.partial&&!i.edgeExcluded),r=new Map((s??[]).map(i=>[i.testNumber,i])),e=[];for(const i of o.filter(De)){const l=i.testNumber,d=i.unit||void 0,f=r.get(l);let a,c,b;if(f)({min:a,max:c,mean:b}=f);else{const p=n.map(w=>w.testValues?.[l]).filter(w=>w!==void 0&&isFinite(w));if(!p.length)continue;p.sort((w,y)=>w-y),a=p[0],c=p[p.length-1],b=p.reduce((w,y)=>w+y,0)/p.length}e.push([h(i.name),F(a,d),F(b,d),F(c,d)])}return e.length?x("Test Values",B(["Test","Min","Mean","Max"],e,{className:"compact"})):""}function J(t,o,s){const n=s??Fe(t,o);if(!n?.length)return"";const r=n.map(e=>[h(e.label),String(e.totalDies),String(e.passDies),String(e.failDies),e.passRatePercent!==null?`${e.passRatePercent.toFixed(1)}%`:"\u2014"]);return x("Functional Tests",B(["Test","N","Pass","Fail","Pass Rate"],r,{className:"compact"}))}function X(t,o){const s=Le(t,o);if(!s.length)return"";const n=e=>e===null?"\u2014":e.toFixed(2),r=s.map(e=>[h(e.label),e.hasSpec?`${F(e.lsl,e.unit)} \u2013 ${F(e.usl,e.unit)}`:"\u2014",F(e.mean,e.unit),n(e.cp),n(e.cpk),n(e.pp),n(e.ppk)]);return x("Process Capability",B(["Test","Spec (LSL\u2013USL)","Mean","Cp","Cpk","Pp","Ppk"],r,{className:"compact"}))}function Z(t,o){const s=t.map(r=>`<tr title="${h(Ae(r))}">
1
+ import{waferDisplayLabel as L}from"../core/waferLabel.js";import{binPassSets as we,binPassSetsByWafer as $e,mergeBinDefs as I}from"../renderer/binColors.js";import{itemPassBins as R,passBinsLabel as xe}from"../core/passBins.js";import{isParametricTest as De}from"../renderer/buildWaferMap.js";import{buildRingRegions as j,buildQuadrantRegions as O,buildRegionYieldData as ve}from"./regions.js";import{openHtmlReport as Be}from"./renderFindingsReport.js";import{analyzeWaferLot as Te}from"./analyzeWaferLot.js";import{computeFunctionalYield as Fe,analyzeWaferMap as Pe}from"./analyzeWaferMap.js";import{mergeTestDefs as Ye}from"./mergeTestDefs.js";import{sortBinsForDisplay as z}from"./binPareto.js";import{buildFacetTable as We,facetValueOf as Me,FACET_NONE_VALUE as Ce}from"./facets.js";import{visibleFindings as A}from"./filterFindings.js";import{buildYieldDataCombined as Re}from"./yield.js";import{describeWaferPopulation as G,populationLabel as Ne}from"./population.js";import{buildTestPassRateData as Ee,hasJudgeableTests as ke,poolFunctionalYield as He}from"./testPassRate.js";import{buildCapabilityData as Le}from"./capability.js";import{fmt as P}from"../renderer/fmt.js";import{getDieKey as Ie,isPositionedDie as je,diePassStatus as U}from"../core/dies.js";import{formatFindingDelta as Oe,formatFindingCoverage as ze,formatFindingTooltip as Ae,renderMetadataSection as V,renderMetricGrid as _,renderSection as x,renderSeverityBadge as Ge,renderTable as B,reportStyles as N}from"./reportHtml.js";import{escHtml as h}from"../core/utils.js";function C(t,o){return o===0?"\u2014":`${(t/o*100).toFixed(1)}%`}function Ue(t){if(!t)return"";const o=t.lot??t.lotId,s=t.wafer??t.waferId,n=[o,s].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(h).join(" \xB7 ")}`:""}function K(t,o,s,n,r=[1]){const e=new Map;if(n)for(const[a,c]of Object.entries(n))e.set(Number(a),c);else for(const a of t){if(a.partial||a.edgeExcluded)continue;const c=s==="hard"?a.hbin:a.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const i=[...e.values()].reduce((a,c)=>a+c,0),l=o?new Map(o.map(a=>[a.bin,a])):null,d=z(e.entries(),we(t,r)[s]).map(([a,c])=>{const p=l?.get(a)?.name??"\u2014";return[String(a),p,String(c),C(c,i)]});return x(s==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",B(["Bin","Name","Count","%"],d,{className:"compact"}))}function Q(t,o,s,n){const r=new Set(n),e=new Map(s.map(d=>[Ie(d),d]));if(!s.some(d=>d.hbin!=null||d.sbin!=null)||!o.length)return"";const l=[];for(const d of o){let f=0,a=0;for(const c of d.dieKeys){const b=e.get(c);if(!b||b.partial||b.edgeExcluded)continue;const p=U(b,r);p!==void 0&&(a++,p&&f++)}a&&l.push([d.label,String(f),String(a),C(f,a)])}return l.length?x(t,B(["Region","Pass","Total","Yield"],l,{className:"compact"})):""}function q(t,o,s){if(!o.length)return"";const n=t.filter(i=>!i.partial&&!i.edgeExcluded),r=new Map((s??[]).map(i=>[i.testNumber,i])),e=[];for(const i of o.filter(De)){const l=i.testNumber,d=i.unit||void 0,f=r.get(l);let a,c,b;if(f)({min:a,max:c,mean:b}=f);else{const p=n.map(w=>w.testValues?.[l]).filter(w=>w!==void 0&&isFinite(w));if(!p.length)continue;p.sort((w,y)=>w-y),a=p[0],c=p[p.length-1],b=p.reduce((w,y)=>w+y,0)/p.length}e.push([h(i.name),P(a,d),P(b,d),P(c,d)])}return e.length?x("Test Values",B(["Test","Min","Mean","Max"],e,{className:"compact"})):""}function J(t,o,s){const n=s??Fe(t,o);if(!n?.length)return"";const r=n.map(e=>[h(e.label),String(e.totalDies),String(e.passDies),String(e.failDies),e.passRatePercent!==null?`${e.passRatePercent.toFixed(1)}%`:"\u2014"]);return x("Functional Tests",B(["Test","N","Pass","Fail","Pass Rate"],r,{className:"compact"}))}function X(t,o){const s=Le(t,o);if(!s.length)return"";const n=e=>e===null?"\u2014":e.toFixed(2),r=s.map(e=>[h(e.label),e.hasSpec?`${P(e.lsl,e.unit)} \u2013 ${P(e.usl,e.unit)}`:"\u2014",P(e.mean,e.unit),n(e.cp),n(e.cpk),n(e.pp),n(e.ppk)]);return x("Process Capability",B(["Test","Spec (LSL\u2013USL)","Mean","Cp","Cpk","Pp","Ppk"],r,{className:"compact"}))}function Z(t,o){const s=t.map(r=>`<tr title="${h(Ae(r))}">
2
2
  <td class="tight">${Ge(r.severity)}</td>
3
3
  <td class="tight">${h(r.comparison.left)}</td>
4
4
  <td>${h(r.variable.label)}</td>
@@ -7,7 +7,7 @@ import{waferDisplayLabel as L}from"../core/waferLabel.js";import{binPassSets as
7
7
  </tr>`).join(`
8
8
  `);return`<table class="report-table findings-table compact"><thead><tr>
9
9
  <th>Severity</th><th>Region</th><th>Metric</th><th class="numeric">Delta</th><th class="numeric">${o!==void 0?"Wafers":"N (region/rest)"}</th>
10
- </tr></thead><tbody>${s}</tbody></table>`}function ee(t,o){const s=A(t);return s.length?x("Findings",Z(s,o)):""}export function renderSummaryReportHtml(t,o={}){const{wafer:s,dies:n,yieldSummary:r,dataCoverage:e,hbinDefs:i,sbinDefs:l,testDefs:d=[],statsSummary:f,passBins:a=[1],ringCount:c=4}=t,b=s.metadata,p=Ue(b),w=o.title??`Wafer Summary${p}`,y=new Date().toLocaleString(),u=n.some(Y=>Y.hbin!=null),g=n.some(Y=>Y.sbin!=null),S=n.filter(je),D=j(S,s,c),$=O(S,s,c),v=[{label:"Total dies",value:String(r.totalDies)},{label:"Filled dies",value:String(e.filledDies),hint:`${C(e.filledDies,e.totalDies)} fill`},{label:"Pass dies",value:String(r.passDies)},{label:"Fail dies",value:String(r.failDies)},...r.partialDies>0?[{label:"Partial",value:String(r.partialDies)}]:[],...r.yieldPercent!==null?[{label:`Yield (pass: ${xe([a])})`,value:`${r.yieldPercent.toFixed(1)}%`}]:[],...r.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(r.edgeExcludedDies)}]:[]],P=[V([{metadata:s.metadata}]),x("Summary",[_(v)].filter(Boolean).join(`
10
+ </tr></thead><tbody>${s}</tbody></table>`}function ee(t,o){const s=A(t);return s.length?x("Findings",Z(s,o)):""}export function renderSummaryReportHtml(t,o={}){const{wafer:s,dies:n,yieldSummary:r,dataCoverage:e,hbinDefs:i,sbinDefs:l,testDefs:d=[],statsSummary:f,passBins:a=[1],ringCount:c=4}=t,b=s.metadata,p=Ue(b),w=o.title??`Wafer Summary${p}`,y=new Date().toLocaleString(),u=n.some(W=>W.hbin!=null),g=n.some(W=>W.sbin!=null),S=n.filter(je),D=j(S,s,c),$=O(S,s,c),v=[{label:"Total dies",value:String(r.totalDies)},{label:"Filled dies",value:String(e.filledDies),hint:`${C(e.filledDies,e.totalDies)} fill`},{label:"Pass dies",value:String(r.passDies)},{label:"Fail dies",value:String(r.failDies)},...r.partialDies>0?[{label:"Partial",value:String(r.partialDies)}]:[],...r.yieldPercent!==null?[{label:`Yield (pass: ${xe([a])})`,value:`${r.yieldPercent.toFixed(1)}%`}]:[],...r.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(r.edgeExcludedDies)}]:[]],Y=[V([{metadata:s.metadata}]),x("Summary",[_(v)].filter(Boolean).join(`
11
11
  `)),u?K(n,i,"hard",f?.stats.hardBinCounts,a):g?K(n,l,"soft",f?.stats.softBinCounts,a):"",Q("Ring Yield",D,n,a),Q("Quadrant Yield",$,n,a),q(n,d,f?.stats.perTestStats),J(n,d,f?.stats.functionalYield),X([{dies:n}],d),f?ee(f.findings):""].filter(Boolean).join(`
12
12
  `);return`<!DOCTYPE html>
13
13
  <html lang="en">
@@ -24,14 +24,14 @@ ${N()}
24
24
  <h1>${h(w)}</h1>
25
25
  <p class="report-subtitle">Generated ${h(y)}</p>
26
26
  </header>
27
- ${P}
27
+ ${Y}
28
28
  <p class="footer">Generated ${h(y)}</p>
29
29
  </main>
30
30
  </body>
31
31
  </html>`}function Ve(t,o){const s=t.perWafer.map(n=>{const r=L(o[n.waferIndex],n.waferIndex),e=n.summary.stats.yieldPercent;return[r,e!==null?`${e.toFixed(1)}%`:"N/A"]});return B(["Wafer","Yield"],s,{className:"compact"})}function _e(t){const o=[];for(const e of t.perWafer){const i=A(e.summary.findings);if(!i.length)continue;const l=e.summary.wafer?.waferId??`Wafer ${e.waferIndex+1}`;o.push(`<h3 class="report-subheading">${h(l)}</h3>`+Z(i))}if(!o.length)return"";const s=o.length,n=t.perWafer.length,r=`<p class="report-note">${s} of ${n} wafer${n===1?"":"s"} have per-wafer findings; the rest had none.</p>`;return x("Findings by Wafer",r+o.join(`
32
32
  `))}const Ke=3;function Qe(t,o,s){const n=t.map(d=>({metadata:d.wafer?.metadata})),r=We(n,{facetableOnly:!0}).filter(d=>d.splittable);if(!r.length)return"";const e=r.slice(0,Ke),i=[];for(const d of e){const f=new Map;t.forEach((u,g)=>{const S=Me(u.wafer?.metadata,d.key)??Ce;(f.get(S)??f.set(S,[]).get(S)).push(g)});const a=[...f.entries()].map(([u,g])=>({key:u,items:g.map(S=>t[S])}));if(a.length<2)continue;const c=a.map(u=>[h(u.key),h(u.items.map(g=>g.label).join(", "))]),b=Re(a.map(u=>({key:u.key,items:u.items.map(g=>({label:g.label,dies:g.dies,passBins:R(g,s)}))})),s).map(u=>[h(u.label),String(u.itemCount),`${u.percent.toFixed(1)}%`]),p=[B([d.label,"Wafers"],c,{className:"compact"}),B([d.label,"Wafers","Yield"],b,{className:"compact"})],w=t.flatMap(u=>u.dies??[]);let y=0;for(const u of["spec","testFlag","functional"]){if(!ke(o,u,w))continue;const g=Ee(a.map($=>({key:$.key,items:$.items.map(v=>({dies:v.dies,testSpecYield:v.statsSummary?.stats.testSpecYield,functionalYield:v.statsSummary?.stats.functionalYield}))})),o,u);if(!g.rows.length)continue;const S=g.rows.map($=>[h($.label),...$.byGroup.map(v=>v.passRatePercent===null?"\u2014":`${v.passRatePercent.toFixed(1)}%`),$.overall.passRatePercent===null?"\u2014":`${$.overall.passRatePercent.toFixed(1)}%`]),D=u==="spec"?"Pass rate \xB7 spec limits":u==="testFlag"?"Pass rate \xB7 tester flag":"Pass rate \xB7 functional";g.disagreementDies&&(y=g.disagreementDies),p.push(`<h4 class="report-subheading">${h(D)}</h4>`+B(["Test",...g.groups,"All"],S,{className:"compact"}))}y&&p.push(`<p class="report-note">${y.toLocaleString()} dies judged differently by limits and tester flag.</p>`),i.push(`<h3 class="report-subheading">${h(d.label)}</h3>${p.join(`
33
33
  `)}`)}if(!i.length)return"";const l=r.length>e.length?`<p class="report-note">${r.length-e.length} further splittable field(s) not shown.</p>`:"";return x("Split Comparison",i.join(`
34
- `)+l)}function te(t,o,s,n,r){const e=new Map,i=s==="hard"?"hardBinCounts":"softBinCounts";if(n?.length&&n.every(a=>a.stats[i]!==void 0))for(const a of n)for(const[c,b]of Object.entries(a.stats[i])){const p=Number(c);e.set(p,(e.get(p)??0)+b)}else for(const a of t){if(a.partial||a.edgeExcluded)continue;const c=s==="hard"?a.hbin:a.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const l=[...e.values()].reduce((a,c)=>a+c,0),d=o?new Map(o.map(a=>[a.bin,a])):null,f=z(e.entries(),$e(r)[s]).map(([a,c])=>{const b=d?.get(a);return[b?.name?`Bin ${a} \xB7 ${b.name} (${c})`:`Bin ${a} (${c})`,C(c,l)]});return x(s==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",B(["Bin","Yield"],f,{className:"compact"}))}function ne(t,o,s,n,r,e){const i=ve(s,n,r,l=>e[l],o).map(l=>[l.label,`${l.yieldPercent.toFixed(1)}%`]);return i.length?x(t,B(["Region","Yield"],i,{className:"compact"})):""}function qe(t,o,s){let n;if(s?.length&&s.every(r=>r.stats.perTestStats!==void 0)){const r=new Map;for(const e of s)for(const i of e.stats.perTestStats??[]){const l=r.get(i.testNumber);l?(l.n+=i.count,l.sum+=i.mean*i.count,l.min=Math.min(l.min,i.min),l.max=Math.max(l.max,i.max)):r.set(i.testNumber,{n:i.count,sum:i.mean*i.count,min:i.min,max:i.max})}n=[...r.entries()].map(([e,i])=>({testNumber:e,min:i.min,max:i.max,mean:i.sum/i.n}))}return q(t,o,n)}function Je(t,o,s){const n=He(s);return J(t,o,n)}const E=["lot","product","testProgram","temperature"];function Xe(t){const o=new Map(E.map(e=>[e,new Set]));for(const e of t)for(const i of E){const l=e.wafer?.metadata?.[i];l!=null&&l!==""&&o.get(i).add(String(l))}const s=E.filter(e=>o.get(e).size>1);if(s.length===0)return[{label:"",items:t,varying:s}];const n=new Map,r=[];for(const e of t){const i=s.map(l=>String(e.wafer?.metadata?.[l]??"(none)")).join(" \xB7 ");n.has(i)||(n.set(i,[]),r.push(i)),n.get(i).push(e)}return r.map(e=>({label:e,items:n.get(e),varying:s}))}function se(t,o,s,n,r,e,i){const l=Te(t.map(m=>m.source??m),{perWaferSummaries:t.map(m=>m.statsSummary),...i}),d=[],f=[],a=[],c=[];for(const m of t){const T=m.dies??[];m.wafer&&(d.push(m.wafer),a.push(T),c.push(R(m,r))),T.length&&f.push(...T)}const b=t.map(m=>({dies:m.dies??[],passBins:R(m,r)})),p=f.some(m=>m.hbin!=null),w=f.some(m=>m.sbin!=null),y=p||w,u=l.perWafer.map(m=>m.summary.stats.yieldPercent).filter(m=>m!==null),g=u.length?u.reduce((m,T)=>m+T,0)/u.length:null;let S=0,D=0,$=0,v=0,P=0;for(const m of b){const T=new Set(m.passBins);for(const W of m.dies){if(S++,W.edgeExcluded&&v++,W.partial&&P++,W.partial||W.edgeExcluded)continue;const H=U(W,T);H!==void 0&&(D++,H&&$++)}}const Y=D-$,k=D>0?$/D*100:null,ie=[{label:"Wafers",value:String(l.stats.waferCount)},{label:"Total dies",value:String(S)},...y?[{label:"Good dies",value:String($)},{label:"Bad dies",value:String(Y)}]:[],...v>0?[{label:"Edge excluded",value:String(v)}]:[],...P>0?[{label:"Partial dies",value:String(P)}]:[],...g!==null?[{label:"Mean wafer yield",value:`${g.toFixed(1)}%`}]:[],...k!==null?[{label:"Total yield",value:`${k.toFixed(1)}%`}]:[]],oe=G(l.perWafer.map(m=>m.summary.wafer)),re=x(oe.lotId!==void 0?"Lot Summary":"Summary",_(ie)),le=V(t.map(m=>({metadata:m.wafer?.metadata}))),ce=x("Per-Wafer Yield",Ve(l,t)),de=Qe(t,n,r),M=l.perWafer.map(m=>m.summary),ue=y?p?te(f,o,"hard",M,b):te(f,s,"soft",M,b):"",fe=y&&d.length?ne("Ring Yield (All Wafers)",j,a,d,e,c):"",me=y&&d.length?ne("Quadrant Yield (All Wafers)",O,a,d,e,c):"",pe=n.length?qe(f,n,M):"",ge=n.length?Je(f,n,M):"",he=n.length?X(t,n):"",be=l.findings.length?ee(l.findings,l.stats.waferCount):"",ye=_e(l),Se=[re,le,ce,de,ue,fe,me,pe,ge,he,be,ye].filter(Boolean).join(`
34
+ `)+l)}function te(t,o,s,n,r){const e=new Map,i=s==="hard"?"hardBinCounts":"softBinCounts";if(n?.length&&n.every(a=>a.stats[i]!==void 0))for(const a of n)for(const[c,b]of Object.entries(a.stats[i])){const p=Number(c);e.set(p,(e.get(p)??0)+b)}else for(const a of t){if(a.partial||a.edgeExcluded)continue;const c=s==="hard"?a.hbin:a.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const l=[...e.values()].reduce((a,c)=>a+c,0),d=o?new Map(o.map(a=>[a.bin,a])):null,f=z(e.entries(),$e(r)[s]).map(([a,c])=>{const b=d?.get(a);return[b?.name?`Bin ${a} \xB7 ${b.name} (${c})`:`Bin ${a} (${c})`,C(c,l)]});return x(s==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",B(["Bin","Yield"],f,{className:"compact"}))}function ne(t,o,s,n,r,e){const i=ve(s,n,r,l=>e[l],o).map(l=>[l.label,`${l.yieldPercent.toFixed(1)}%`]);return i.length?x(t,B(["Region","Yield"],i,{className:"compact"})):""}function qe(t,o,s){let n;if(s?.length&&s.every(r=>r.stats.perTestStats!==void 0)){const r=new Map;for(const e of s)for(const i of e.stats.perTestStats??[]){const l=r.get(i.testNumber);l?(l.n+=i.count,l.sum+=i.mean*i.count,l.min=Math.min(l.min,i.min),l.max=Math.max(l.max,i.max)):r.set(i.testNumber,{n:i.count,sum:i.mean*i.count,min:i.min,max:i.max})}n=[...r.entries()].map(([e,i])=>({testNumber:e,min:i.min,max:i.max,mean:i.sum/i.n}))}return q(t,o,n)}function Je(t,o,s){const n=He(s);return J(t,o,n)}const E=["lot","product","testProgram","temperature"];function Xe(t){const o=new Map(E.map(e=>[e,new Set]));for(const e of t)for(const i of E){const l=e.wafer?.metadata?.[i];l!=null&&l!==""&&o.get(i).add(String(l))}const s=E.filter(e=>o.get(e).size>1);if(s.length===0)return[{label:"",items:t,varying:s}];const n=new Map,r=[];for(const e of t){const i=s.map(l=>String(e.wafer?.metadata?.[l]??"(none)")).join(" \xB7 ");n.has(i)||(n.set(i,[]),r.push(i)),n.get(i).push(e)}return r.map(e=>({label:e,items:n.get(e),varying:s}))}function se(t,o,s,n,r,e,i){const l=Te(t.map(m=>m.source??m),{perWaferSummaries:t.map(m=>m.statsSummary),...i}),d=[],f=[],a=[],c=[];for(const m of t){const F=m.dies??[];m.wafer&&(d.push(m.wafer),a.push(F),c.push(R(m,r)));for(const T of F)f.push(T)}const b=t.map(m=>({dies:m.dies??[],passBins:R(m,r)})),p=f.some(m=>m.hbin!=null),w=f.some(m=>m.sbin!=null),y=p||w,u=l.perWafer.map(m=>m.summary.stats.yieldPercent).filter(m=>m!==null),g=u.length?u.reduce((m,F)=>m+F,0)/u.length:null;let S=0,D=0,$=0,v=0,Y=0;for(const m of b){const F=new Set(m.passBins);for(const T of m.dies){if(S++,T.edgeExcluded&&v++,T.partial&&Y++,T.partial||T.edgeExcluded)continue;const H=U(T,F);H!==void 0&&(D++,H&&$++)}}const W=D-$,k=D>0?$/D*100:null,ie=[{label:"Wafers",value:String(l.stats.waferCount)},{label:"Total dies",value:String(S)},...y?[{label:"Good dies",value:String($)},{label:"Bad dies",value:String(W)}]:[],...v>0?[{label:"Edge excluded",value:String(v)}]:[],...Y>0?[{label:"Partial dies",value:String(Y)}]:[],...g!==null?[{label:"Mean wafer yield",value:`${g.toFixed(1)}%`}]:[],...k!==null?[{label:"Total yield",value:`${k.toFixed(1)}%`}]:[]],oe=G(l.perWafer.map(m=>m.summary.wafer)),re=x(oe.lotId!==void 0?"Lot Summary":"Summary",_(ie)),le=V(t.map(m=>({metadata:m.wafer?.metadata}))),ce=x("Per-Wafer Yield",Ve(l,t)),de=Qe(t,n,r),M=l.perWafer.map(m=>m.summary),ue=y?p?te(f,o,"hard",M,b):te(f,s,"soft",M,b):"",fe=y&&d.length?ne("Ring Yield (All Wafers)",j,a,d,e,c):"",me=y&&d.length?ne("Quadrant Yield (All Wafers)",O,a,d,e,c):"",pe=n.length?qe(f,n,M):"",ge=n.length?Je(f,n,M):"",he=n.length?X(t,n):"",be=l.findings.length?ee(l.findings,l.stats.waferCount):"",ye=_e(l),Se=[re,le,ce,de,ue,fe,me,pe,ge,he,be,ye].filter(Boolean).join(`
35
35
  `);return{lotSummary:l,sections:Se}}function ae(t,o,s){return`<main class="report">
36
36
  <header class="report-header">
37
37
  <h1>${h(t)}</h1>
package/llms.txt CHANGED
@@ -1,7 +1,10 @@
1
1
  # wafermap
2
2
 
3
3
  Browser-first wafer map visualization toolkit for semiconductor test data.
4
- Primary entry point: build wafer models with `buildWaferMap()`, then render with a canvas.
4
+ Primary path: `buildWaferMap()` to build the map once, then `renderWaferMap()` (one wafer)
5
+ or `renderWaferGallery()` (several) to render it, and `analyzeWaferMap()` for statistics
6
+ and findings. The renderers carry their own toolbar, summary panel, Insights charts and
7
+ reports — the low-level pipeline below them is deprecated and removed in 0.31.0.
5
8
 
6
9
  BEFORE INTEGRATING: tsmap (https://github.com/wafertools/tsmap) is a finished, free,
7
10
  MIT-licensed desktop and browser application built on this library — it opens STDF,
@@ -20,11 +23,11 @@ where repo-relative paths do not resolve.
20
23
  - [Examples](https://wafertools.github.io/wafermap/examples/) - 20 runnable demo pages, also downloadable at https://wafertools.github.io/wafermap/wafermap-examples.zip to run offline.
21
24
 
22
25
  ## Public package entry points
23
- - `@wafertools/wafermap`
24
- - `@wafertools/wafermap/render`
25
- - `@wafertools/wafermap/stats`
26
- - `@wafertools/wafermap/worker`
27
- - `@wafertools/wafermap/worker-script`
26
+ - `@wafertools/wafermap` - `buildWaferMap()`, geometry, colour-scheme registries. Pure, no DOM.
27
+ - `@wafertools/wafermap/render` - `renderWaferMap()`, `renderWaferGallery()`. Needs the DOM.
28
+ - `@wafertools/wafermap/stats` - `analyzeWaferMap()`, `analyzeWaferLot()`, HTML reports. Pure.
29
+ - `@wafertools/wafermap/worker` - `createWafermapWorker()` for off-main-thread builds.
30
+ - `@wafertools/wafermap/worker-script` - the worker script itself.
28
31
 
29
32
  ## Important notes
30
33
  - `x` and `y` in `DieResult` are die grid positions (prober step integers), not millimetres.
@@ -33,7 +36,9 @@ where repo-relative paths do not resolve.
33
36
  - All coordinates shown to the user (axis ticks, tooltips) must be original die grid positions (`die.x` / `die.y`), not post-transform display coordinates.
34
37
  - `buildWaferMap()` is pure and server-safe; renderers require the DOM.
35
38
  - Recommended path for most users: `buildWaferMap()` + `renderWaferMap()`.
36
- - `passBins` sets both the yield number and the wording of its label — do not assume `[1]`.
39
+ - Bins, test values and verdicts must be numbers/booleans. A parser that leaves `hbin` as `"1"` builds a map where those dies count as fails — convert with `Number()` at parse time.
40
+ - `passBins` and `ringCount` are set once on `buildWaferMap()` and travel on the result. Neither is an option on `analyzeWaferMap`, `analyzeWaferLot`, `renderWaferMap` or `renderWaferGallery`. `passBins` sets both the yield number and the wording of its label — do not assume `[1]`.
41
+ - Statistics surfaces are built in: pass `statsSummary` for the Summary panel and `insights: { enabled: true }` for the chart suite. Do not rebuild those charts from the deprecated chart-data builders.
37
42
  - `activeTest` takes a test number (e.g. `1050`), not a positional index.
38
43
  - Functional tests (`testType: 'F'`) have no value; read verdicts only via `getTestPassStatus()`. A missing verdict is no-data, never a fail.
39
44
  - `PlotMode` values are camelCase (`'hardBin'`, `'stackedValues'`), never snake_case.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafertools/wafermap",
3
- "version": "0.30.1",
3
+ "version": "0.30.2",
4
4
  "description": "Interactive wafer map visualization and yield analysis for semiconductor test data. Hard bins, soft bins, test values, retests, edge exclusion, reticle overlays, spatial statistics, failure clustering, and lot-level trend analysis — pure ES modules, no runtime dependencies, no server required.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -75,7 +75,7 @@
75
75
  "build": "node scripts/build-user-guide.mjs && node scripts/sync-icons.mjs && node scripts/sync-version.mjs && rm -rf dist && tsc -p tsconfig.build.json",
76
76
  "build:guide": "node scripts/build-user-guide.mjs",
77
77
  "clean": "rm -rf dist",
78
- "check": "node scripts/build-user-guide.mjs && node scripts/sync-version.mjs && tsc --noEmit -p tsconfig.json && node scripts/check-examples-manifest.mjs && node scripts/build-agents-page.mjs --check && node scripts/check-agents-guide.mjs && node scripts/check-toolbar-docs.mjs && node scripts/check-doc-links.mjs && node scripts/check-quickstart-snippet.mjs && node scripts/check-overlay-conventions.mjs && node scripts/check-changelog.mjs && node scripts/help.mjs --check && node scripts/check-api-claims.mjs && node scripts/check-style-scales.mjs packages/canvas-adapter && node scripts/check-clones.mjs packages",
78
+ "check": "node scripts/build-user-guide.mjs && node scripts/sync-version.mjs && tsc --noEmit -p tsconfig.json && node scripts/check-examples-manifest.mjs && node scripts/build-agents-page.mjs --check && node scripts/check-agents-guide.mjs && node scripts/check-toolbar-docs.mjs && node scripts/check-doc-links.mjs && node scripts/check-quickstart-snippet.mjs && node scripts/check-overlay-conventions.mjs && node scripts/check-changelog.mjs && node scripts/help.mjs --check && node scripts/check-api-claims.mjs && node scripts/check-style-scales.mjs packages/canvas-adapter && node scripts/check-spread-limits.mjs && node scripts/check-clones.mjs packages",
79
79
  "build:examples-index": "node scripts/build-examples-index.mjs",
80
80
  "check:drift": "node scripts/check-drift.mjs",
81
81
  "dev": "rm -rf docs/dist && cp -r dist docs/dist && ./.venv/bin/zensical serve --dev-addr localhost:8001",