@wafertools/wafermap 0.30.1 → 0.30.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/AGENTS.md +71 -11
  2. package/CHANGELOG.md +464 -8
  3. package/README.md +6 -3
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +48 -0
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +4 -1
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +4 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +81 -7
  12. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  14. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  15. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  18. package/dist/packages/canvas-adapter/charts/sweep.d.ts +31 -0
  19. package/dist/packages/canvas-adapter/charts/sweep.js +1 -0
  20. package/dist/packages/canvas-adapter/charts/trend.d.ts +3 -1
  21. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  22. package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
  23. package/dist/packages/canvas-adapter/chunked.js +1 -0
  24. package/dist/packages/canvas-adapter/dieList.js +2 -2
  25. package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
  26. package/dist/packages/canvas-adapter/drilldown.js +1 -0
  27. package/dist/packages/canvas-adapter/icons.js +1 -1
  28. package/dist/packages/canvas-adapter/insightsTab.d.ts +55 -3
  29. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  30. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +55 -0
  31. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  32. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  33. package/dist/packages/canvas-adapter/summaryPanel.d.ts +55 -9
  34. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  35. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  36. package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
  37. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  38. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  39. package/dist/packages/canvas-adapter/userGuideHtml.js +126 -3
  40. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  41. package/dist/packages/canvas-adapter/version.js +1 -1
  42. package/dist/packages/canvas-adapter/warnings.js +1 -1
  43. package/dist/packages/core/aggregates.js +1 -1
  44. package/dist/packages/core/utils.d.ts +55 -0
  45. package/dist/packages/core/utils.js +1 -1
  46. package/dist/packages/renderer/axisTicks.d.ts +32 -0
  47. package/dist/packages/renderer/axisTicks.js +1 -0
  48. package/dist/packages/renderer/binColors.d.ts +14 -0
  49. package/dist/packages/renderer/binColors.js +1 -1
  50. package/dist/packages/renderer/buildView.d.ts +11 -37
  51. package/dist/packages/renderer/buildView.js +1 -1
  52. package/dist/packages/renderer/buildWaferMap.d.ts +58 -1
  53. package/dist/packages/renderer/buildWaferMap.js +1 -1
  54. package/dist/packages/renderer/deprecate.d.ts +13 -5
  55. package/dist/packages/renderer/deprecate.js +1 -1
  56. package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
  57. package/dist/packages/renderer/derivedTests/apply.js +1 -0
  58. package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
  59. package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
  60. package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
  61. package/dist/packages/renderer/derivedTests/parser.js +2 -0
  62. package/dist/packages/renderer/fmt.d.ts +24 -2
  63. package/dist/packages/renderer/fmt.js +1 -1
  64. package/dist/packages/renderer/index.d.ts +1 -0
  65. package/dist/packages/renderer/index.js +1 -1
  66. package/dist/packages/renderer/spec.d.ts +49 -0
  67. package/dist/packages/renderer/spec.js +1 -0
  68. package/dist/packages/renderer/testLabel.d.ts +154 -0
  69. package/dist/packages/renderer/testLabel.js +1 -0
  70. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  71. package/dist/packages/stats/capability.d.ts +60 -0
  72. package/dist/packages/stats/capability.js +1 -1
  73. package/dist/packages/stats/correlation.d.ts +33 -0
  74. package/dist/packages/stats/correlation.js +1 -1
  75. package/dist/packages/stats/deprecated.d.ts +3 -0
  76. package/dist/packages/stats/deprecated.js +1 -1
  77. package/dist/packages/stats/findingsNarrative.js +1 -1
  78. package/dist/packages/stats/index.d.ts +2 -2
  79. package/dist/packages/stats/index.js +1 -1
  80. package/dist/packages/stats/math.d.ts +27 -2
  81. package/dist/packages/stats/math.js +1 -1
  82. package/dist/packages/stats/mergeTestDefs.js +1 -1
  83. package/dist/packages/stats/renderFindingsReport.js +8 -28
  84. package/dist/packages/stats/renderSummaryReport.js +22 -31
  85. package/dist/packages/stats/reportHtml.d.ts +27 -0
  86. package/dist/packages/stats/reportHtml.js +35 -14
  87. package/dist/packages/stats/sweep.d.ts +178 -0
  88. package/dist/packages/stats/sweep.js +1 -0
  89. package/dist/packages/stats/sweepXFromName.d.ts +27 -0
  90. package/dist/packages/stats/sweepXFromName.js +1 -0
  91. package/dist/packages/stats/testPassRate.js +1 -1
  92. package/dist/packages/stats/types.d.ts +28 -1
  93. package/llms.txt +14 -8
  94. package/package.json +2 -2
@@ -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)}
@@ -0,0 +1,154 @@
1
+ import type { TestDef } from './buildWaferMap.js';
2
+ /**
3
+ * The derived-test part of anything that describes a test: a `TestDef`, or a
4
+ * narrower record carried onward from one — a finding's `variable`, a
5
+ * capability row. Structural, so every such shape is read by the same helpers.
6
+ */
7
+ export type DerivedSource = {
8
+ derived?: true;
9
+ expression?: string;
10
+ } | undefined;
11
+ /**
12
+ * Marks a test name as derived rather than measured.
13
+ *
14
+ * A dagger rather than the obvious `ƒ`: in this domain `f` is femto, and a `ƒ`
15
+ * beside a column of `fA`/`fF` units is a genuine misread. The dagger collides
16
+ * with no unit and no operator, and already means "see the note" — which is the
17
+ * contract: wherever the glyph can appear, {@link DERIVED_KEY} appears with it.
18
+ */
19
+ export declare const DERIVED_MARK = "\u2020";
20
+ /**
21
+ * What {@link DERIVED_MARK} means, in words — the key every marked surface shows.
22
+ *
23
+ * Short on purpose: it has to fit the lower-right corner of a map beside the
24
+ * colorbar as well as a legend, and one string everywhere cannot drift. What a
25
+ * value was computed FROM is the expression, which tooltips show alongside it.
26
+ */
27
+ export declare const DERIVED_KEY = "Derived, not measured";
28
+ /**
29
+ * True when this def describes a test computed from other tests rather than
30
+ * measured by the tester. Reads the flag `buildWaferMap` stamps on the defs it
31
+ * admits from `derivedTests` — never infers it from the test number, which is
32
+ * an application-defined identifier carrying no such meaning.
33
+ */
34
+ export declare function isDerivedTest(def: DerivedSource): boolean;
35
+ /**
36
+ * The display name for a test: its `name`, or `"Test <number>"` when it has no
37
+ * def or no name. The single source of that fallback.
38
+ *
39
+ * `testNumber` is passed separately so a caller holding only a number and a
40
+ * possibly-missing def gets the same string as one holding a complete def — the
41
+ * shape every previous copy of this rule had.
42
+ *
43
+ * Typed to the structural minimum rather than `TestDef` so the chart test
44
+ * picker's own `TestSelectItem` (which carries only a number and an optional
45
+ * name) uses the same fallback as everything else instead of keeping its copy.
46
+ */
47
+ export declare function testLabel(def: {
48
+ name?: string;
49
+ } | undefined, testNumber: number): string;
50
+ /**
51
+ * {@link testLabel} with the derived-test marker in front when the def is
52
+ * derived — `"† On/Off Ratio"`. The form every surface uses to name a test: the
53
+ * map title, tooltips, findings, tables, pickers and menus.
54
+ *
55
+ * The marker always PRECEDES the name, everywhere (Paul, 2026-09-23). In a list
56
+ * the eye runs down the left edge, so leading marks form a column and a derived
57
+ * test is found at a glance, where trailing ones land at a different x on every
58
+ * row; and truncating a long name can never cut the marker off. One position on
59
+ * every surface, so a reader learns it once. A unit follows the name as usual —
60
+ * `"† Vth Margin (V)"` — so the marker qualifies the whole test.
61
+ *
62
+ * A list that should keep measured names aligned with marked ones pads them by
63
+ * the marker's width when any entry is derived — {@link DERIVED_LANE_PAD} for
64
+ * plain text, a fixed-width slot where there is DOM or canvas to measure with.
65
+ */
66
+ export declare function markedTestLabel(def: (TestDef | {
67
+ name?: string;
68
+ derived?: true;
69
+ }) | undefined, testNumber: number): string;
70
+ /**
71
+ * Plain-text stand-in for `"† "` in front of a MEASURED name, for a text-only
72
+ * list (a native `<select>`) that holds derived tests too, so every name starts
73
+ * at about the same x. A figure space plus a thin space: close to the width of
74
+ * a dagger and a space in the UI fonts, and invisible. Approximate by nature —
75
+ * a surface that can measure uses a real fixed-width slot instead.
76
+ */
77
+ export declare const DERIVED_LANE_PAD = "\u2007\u2009";
78
+ /**
79
+ * The one-line explanation of a derived test for a tooltip:
80
+ * `"† Derived, not measured: t[1010] / t[1020]"`, or without
81
+ * the expression when none was carried. `undefined` for a measured test.
82
+ *
83
+ * Plain text — the caller escapes it, like every other tooltip line.
84
+ */
85
+ export declare function derivedTestNote(def: DerivedSource): string | undefined;
86
+ /**
87
+ * What a derived test was computed from, as a phrase fit to show a user, or
88
+ * `undefined` for a measured test.
89
+ *
90
+ * Deliberately returns the expression verbatim rather than prettifying it: it
91
+ * is the exact text the application author wrote and can be matched against
92
+ * their own test-program source, which a reformatted version could not be.
93
+ */
94
+ export declare function derivedTestSource(def: DerivedSource): string | undefined;
95
+ /**
96
+ * `{ derived: true, expression }` for a derived test, `{}` for a measured one —
97
+ * spread into any row or record that carries a test onward (a capability row,
98
+ * a summary table entry, a finding's variable, a stacked card's def).
99
+ *
100
+ * The single form of that hand-off. Every place a test is re-described in a
101
+ * narrower shape is a place the flag can be dropped — it was, in
102
+ * `mergeTestDefs`, the Summary panel's table and `poolFunctionalYield`, each a
103
+ * row rebuilt field by field. A shape that spreads this cannot forget the
104
+ * expression while remembering the flag.
105
+ */
106
+ export declare function derivedFields(def: DerivedSource): {
107
+ derived?: true;
108
+ expression?: string;
109
+ };
110
+ /**
111
+ * A label with the derived-test marker taken back out — for a surface that
112
+ * states derivation another way and so must not also carry a bare glyph (a
113
+ * CSV column, the key line that opens with the marker). The inverse of
114
+ * {@link markedTestLabel}, which is the only thing that marks a label, so the
115
+ * two live side by side.
116
+ */
117
+ export declare function unmarkedLabel(label: string): string;
118
+ /** Header of the column a CSV export adds when any row is a derived test. */
119
+ export declare const DERIVED_CSV_HEADER = "Derived from";
120
+ /**
121
+ * The {@link DERIVED_CSV_HEADER} cell for one row. A CSV has nowhere to put a
122
+ * key, so a derived test is stated as data: its expression, or the key's
123
+ * words if none was carried — never blank, which would read as measured.
124
+ * Blank for a measured test.
125
+ */
126
+ export declare function derivedCsvCell(entry: DerivedSource): string;
127
+ /**
128
+ * The key line for a surface that shows several tests at once with no hover to
129
+ * explain each one — a stats table, a findings list, a printed report:
130
+ * `"† Derived, not measured — Leakage Shift: abs(t[1020] - t[1010]); …"`.
131
+ * `undefined` when none of `entries` is derived, so the line appears exactly
132
+ * when a marked name is on the surface.
133
+ *
134
+ * Entries are de-duplicated by name, and the marker is dropped from each name
135
+ * ({@link unmarkedLabel}) since the line already opens with it. Plain text; the
136
+ * caller escapes it.
137
+ */
138
+ export declare function derivedKeyText(entries: ReadonlyArray<{
139
+ label: string;
140
+ derived?: true;
141
+ expression?: string;
142
+ }>): string | undefined;
143
+ /**
144
+ * True when any test in this population is derived — the gate for reserving a
145
+ * marker gutter in a view.
146
+ *
147
+ * Reserving it per view rather than per row is what keeps the marker from
148
+ * shifting content relative to itself: every row of a view indents equally, so
149
+ * the markers align in a column instead of leaving a ragged edge, and a
150
+ * population with no derived tests — the common case — is unchanged to the
151
+ * pixel.
152
+ */
153
+ export declare function hasDerivedTests(defs: readonly TestDef[] | undefined): boolean;
154
+ //# sourceMappingURL=testLabel.d.ts.map
@@ -0,0 +1 @@
1
+ export const DERIVED_MARK="\u2020",DERIVED_KEY="Derived, not measured";export function isDerivedTest(e){return e?.derived===!0}export function testLabel(e,r){const t=e?.name;return t!==void 0&&t!==""?t:`Test ${r}`}export function markedTestLabel(e,r){const t=testLabel(e,r);return e?.derived===!0?`\u2020 ${t}`:t}export const DERIVED_LANE_PAD="\u2007\u2009";export function derivedTestNote(e){if(!isDerivedTest(e))return;const r=derivedTestSource(e);return r?`\u2020 ${DERIVED_KEY}: ${r}`:`\u2020 ${DERIVED_KEY}`}export function derivedTestSource(e){if(!(!isDerivedTest(e)||!e?.expression))return e.expression}export function derivedFields(e){return isDerivedTest(e)?e.expression!==void 0?{derived:!0,expression:e.expression}:{derived:!0}:{}}export function unmarkedLabel(e){return e.startsWith("\u2020 ")?e.slice(2):e}export const DERIVED_CSV_HEADER="Derived from";export function derivedCsvCell(e){return isDerivedTest(e)?derivedTestSource(e)??DERIVED_KEY:""}export function derivedKeyText(e){const r=new Map;for(const n of e){if(!isDerivedTest(n))continue;const i=unmarkedLabel(n.label);r.has(i)||r.set(i,n.expression)}if(r.size===0)return;const t=[...r].map(([n,i])=>i?`${n}: ${i}`:n);return`\u2020 ${DERIVED_KEY} \u2014 ${t.join("; ")}`}export function hasDerivedTests(e){return(e??[]).some(isDerivedTest)}
@@ -1 +1 @@
1
- import{normalizeInput as Ce}from"./normalizeInput.js";import{computeCapability as Ne,computeTestFlagYield as Pe,computeRegionYield as Ve}from"./summaryFigures.js";import{isYieldEligibleDie as te,getDieKey as T,isPositionedDie as ge}from"../core/dies.js";import{getTestPassStatus as me,isParametricTest as O}from"../renderer/buildWaferMap.js";import{buildQuadrantRegions as Le,buildReticlePositionRegions as Ee,buildRingRegions as Ke,buildSectorRegions as Te,buildTestSiteRegions as xe,sectorCompassNames as he,areQuadrantsAdjacent as je,parseRegionKey as q}from"./regions.js";import{buildClusterFindings as Ae}from"./clusterDetection.js";import{classifyPattern as Oe}from"./patternClassification.js";import{normalCdf as pe}from"./math.js";import{quantile as ne}from"./math.js";import{mean as J,clamp01 as be}from"../core/utils.js";const qe={significanceLevel:{min:1e-6,max:1},minimumEffectSize:{min:0,max:1},minimumRelativeEffect:{min:0,max:Number.MAX_SAFE_INTEGER}},ye=[4,8,16,32];function He(s){const a=Object.fromEntries(Object.entries(s).filter(([,t])=>t!==void 0)),i={...x,...a},o=[],e=[];for(const t of["significanceLevel","minimumEffectSize","minimumRelativeEffect"]){const{min:l,max:n}=qe[t],u=i[t];if(typeof u!="number"||!Number.isFinite(u)){e.push(`${t}=${String(u)} is not a finite number (using ${x[t]})`),i[t]=x[t];continue}const f=Math.min(n,Math.max(l,u));f!==u&&(e.push(`${t}=${u} is outside ${l}\u2013${n} (using ${f})`),i[t]=f)}if(ye.includes(i.sectorCount)||(e.push(`sectorCount=${i.sectorCount} is not one of ${ye.join(", ")} (using ${x.sectorCount})`),i.sectorCount=x.sectorCount),e.length){const t=`analyzeWaferMap: ${e.join("; ")}. The analysis ran with the corrected values.`;console.warn(`[wafermap] ${t}`),o.push({code:"analysis-option-corrected",message:t,severity:"warning"})}const c=["passBins","ringCount"].filter(t=>a[t]!==void 0);if(c.length){const t=`analyzeWaferMap: ${c.join(" and ")} ${c.length>1?"are":"is"} no longer an analysis option and was ignored. Set ${c.length>1?"them":"it"} on buildWaferMap; the analysis used the value the map was built with.`;console.warn(`[wafermap] ${t}`),o.push({code:"analysis-option-corrected",message:t,severity:"warning"})}const r=["enableYieldAnalysis","enableHardBinAnalysis","enableSoftBinAnalysis","enableReticlePositionAnalysis","enableTestSiteAnalysis","enableClusterAnalysis","enableAngularAnalysis","enablePatternClassification"].filter(t=>a[t]!==void 0);if(r.length){const t=`analyzeWaferMap: ${r.join(", ")} ${r.length>1?"are":"is"} no longer an option and was ignored: every analysis now runs. Filter the findings (filterFindings) to show fewer.`;console.warn(`[wafermap] ${t}`),o.push({code:"analysis-option-corrected",message:t,severity:"warning"})}return{resolved:i,warnings:o}}const x={ringCount:4,passBins:[1],significanceLevel:.05,minimumEffectSize:.2,minimumRelativeEffect:1,minimumSampleSize:5,enableTestValueAnalysis:!1,computePerTestStats:!1,sectorCount:8,minimumClusterSize:5};function Ie(s,a){const i={...s};return i.minimumClusterSize=Math.max(5,Math.round(a*.003)),i}function Ye(s){return te(s)?s.hbin!==void 0||s.sbin!==void 0||s.testValues!==void 0&&Object.keys(s.testValues).length>0:!1}function _e(s,a,i){if(!s||a)return;const o=(i??[]).filter(e=>O(e)&&(e.limitLow!==void 0||e.limitHigh!==void 0));if(o.length!==0)return e=>{for(const c of o){const r=c.testNumber,t=e.testValues?.[r];if(t!==void 0&&(c.limitLow!==void 0&&t<c.limitLow||c.limitHigh!==void 0&&t>c.limitHigh))return!0}return!1}}function We(s,a,i){const o=new Set,e=new Set,c=new Set,r=new Map,t=new Map;for(const l of s){if(l.testValues)for(const n of Object.keys(l.testValues))o.add(Number(n));if(l.testPass)for(const n of Object.keys(l.testPass))o.add(Number(n));l.hbin!==void 0&&e.add(l.hbin),l.sbin!==void 0&&c.add(l.sbin),te(l)&&(l.hbin!==void 0&&r.set(l.hbin,(r.get(l.hbin)??0)+1),l.sbin!==void 0&&t.set(l.sbin,(t.get(l.sbin)??0)+1))}return{totalDies:s.length,analyzedDies:a,excludedDies:s.length-a,yieldPercent:i,testsConsidered:[...o].sort((l,n)=>l-n),hardBinsConsidered:[...e].sort((l,n)=>l-n),softBinsConsidered:[...c].sort((l,n)=>l-n),...r.size?{hardBinCounts:Object.fromEntries(r)}:{},...t.size?{softBinCounts:Object.fromEntries(t)}:{}}}function Qe(s,a){if(!a?.length)return;const i=a.filter(e=>O(e)&&(e.limitLow!==void 0||e.limitHigh!==void 0));if(!i.length)return;const o=[];for(const e of i){const c=e.testNumber;let r=0,t=0,l=0,n=0;for(const u of s){if(u.partial||u.edgeExcluded)continue;const f=u.testValues?.[c];f!==void 0&&(n++,e.limitLow!==void 0&&f<e.limitLow?t++:e.limitHigh!==void 0&&f>e.limitHigh?l++:r++)}o.push({testNumber:c,label:e.name,passDies:r,failLowDies:t,failHighDies:l,totalDies:n,yieldPercent:n>0?r/n*100:null})}return o.length?o:void 0}export function computeFunctionalYield(s,a){if(!a?.length)return;const i=a.filter(e=>!O(e));if(!i.length)return;const o=[];for(const e of i){const c=e.testNumber;let r=0,t=0;for(const n of s){if(n.partial||n.edgeExcluded)continue;const u=me(n,c,e);u!==void 0&&(u?r++:t++)}const l=r+t;o.push({testNumber:c,label:e.name,passDies:r,failDies:t,totalDies:l,passRatePercent:l>0?r/l*100:null})}return o.length?o:void 0}function Ue(s,a,i,o){const e=[];for(const c of a){const r=[];for(const f of s){if(f.partial||f.edgeExcluded)continue;const m=f.testValues?.[c];m!==void 0&&r.push(m)}if(r.length<o)continue;const t=J(r),l=Math.sqrt(fe(r,t)),n=r.slice().sort((f,m)=>f-m),u=i?.find(f=>f.testNumber===c)?.name??String(c);e.push({testNumber:c,label:u,count:r.length,min:n[0],max:n[n.length-1],mean:t,stddev:l,median:ne(n,.5),q1:ne(n,.25),q3:ne(n,.75)})}return e.length?e:void 0}function H(s,a,i,o){const e=(s+i)/(a+o),c=e*(1-e)*(1/a+1/o);if(!Number.isFinite(c)||c<=0)return 1;const r=(s/a-i/o)/Math.sqrt(c);return be(2*(1-pe(Math.abs(r))))}function ie(s){const a=new Map;for(const i of s){const o=`${i.variable.kind}:${i.comparison.family}`,e=a.get(o)??[];e.push(i),a.set(o,e)}for(const i of a.values()){const o=[...i].filter(r=>r.stats.pValue!==void 0).sort((r,t)=>(r.stats.pValue??1)-(t.stats.pValue??1)),e=o.length;let c=1;for(let r=e-1;r>=0;r--){const t=o[r],l=t.stats.pValue??1,n=Math.min(1,Math.min(c,l*e/(r+1)));t.stats.adjustedPValue=n,c=n}}return s}function se(s,a,i){const o=Math.abs(a),e=i!==void 0?Math.abs(i):0;return s<=.01&&(o>=.3||e>=2.5)?"unusual":s<=.05&&(o>=.2||e>=1.5)?"notable":"info"}function oe(s,a){return s<=.01&&Math.abs(a)>=.5?"unusual":s<=.05&&Math.abs(a)>=.15?"notable":"info"}function Z(s){return s==="reticle-position"?"other reticle positions":s==="test-site"?"other test sites":"the rest of the map"}function I(s){return s==="reticle-position"?"Other reticle positions":s==="test-site"?"Other test sites":"Rest of map"}function Se(s,a,i){const o=(Math.abs(a)*100).toFixed(1);return`${s} yield is ${o} percentage points ${a>0?"higher":"lower"} than ${Z(i)}`}function ae(s,a){return a==="quadrant"&&!s.startsWith("Quadrant")?`quadrant ${s}`:s}function we(s,a,i,o){const e=ae(s,o),c=(Math.abs(i)*100).toFixed(1);return`${e} has ${a} occurrence ${c} percentage points ${i>0?"higher":"lower"} than ${Z(o)}`}function Ge(s,a,i,o){const e=ae(s,o),c=(Math.abs(i)*100).toFixed(1);return`${e} has ${a} pass rate ${c} percentage points ${i>0?"higher":"lower"} than ${Z(o)}`}function $e(s,a,i,o,e,c){const r=ae(s,e),t=Z(e),l=i>0?"higher":"lower";if(o!==void 0&&Number.isFinite(o)){const u=(Math.abs(o)*100).toFixed(1);return`${r} mean ${a} is ${u}% ${l} than ${t}`}const n=c?` ${c}`:"";return`${r} mean ${a} is ${Math.abs(i).toPrecision(3)}${n} ${l} than ${t}`}function ve(s,a,i){const o=a?.find(e=>e.bin===s);return o?.name?`${i} ${s} (${o.name})`:`${i} ${s}`}function Xe(s,a){const i=a?.find(o=>o.testNumber===s);return{label:i?.name??`Test ${s}`,unit:i?.unit}}function re(s,a){const i=new Map(s.map(e=>[T(e),e])),o=new Map;for(const e of a){const c=[];for(const r of e.dieKeys){const t=i.get(r);t&&c.push(t)}o.set(e.key,c)}return o}function le(s,a){return ie(s),s.filter(i=>{const o=i.stats.adjustedPValue??i.stats.pValue??1,e=Math.abs(i.effect.absoluteDelta??0),c=Math.abs(i.effect.relativeDelta??0);return o<=a.significanceLevel&&(e>=a.minimumEffectSize||c>=a.minimumRelativeEffect)}).map(i=>({...i,severity:se(i.stats.adjustedPValue??i.stats.pValue??1,i.effect.absoluteDelta??0,i.effect.relativeDelta)}))}function W(s,a,i,o){const e=new Set(i),c=re(s,a),r=new Map,t=new Map;for(const[n,u]of c){let f=0,m=0;for(const g of u)g.hbin!==void 0&&(m++,e.has(g.hbin)&&f++);r.set(n,f),t.set(n,m)}const l=[];for(const n of a){const u=t.get(n.key)??0,f=r.get(n.key)??0;let m=0,g=0;for(const[w,h]of t)w!==n.key&&(m+=h,g+=r.get(w)??0);if(u<o.minimumSampleSize||m<o.minimumSampleSize)continue;const p=f/u,b=g/m,y=p-b,z=H(f,u,g,m);l.push({id:`yield:${n.key}`,level:"wafer",severity:"info",variable:{kind:"yield",label:"Yield"},comparison:{family:n.family,left:n.label,right:I(n.family)},effect:{direction:y===0?"different":y>0?"higher":"lower",absoluteDelta:y,relativeDelta:b===0?void 0:y/b,effectSize:y},stats:{method:"two-proportion-z",pValue:z,sampleSizeLeft:u,sampleSizeRight:m},summary:Se(n.label,y,n.family),highlight:{kind:"region",regionFamily:n.family,regionKeys:[n.key],dieKeys:[...n.dieKeys]}})}return le(l,o)}function L(s,a,i,o,e,c){const r=g=>i==="soft"?g.sbin:g.hbin,t=[...new Set(s.map(r).filter(g=>g!==void 0))].sort((g,p)=>g-p),l=re(s,a),n=new Map,u=new Map;for(const[g,p]of l){const b=new Map;for(const y of p){const z=r(y);z!==void 0&&b.set(z,(b.get(z)??0)+1)}n.set(g,b),u.set(g,p.length)}const f=[],m=e==="hardBin"?"HBin":"SBin";for(const g of a){const p=u.get(g.key)??0,b=n.get(g.key);let y=0;const z=new Map;for(const[w,h]of n)if(w!==g.key){y+=u.get(w)??0;for(const[S,v]of h)z.set(S,(z.get(S)??0)+v)}if(!(p<c.minimumSampleSize||y<c.minimumSampleSize))for(const w of t){const h=b.get(w)??0,S=z.get(w)??0,v=h/p,D=S/y,$=v-D,k=H(h,p,S,y),M=ve(w,o,m);f.push({id:`${e}:${w}:${g.key}`,level:"wafer",severity:"info",variable:{kind:e,bin:w,label:M},comparison:{family:g.family,left:g.label,right:I(g.family)},effect:{direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:D===0?void 0:$/D,effectSize:$},stats:{method:"two-proportion-z",pValue:k,sampleSizeLeft:p,sampleSizeRight:y},summary:we(g.label,M,$,g.family),highlight:{kind:"bin",bin:w,regionKeys:[g.key],dieKeys:[...g.dieKeys]}})}}return le(f,c)}function Q(s,a,i,o){const e=(i??[]).filter(u=>!O(u));if(!e.length)return[];const c=e.map(u=>u.testNumber),r=re(s,a),t=new Map,l=new Map;for(const[u,f]of r){const m=new Uint32Array(e.length),g=new Uint32Array(e.length);for(const p of f)for(let b=0;b<e.length;b++){const y=me(p,c[b],e[b]);y!==void 0&&(g[b]++,y&&m[b]++)}t.set(u,m),l.set(u,g)}const n=[];for(const u of a){const f=t.get(u.key),m=l.get(u.key);for(let g=0;g<e.length;g++){let p=0,b=0;for(const[$,k]of l)$!==u.key&&(b+=k[g],p+=t.get($)[g]);const y=m[g],z=f[g];if(y<o.minimumSampleSize||b<o.minimumSampleSize)continue;const w=z/y,h=p/b,S=w-h,v=H(z,y,p,b),D=c[g];n.push({id:`functional:${D}:${u.key}`,level:"wafer",severity:"info",variable:{kind:"functionalTest",index:D,label:`${e[g].name} pass rate`},comparison:{family:u.family,left:u.label,right:I(u.family)},effect:{direction:S===0?"different":S>0?"higher":"lower",absoluteDelta:S,relativeDelta:h===0?void 0:S/h,effectSize:S},stats:{method:"two-proportion-z",pValue:v,sampleSizeLeft:y,sampleSizeRight:b},summary:Ge(u.label,e[g].name,S,u.family),highlight:{kind:"region",regionFamily:u.family,regionKeys:[u.key],dieKeys:[...u.dieKeys]}})}}return le(n,o)}function fe(s,a){return s.length<2?0:s.reduce((i,o)=>i+(o-a)**2,0)/(s.length-1)}function De(s,a,i,o,e,c){const r=Math.sqrt(i/s+c/o),t=a-e;if(!Number.isFinite(r)||r===0)return{pValue:1,effectSize:0,delta:t};const l=t/r,n=Math.sqrt(Math.max(0,(i+c)/2)),u=n===0?t:t/n;return{pValue:be(2*(1-pe(Math.abs(l)))),effectSize:u,delta:t}}function Je(s,a){const i=J(s),o=J(a);return De(s.length,i,fe(s,i),a.length,o,fe(a,o))}const ke=250;function ze(s,a,i){const o=r=>r.filter(t=>O(i?.find(l=>l.testNumber===t)));if(a)return{testNumbers:o(a.slice().sort((r,t)=>r-t))};const e=new Set;let c=!1;e:for(const r of s)for(const t of Object.keys(r.testValues??{})){const l=Number(t);if(!e.has(l)&&(e.add(l),e.size>ke)){c=!0;break e}}if(c){const r=`Test-value analysis was skipped: more than ${ke} tests were found in the die data, so no test findings were computed. Pass testNumbers: [...] to analyse specific tests.`;return console.warn(`[wafermap] analyzeWaferMap: ${r}`),{testNumbers:[],warning:{code:"test-count-capped",message:r,severity:"warning"}}}return{testNumbers:o([...e].sort((r,t)=>r-t))}}function U(s,a,i,o){const e=ze(s,o.testNumbers,i);if(e.warning)return{findings:[],warning:e.warning};const c=e.testNumbers,r=new Map;for(let p=0;p<a.length;p++)for(const b of a[p].dieKeys)r.set(b,p);const t=a.length,l=[],n=[];for(const p of s){const b=r.get(T(p));b!==void 0&&(l.push(p),n.push(b))}const u=[],f=new Float64Array(t),m=new Float64Array(t),g=new Float64Array(t);for(const p of c){f.fill(0),m.fill(0),g.fill(0);let b=0,y=0,z=0,w;for(let h=0;h<l.length;h++){const v=l[h].testValues?.[p];if(v===void 0)continue;w===void 0&&(w=v);const D=v-w,$=n[h];f[$]+=1,m[$]+=D,g[$]+=D*D,b+=1,y+=D,z+=D*D}if(w!==void 0)for(let h=0;h<t;h++){const S=f[h],v=b-S;if(S<o.minimumSampleSize||v<o.minimumSampleSize)continue;const D=m[h]/S,k=(y-m[h])/v,M=D+w,R=k+w,P=S>1?Math.max(0,g[h]-S*D*D)/(S-1):0,d=v>1?Math.max(0,z-g[h]-v*k*k)/(v-1):0,{pValue:N,effectSize:B,delta:V}=De(S,M,P,v,R,d),F=a[h],{label:A,unit:G}=Xe(p,i),_=R!==0?V/Math.abs(R):void 0;u.push({id:`test:${p}:${F.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:p,label:A,unit:G},comparison:{family:F.family,left:F.label,right:I(F.family)},effect:{direction:V===0?"different":V>0?"higher":"lower",absoluteDelta:V,relativeDelta:_,effectSize:B},stats:{method:"welch-z-approx",pValue:N,sampleSizeLeft:S,sampleSizeRight:v},summary:$e(F.label,A,V,_,F.family,G),highlight:{kind:"region",regionFamily:F.family,regionKeys:[F.key],dieKeys:[...F.dieKeys]}})}}return ie(u),{activeTestNumbers:c,findings:u.filter(p=>{const b=p.stats.adjustedPValue??p.stats.pValue??1,y=Math.abs(p.effect.effectSize??0);return b<=o.significanceLevel&&y>=o.minimumEffectSize}).map(p=>({...p,severity:oe(p.stats.adjustedPValue??p.stats.pValue??1,p.effect.effectSize??0)}))}}function Ze(s,a,i,o){if(!i?.length)return[];const e=i.filter(t=>O(t)&&(t.limitLow!==void 0||t.limitHigh!==void 0));if(!e.length)return[];const c=[],r=new Map(s.map(t=>[T(t),t]));for(const t of e){const l=t.testNumber;for(const n of a){const u=new Map;for(const m of n){const g=[];for(const p of m.dieKeys){const b=r.get(p);b&&g.push(b)}u.set(m.key,g)}const f=[];for(const m of n){const g=u.get(m.key),p=[];for(const[M,R]of u)if(M!==m.key)for(const P of R)p.push(P);const b=M=>M.testValues?.[l]!==void 0,y=M=>{const R=M.testValues?.[l];return R===void 0?!1:t.limitLow!==void 0&&R<t.limitLow||t.limitHigh!==void 0&&R>t.limitHigh},z=g.filter(b),w=p.filter(b);if(z.length<o.minimumSampleSize||w.length<o.minimumSampleSize)continue;const h=z.filter(y).length,S=w.filter(y).length,v=h/z.length,D=S/w.length,$=v-D,k=H(h,z.length,S,w.length);f.push({id:`specLimit:${l}:${m.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:l,label:t.name,unit:t.unit},comparison:{family:m.family,left:m.label,right:I(m.family)},effect:{direction:$===0?"different":$>0?"higher":"lower",absoluteDelta:$,relativeDelta:D===0?void 0:$/D,effectSize:$},stats:{method:"two-proportion-z",pValue:k,sampleSizeLeft:z.length,sampleSizeRight:w.length},summary:`${m.label} spec-fail rate for ${t.name} is ${(Math.abs($)*100).toFixed(1)} pp ${$>0?"higher":"lower"} than the rest of the wafer`,highlight:{kind:"region",regionFamily:m.family,regionKeys:[m.key],dieKeys:[...m.dieKeys]}})}ie(f),c.push(...f.filter(m=>{const g=m.stats.adjustedPValue??m.stats.pValue??1,p=Math.abs(m.effect.absoluteDelta??0),b=Math.abs(m.effect.relativeDelta??0);return g<=o.significanceLevel&&(p>=o.minimumEffectSize||b>=o.minimumRelativeEffect)}).map(m=>({...m,severity:oe(m.stats.adjustedPValue??m.stats.pValue??1,m.effect.effectSize??0)})))}}return c}function j(s){return s.highlight.regionKeys??[]}function et(s){const a=s.variable.bin??s.variable.index??"";return`${s.comparison.family}\0${s.variable.kind}\0${a}\0${s.effect.direction}`}function Y(s,a){const i=j(s)[0]??"",o=q(i);return o.family==="ring"?o.ring??0:o.family==="sector"?he(a).indexOf(o.sector??""):0}function tt(s,a,i){if(a==="quadrant"){const r=new Map;for(const u of s){const f=q(j(u)[0]??"").quadrant;f&&r.set(f,u)}const t=[...r.keys()],l=new Set,n=[];for(const u of t){if(l.has(u))continue;const f=[],m=[u];for(l.add(u);m.length;){const g=m.pop();f.push(g);for(const p of t)!l.has(p)&&je(g,p)&&(l.add(p),m.push(p))}n.push(f.map(g=>r.get(g)))}return n}const o=[...s].sort((r,t)=>Y(r,i)-Y(t,i)),e=[];let c=[];for(const r of o){if(c.length===0){c.push(r);continue}const t=Y(c[c.length-1],i);Y(r,i)-t===1?c.push(r):(e.push(c),c=[r])}if(c.length&&e.push(c),a==="sector"&&e.length>1){const t=he(i).length,l=Y(e[0][0],i),n=Y(e[e.length-1][e[e.length-1].length-1],i);if(l===0&&n===t-1){const u=e.pop();e[0]=[...u,...e[0]]}}return e}function nt(s,a){const i=s.map(r=>q(j(r)[0]??"").ring).filter(r=>r!==void 0).sort((r,t)=>r-t),o=i[0],e=i[i.length-1];let c="";return o===e&&o===1&&a>1?c=" (core)":o===e&&o===a&&a>1&&(c=" (edge)"),o===e?`Ring ${o}${c}`:`Rings ${o}\u2013${e}`}function it(s,a){const i=s.map(o=>q(j(o)[0]??"").sector??"");return i.length===1?`Sector ${i[0]}`:`Sectors ${i[0]}\u2013${i[i.length-1]}`}function st(s){const a=s.map(i=>q(j(i)[0]??"").quadrant??"");return a.length===1?a[0]:a.length===2?`Quadrants ${a[0]} & ${a[1]}`:`Quadrants ${a.slice(0,-1).join(", ")} & ${a[a.length-1]}`}function ce(s){return[...new Set(s)]}function ot(s,a){const i=s[0],o=i.comparison.family,e=i.variable.kind,c=ce(s.flatMap(h=>h.highlight.dieKeys??[])),r=ce(s.flatMap(j)),t=new Set(c),l=o==="ring"?nt(s,a.ringCount):o==="sector"?it(s,a.sectorCount):st(s),n=e==="softBin"?a.softEligibleDies:e==="test"?a.testDies:a.eligibleDies,u=n.filter(h=>t.has(T(h))),f=n.filter(h=>!t.has(T(h)));let m,g,p,b,y;if(e==="test"){const h=i.variable.index,S=d=>d.testValues?.[h],v=u.map(S).filter(d=>d!==void 0),D=f.map(S).filter(d=>d!==void 0),{pValue:$,effectSize:k,delta:M}=Je(v,D),R=J(D),P=R!==0?M/Math.abs(R):void 0;m={direction:M===0?"different":M>0?"higher":"lower",absoluteDelta:M,relativeDelta:P,effectSize:k},g={method:"welch-z-approx",pValue:$,sampleSizeLeft:v.length,sampleSizeRight:D.length},p=oe($,k),b=$e(l,i.variable.label,M,P,o,i.variable.unit),y=`test:${h}`}else if(e==="yield"){const h=new Set(a.passBins),S=u.filter(B=>B.hbin!==void 0),v=f.filter(B=>B.hbin!==void 0),D=S.length,$=v.length,k=S.filter(B=>h.has(B.hbin)).length,M=v.filter(B=>h.has(B.hbin)).length,R=D>0?k/D:0,P=$>0?M/$:0,d=R-P,N=H(k,D,M,$);m={direction:d===0?"different":d>0?"higher":"lower",absoluteDelta:d,relativeDelta:P===0?void 0:d/P,effectSize:d},g={method:"two-proportion-z",pValue:N,sampleSizeLeft:D,sampleSizeRight:$},p=se(N,d,m.relativeDelta),b=Se(l,d,o),y="yield"}else{const h=i.variable.bin,S=d=>e==="softBin"?d.sbin:d.hbin,v=u.filter(d=>S(d)===h).length,D=f.filter(d=>S(d)===h).length,$=v/u.length,k=D/f.length,M=$-k,R=H(v,u.length,D,f.length);m={direction:M===0?"different":M>0?"higher":"lower",absoluteDelta:M,relativeDelta:k===0?void 0:M/k,effectSize:M},g={method:"two-proportion-z",pValue:R,sampleSizeLeft:u.length,sampleSizeRight:f.length},p=se(R,M,m.relativeDelta);const P=e==="softBin"?a.sbinDefs:a.hbinDefs;b=we(l,ve(h,P,e==="softBin"?"SBin":"HBin"),M,o),y=`${e}:${h}`}const z=ce(r.map(h=>h.replace(`${o}:`,""))).join("-"),w=e==="hardBin"||e==="softBin"?{kind:"bin",bin:i.variable.bin,regionKeys:r,dieKeys:c}:{kind:"region",regionFamily:o,regionKeys:r,dieKeys:c};return{...i,id:`${y}:${o}:${z}`,severity:p,comparison:{family:o,left:l,right:I(o)},effect:m,stats:g,summary:b,highlight:w,relatedIds:s.map(h=>h.id)}}function at(s,a){const i=new Set(["ring","quadrant","sector"]),o=[],e=new Map;for(const r of s){if(!i.has(r.comparison.family)){o.push(r);continue}const t=et(r),l=e.get(t)??[];l.push(r),e.set(t,l)}const c=[];for(const r of e.values()){if(r.length===1){c.push(r[0]);continue}const t=r[0].comparison.family;for(const l of tt(r,t,a.sectorCount))c.push(l.length===1?l[0]:ot(l,a))}return[...o,...c]}function rt(s){const a=new Map,i=new Map;for(const e of s){const c=T(e);e.hbin!==void 0&&e.hbin!==null&&(a.get(e.hbin)??a.set(e.hbin,new Set).get(e.hbin)).add(c),e.sbin!==void 0&&e.sbin!==null&&(i.get(e.sbin)??i.set(e.sbin,new Set).get(e.sbin)).add(c)}const o=new Set;for(const[e,c]of a)for(const[r,t]of i){if(c.size!==t.size)continue;let l=!0;for(const n of c)if(!t.has(n)){l=!1;break}l&&o.add(`${e}:${r}`)}return o}function lt(s){return`${s.comparison.family}\0${s.comparison.left}`}function ft(s,a,i){const o=(t,l)=>{t.absorbedIds=[...t.absorbedIds??[],l.id]},e=new Map;for(const t of s){const l=lt(t);(e.get(l)??e.set(l,[]).get(l)).push(t)}const c=rt(a),r=new Set;for(const t of e.values()){for(const n of t)if(!(n.variable.kind!=="hardBin"||n.variable.bin===void 0))for(const u of t){if(u.variable.kind!=="softBin"||u.variable.bin===void 0||r.has(u.id)||!c.has(`${n.variable.bin}:${u.variable.bin}`)||u.effect.direction!==n.effect.direction)continue;o(n,u),r.add(u.id);const f=n.variable.label.replace(/^HBin\s+/,""),m=u.variable.label.replace(/^SBin\s+/,""),g=f===m?`HBin and SBin ${f} (same dies)`:`${n.variable.label} and ${u.variable.label} (same dies)`;n.summary=n.summary.replace(n.variable.label,g),n.variable.label=g;break}if(i.length!==1)continue;const l=t.find(n=>n.variable.kind==="yield");if(l){for(const n of t)if(!(n===l||r.has(n.id)||!((n.variable.kind==="hardBin"||n.variable.kind==="softBin")&&n.variable.bin===i[0]))){o(l,n),r.add(n.id);for(const f of n.absorbedIds??[])l.absorbedIds=[...l.absorbedIds??[],f]}}}}export function analyzeWaferMap(s,a={}){const{resolved:i,warnings:o}=He(a),e=Ce(s),c={...i,passBins:e.passBins??x.passBins,ringCount:e.ringCount??x.ringCount},r=e.isLotStack,t=e.aggrMethod,l=!r||t==="mode"||t==="countBin"||t==="percent",n=e.dies.filter(d=>Ye(d)),u=n.filter(ge),f=Ie(c,n.length),m=e.dies.filter(d=>te(d)),g=m.filter(ge),p=Ke(g,e.wafer,f.ringCount),b=Le(g,e.wafer,f.ringCount),y=Ee(g,e.reticleConfig),z=xe(m,!1),w=Te(g,e.wafer,f.sectorCount),h=[];l&&h.push(...W(n,p,f.passBins,f),...W(n,b,f.passBins,f),...W(n,y,f.passBins,f),...W(n,z,f.passBins,f),...W(n,w,f.passBins,f)),l&&h.push(...L(n,p,"hard",e.hbinDefs,"hardBin",f),...L(n,b,"hard",e.hbinDefs,"hardBin",f),...L(n,y,"hard",e.hbinDefs,"hardBin",f),...L(n,z,"hard",e.hbinDefs,"hardBin",f),...L(n,w,"hard",e.hbinDefs,"hardBin",f));{const d=n.filter(N=>N.sbin!==void 0);h.push(...L(d,p,"soft",e.sbinDefs,"softBin",f),...L(d,b,"soft",e.sbinDefs,"softBin",f),...L(d,y,"soft",e.sbinDefs,"softBin",f),...L(d,z,"soft",e.sbinDefs,"softBin",f),...L(d,w,"soft",e.sbinDefs,"softBin",f))}const S=[...o];let v;if(f.enableTestValueAnalysis){const d=U(n,p,e.testDefs,f),N=U(n,b,e.testDefs,f),B=U(n,y,e.testDefs,f),V=U(n,z,e.testDefs,f),F=U(n,w,e.testDefs,f);h.push(...d.findings,...N.findings,...B.findings,...V.findings,...F.findings),d.warning&&S.push(d.warning),v=d.activeTestNumbers,h.push(...Ze(n,[p,b,y,z,w],e.testDefs,f)),h.push(...Q(n,p,e.testDefs,f),...Q(n,b,e.testDefs,f),...Q(n,y,e.testDefs,f),...Q(n,z,e.testDefs,f),...Q(n,w,e.testDefs,f))}{const d=_e(r,l,e.testDefs);(!r||l||d!==void 0)&&h.push(...Ae(u,e.wafer,{...f,isFailingDie:d}))}const D=at(h,{eligibleDies:n,softEligibleDies:n.filter(d=>d.sbin!==void 0),testDies:n,passBins:f.passBins,ringCount:f.ringCount,sectorCount:f.sectorCount,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs});h.length=0,h.push(...D);let $;if(l){const d=Oe(u,e.wafer,{passBins:f.passBins,ringCount:f.ringCount});if($=d??void 0,d!==null&&d.pattern!=="random"&&d.pattern!=="none"){let Fe=function(C){const E=j(C).map(K=>q(K).ring).filter(K=>K!==void 0);if(E.length===0)return!0;const ue=E.some(K=>K===Be),de=E.some(K=>K===1);return X==="edge-ring"||X==="edge-local"?ue:X==="center"?de:X==="donut"?!ue&&!de:!0};const B={center:"Center cluster",donut:"Donut","edge-ring":"Edge-ring","edge-local":"Edge-local",scratch:"Scratch","near-full":"Near-full"}[d.pattern]??d.pattern,V=d.confidence==="high"?"unusual":d.confidence==="medium"?"notable":"info",F=d.features,A=C=>`${(C*100).toFixed(0)}%`,G=d.pattern==="edge-ring"||d.pattern==="edge-local"?`${A(F.edgeRdd)} of edge dies failing`:d.pattern==="center"||d.pattern==="donut"?`centroid at ${A(F.centroidDistNorm)} of wafer radius from centre`:d.pattern==="scratch"?`linear score ${F.linearScore.toFixed(2)}, eccentricity ${F.eccentricity.toFixed(2)}`:`${A(F.globalRdd)} of dies failing`,_=n.filter(C=>{const E=C.hbin??C.sbin;return E!==void 0&&!new Set(f.passBins).has(E)}),Me={"edge-ring":["ring","edge-arc"],"edge-local":["edge-arc","sector","quadrant"],center:["ring","cluster"],donut:["ring"],scratch:["cluster","sector","quadrant"],"near-full":["ring"]},Re=new Set(Me[d.pattern]??[]),X=d.pattern,Be=f.ringCount,ee=[];for(const C of h){if(!Re.has(C.comparison.family)||C.comparison.family==="ring"&&!Fe(C))continue;ee.push(C.id);const E=C.comparison.family==="ring"||C.comparison.family==="quadrant"||C.comparison.family==="sector";V!=="info"&&E&&(C.severity="info")}h.push({id:`spatial-pattern:${d.pattern}`,level:"wafer",severity:V,variable:{kind:"spatialPattern",label:B},comparison:{family:"spatial-pattern",left:B,right:"Wafer"},effect:{direction:"different",effectSize:F.globalRdd},stats:{method:"geometry",sampleSizeLeft:_.length,sampleSizeRight:n.length},summary:`Spatial pattern: ${B.toLowerCase()} (${d.confidence} confidence) \u2014 ${G}${d.note?` [${d.note}]`:""}`,highlight:{kind:"dies",dieKeys:_.map(C=>T(C))},relatedIds:ee.length>0?ee:void 0})}}h.sort((d,N)=>{const B=d.stats.adjustedPValue??d.stats.pValue??1,V=N.stats.adjustedPValue??N.stats.pValue??1;return B!==V?B-V:Math.abs(N.effect.absoluteDelta??0)-Math.abs(d.effect.absoluteDelta??0)});const k=We(e.dies,n.length,e.yield.yieldPercent);r&&(k.isLotStack=!0,t&&(k.aggregationMethod=t));const M=Qe(e.dies,e.testDefs);M&&(k.testSpecYield=M);const R=computeFunctionalYield(e.dies,e.testDefs);if(R&&(k.functionalYield=R),$&&(k.spatialPattern=$),v===void 0&&f.computePerTestStats){const d=ze(n,f.testNumbers,e.testDefs);d.warning&&S.push(d.warning),v=d.testNumbers}if(v?.length){const d=Ue(e.dies,v,e.testDefs,f.minimumSampleSize);d&&(k.perTestStats=d);const N=Ne([{dies:e.dies}],e.testDefs,v);N&&(k.capability=N)}Object.assign(k,Pe(e.dies,e.testDefs));const P=Ve([{dies:e.dies,wafer:e.wafer,passBins:f.passBins,ringCount:f.ringCount}]);return P&&(k.regionYield=P),S.length>0&&(k.warnings=S),ft(h,n,f.passBins),{level:"wafer",hasNotableFindings:h.some(d=>d.severity==="notable"||d.severity==="unusual"),findings:h,wafer:e.wafer.metadata??void 0,stats:k}}
1
+ import{normalizeInput as Le}from"./normalizeInput.js";import{computeCapability as Ee,computeTestFlagYield as Ke,computeRegionYield as Te}from"./summaryFigures.js";import{isYieldEligibleDie as ae,getDieKey as T,isPositionedDie as be}from"../core/dies.js";import{getTestPassStatus as ee,isParametricTest as q}from"../renderer/buildWaferMap.js";import{markedTestLabel as x,derivedFields as te}from"../renderer/testLabel.js";import{buildQuadrantRegions as xe,buildReticlePositionRegions as je,buildRingRegions as Ae,buildSectorRegions as Oe,buildTestSiteRegions as He,sectorCompassNames as ye,areQuadrantsAdjacent as qe,parseRegionKey as I}from"./regions.js";import{buildClusterFindings as Ie}from"./clusterDetection.js";import{classifyPattern as Ye}from"./patternClassification.js";import{normalCdf as Se}from"./math.js";import{quantile as re}from"./math.js";import{mean as ne,clamp01 as we}from"../core/utils.js";const _e={significanceLevel:{min:1e-6,max:1},minimumEffectSize:{min:0,max:1},minimumRelativeEffect:{min:0,max:Number.MAX_SAFE_INTEGER}},ve=[4,8,16,32];function We(s){const a=Object.fromEntries(Object.entries(s).filter(([,t])=>t!==void 0)),i={...j,...a},o=[],e=[];for(const t of["significanceLevel","minimumEffectSize","minimumRelativeEffect"]){const{min:f,max:n}=_e[t],u=i[t];if(typeof u!="number"||!Number.isFinite(u)){e.push(`${t}=${String(u)} is not a finite number (using ${j[t]})`),i[t]=j[t];continue}const l=Math.min(n,Math.max(f,u));l!==u&&(e.push(`${t}=${u} is outside ${f}\u2013${n} (using ${l})`),i[t]=l)}if(ve.includes(i.sectorCount)||(e.push(`sectorCount=${i.sectorCount} is not one of ${ve.join(", ")} (using ${j.sectorCount})`),i.sectorCount=j.sectorCount),e.length){const t=`analyzeWaferMap: ${e.join("; ")}. The analysis ran with the corrected values.`;console.warn(`[wafermap] ${t}`),o.push({code:"analysis-option-corrected",message:t,severity:"warning"})}const c=["passBins","ringCount"].filter(t=>a[t]!==void 0);if(c.length){const t=`analyzeWaferMap: ${c.join(" and ")} ${c.length>1?"are":"is"} no longer an analysis option and was ignored. Set ${c.length>1?"them":"it"} on buildWaferMap; the analysis used the value the map was built with.`;console.warn(`[wafermap] ${t}`),o.push({code:"analysis-option-corrected",message:t,severity:"warning"})}const r=["enableYieldAnalysis","enableHardBinAnalysis","enableSoftBinAnalysis","enableReticlePositionAnalysis","enableTestSiteAnalysis","enableClusterAnalysis","enableAngularAnalysis","enablePatternClassification"].filter(t=>a[t]!==void 0);if(r.length){const t=`analyzeWaferMap: ${r.join(", ")} ${r.length>1?"are":"is"} no longer an option and was ignored: every analysis now runs. Filter the findings (filterFindings) to show fewer.`;console.warn(`[wafermap] ${t}`),o.push({code:"analysis-option-corrected",message:t,severity:"warning"})}return{resolved:i,warnings:o}}const j={ringCount:4,passBins:[1],significanceLevel:.05,minimumEffectSize:.2,minimumRelativeEffect:1,minimumSampleSize:5,enableTestValueAnalysis:!1,computePerTestStats:!1,sectorCount:8,minimumClusterSize:5};function Qe(s,a){const i={...s};return i.minimumClusterSize=Math.max(5,Math.round(a*.003)),i}function Ue(s){return ae(s)?s.hbin!==void 0||s.sbin!==void 0||s.testValues!==void 0&&Object.keys(s.testValues).length>0:!1}function Ge(s,a,i){if(!s||a)return;const o=(i??[]).filter(e=>q(e)&&(e.limitLow!==void 0||e.limitHigh!==void 0));if(o.length!==0)return e=>{for(const c of o){const r=c.testNumber,t=e.testValues?.[r];if(t!==void 0&&(c.limitLow!==void 0&&t<c.limitLow||c.limitHigh!==void 0&&t>c.limitHigh))return!0}return!1}}function Xe(s,a,i){const o=new Set,e=new Set,c=new Set,r=new Map,t=new Map;for(const f of s){if(f.testValues)for(const n of Object.keys(f.testValues))o.add(Number(n));if(f.testPass)for(const n of Object.keys(f.testPass))o.add(Number(n));f.hbin!==void 0&&e.add(f.hbin),f.sbin!==void 0&&c.add(f.sbin),ae(f)&&(f.hbin!==void 0&&r.set(f.hbin,(r.get(f.hbin)??0)+1),f.sbin!==void 0&&t.set(f.sbin,(t.get(f.sbin)??0)+1))}return{totalDies:s.length,analyzedDies:a,excludedDies:s.length-a,yieldPercent:i,testsConsidered:[...o].sort((f,n)=>f-n),hardBinsConsidered:[...e].sort((f,n)=>f-n),softBinsConsidered:[...c].sort((f,n)=>f-n),...r.size?{hardBinCounts:Object.fromEntries(r)}:{},...t.size?{softBinCounts:Object.fromEntries(t)}:{}}}function Je(s,a){if(!a?.length)return;const i=a.filter(e=>q(e)&&(e.limitLow!==void 0||e.limitHigh!==void 0));if(!i.length)return;const o=[];for(const e of i){const c=e.testNumber;let r=0,t=0,f=0,n=0;for(const u of s){if(u.partial||u.edgeExcluded)continue;const l=u.testValues?.[c];l!==void 0&&(n++,e.limitLow!==void 0&&l<e.limitLow?t++:e.limitHigh!==void 0&&l>e.limitHigh?f++:r++)}o.push({testNumber:c,label:e.name,passDies:r,failLowDies:t,failHighDies:f,totalDies:n,yieldPercent:n>0?r/n*100:null})}return o.length?o:void 0}export function computeFunctionalYield(s,a){if(!a?.length)return;const i=a.filter(e=>!q(e));if(!i.length)return;const o=[];for(const e of i){const c=e.testNumber;let r=0,t=0;for(const n of s){if(n.partial||n.edgeExcluded)continue;const u=ee(n,c,e);u!==void 0&&(u?r++:t++)}const f=r+t;o.push({testNumber:c,label:x(e,c),...te(e),passDies:r,failDies:t,totalDies:f,passRatePercent:f>0?r/f*100:null})}return o.length?o:void 0}function Ze(s,a,i,o){const e=[];for(const c of a){const r=[];for(const l of s){if(l.partial||l.edgeExcluded)continue;const h=l.testValues?.[c];h!==void 0&&Number.isFinite(h)&&r.push(h)}if(r.length<o)continue;const t=ne(r),f=Math.sqrt(ge(r,t)),n=Float64Array.from(r);n.sort();const u=i?.find(l=>l.testNumber===c)?.name??String(c);e.push({testNumber:c,label:u,count:r.length,min:n[0],max:n[n.length-1],mean:t,stddev:f,median:re(n,.5),q1:re(n,.25),q3:re(n,.75)})}return e.length?e:void 0}function A(s,a,i,o){const e=(s+i)/(a+o),c=e*(1-e)*(1/a+1/o);if(!Number.isFinite(c)||c<=0)return 1;const r=(s/a-i/o)/Math.sqrt(c);return we(2*(1-Se(Math.abs(r))))}function fe(s){const a=new Map;for(const i of s){const o=`${i.variable.kind}:${i.comparison.family}`,e=a.get(o)??[];e.push(i),a.set(o,e)}for(const i of a.values()){const o=[...i].filter(r=>r.stats.pValue!==void 0).sort((r,t)=>(r.stats.pValue??1)-(t.stats.pValue??1)),e=o.length;let c=1;for(let r=e-1;r>=0;r--){const t=o[r],f=t.stats.pValue??1,n=Math.min(1,Math.min(c,f*e/(r+1)));t.stats.adjustedPValue=n,c=n}}return s}function ie(s,a,i){const o=Math.abs(a),e=i!==void 0?Math.abs(i):0;return s<=.01&&(o>=.3||e>=2.5)?"unusual":s<=.05&&(o>=.2||e>=1.5)?"notable":"info"}function le(s,a){return s<=.01&&Math.abs(a)>=.5?"unusual":s<=.05&&Math.abs(a)>=.15?"notable":"info"}function se(s){return s==="reticle-position"?"other reticle positions":s==="test-site"?"other test sites":"the rest of the map"}function Y(s){return s==="reticle-position"?"Other reticle positions":s==="test-site"?"Other test sites":"Rest of map"}function $e(s,a,i){const o=(Math.abs(a)*100).toFixed(1);return`${s} yield is ${o} percentage points ${a>0?"higher":"lower"} than ${se(i)}`}function ce(s,a){return a==="quadrant"&&!s.startsWith("Quadrant")?`quadrant ${s}`:s}function De(s){return/^(Rings|Sectors|Quadrants)\b/.test(s)?"have":"has"}function ke(s,a,i,o){const e=ce(s,o),c=(Math.abs(i)*100).toFixed(1);return`${e} ${De(e)} ${a} occurrence ${c} percentage points ${i>0?"higher":"lower"} than ${se(o)}`}function ze(s,a,i,o){const e=ce(s,o),c=(Math.abs(i)*100).toFixed(1);return`${e} ${De(e)} ${a} pass rate ${c} percentage points ${i>0?"higher":"lower"} than ${se(o)}`}function Me(s,a,i,o,e,c){const r=ce(s,e),t=se(e),f=i>0?"higher":"lower";if(o!==void 0&&Number.isFinite(o)){const u=(Math.abs(o)*100).toFixed(1);return`${r} mean ${a} is ${u}% ${f} than ${t}`}const n=c?` ${c}`:"";return`${r} mean ${a} is ${Math.abs(i).toPrecision(3)}${n} ${f} than ${t}`}function Re(s,a,i){const o=a?.find(e=>e.bin===s);return o?.name?`${i} ${s} (${o.name})`:`${i} ${s}`}function et(s,a){const i=a?.find(o=>o.testNumber===s);return{label:x(i,s),unit:i?.unit,...te(i)}}function ue(s,a){const i=new Map(s.map(e=>[T(e),e])),o=new Map;for(const e of a){const c=[];for(const r of e.dieKeys){const t=i.get(r);t&&c.push(t)}o.set(e.key,c)}return o}function de(s,a){return fe(s),s.filter(i=>{const o=i.stats.adjustedPValue??i.stats.pValue??1,e=Math.abs(i.effect.absoluteDelta??0),c=Math.abs(i.effect.relativeDelta??0);return o<=a.significanceLevel&&(e>=a.minimumEffectSize||c>=a.minimumRelativeEffect)}).map(i=>({...i,severity:ie(i.stats.adjustedPValue??i.stats.pValue??1,i.effect.absoluteDelta??0,i.effect.relativeDelta)}))}function W(s,a,i,o){const e=new Set(i),c=ue(s,a),r=new Map,t=new Map;for(const[n,u]of c){let l=0,h=0;for(const g of u)g.hbin!==void 0&&(h++,e.has(g.hbin)&&l++);r.set(n,l),t.set(n,h)}const f=[];for(const n of a){const u=t.get(n.key)??0,l=r.get(n.key)??0;let h=0,g=0;for(const[k,m]of t)k!==n.key&&(h+=m,g+=r.get(k)??0);if(u<o.minimumSampleSize||h<o.minimumSampleSize)continue;const p=l/u,b=g/h,y=p-b,M=A(l,u,g,h);f.push({id:`yield:${n.key}`,level:"wafer",severity:"info",variable:{kind:"yield",label:"Yield"},comparison:{family:n.family,left:n.label,right:Y(n.family)},effect:{direction:y===0?"different":y>0?"higher":"lower",absoluteDelta:y,relativeDelta:b===0?void 0:y/b,effectSize:y},stats:{method:"two-proportion-z",pValue:M,sampleSizeLeft:u,sampleSizeRight:h},summary:$e(n.label,y,n.family),highlight:{kind:"region",regionFamily:n.family,regionKeys:[n.key],dieKeys:[...n.dieKeys]}})}return de(f,o)}function L(s,a,i,o,e,c){const r=g=>i==="soft"?g.sbin:g.hbin,t=[...new Set(s.map(r).filter(g=>g!==void 0))].sort((g,p)=>g-p),f=ue(s,a),n=new Map,u=new Map;for(const[g,p]of f){const b=new Map;for(const y of p){const M=r(y);M!==void 0&&b.set(M,(b.get(M)??0)+1)}n.set(g,b),u.set(g,p.length)}const l=[],h=e==="hardBin"?"HBin":"SBin";for(const g of a){const p=u.get(g.key)??0,b=n.get(g.key);let y=0;const M=new Map;for(const[k,m]of n)if(k!==g.key){y+=u.get(k)??0;for(const[S,$]of m)M.set(S,(M.get(S)??0)+$)}if(!(p<c.minimumSampleSize||y<c.minimumSampleSize))for(const k of t){const m=b.get(k)??0,S=M.get(k)??0,$=m/p,w=S/y,v=$-w,D=A(m,p,S,y),z=Re(k,o,h);l.push({id:`${e}:${k}:${g.key}`,level:"wafer",severity:"info",variable:{kind:e,bin:k,label:z},comparison:{family:g.family,left:g.label,right:Y(g.family)},effect:{direction:v===0?"different":v>0?"higher":"lower",absoluteDelta:v,relativeDelta:w===0?void 0:v/w,effectSize:v},stats:{method:"two-proportion-z",pValue:D,sampleSizeLeft:p,sampleSizeRight:y},summary:ke(g.label,z,v,g.family),highlight:{kind:"bin",bin:k,regionKeys:[g.key],dieKeys:[...g.dieKeys]}})}}return de(l,c)}function Q(s,a,i,o){const e=(i??[]).filter(u=>!q(u));if(!e.length)return[];const c=e.map(u=>u.testNumber),r=ue(s,a),t=new Map,f=new Map;for(const[u,l]of r){const h=new Uint32Array(e.length),g=new Uint32Array(e.length);for(const p of l)for(let b=0;b<e.length;b++){const y=ee(p,c[b],e[b]);y!==void 0&&(g[b]++,y&&h[b]++)}t.set(u,h),f.set(u,g)}const n=[];for(const u of a){const l=t.get(u.key),h=f.get(u.key);for(let g=0;g<e.length;g++){let p=0,b=0;for(const[v,D]of f)v!==u.key&&(b+=D[g],p+=t.get(v)[g]);const y=h[g],M=l[g];if(y<o.minimumSampleSize||b<o.minimumSampleSize)continue;const k=M/y,m=p/b,S=k-m,$=A(M,y,p,b),w=c[g];n.push({id:`functional:${w}:${u.key}`,level:"wafer",severity:"info",variable:{kind:"functionalTest",index:w,label:`${x(e[g],w)} pass rate`,...te(e[g])},comparison:{family:u.family,left:u.label,right:Y(u.family)},effect:{direction:S===0?"different":S>0?"higher":"lower",absoluteDelta:S,relativeDelta:m===0?void 0:S/m,effectSize:S},stats:{method:"two-proportion-z",pValue:$,sampleSizeLeft:y,sampleSizeRight:b},summary:ze(u.label,x(e[g],w),S,u.family),highlight:{kind:"region",regionFamily:u.family,regionKeys:[u.key],dieKeys:[...u.dieKeys]}})}}return de(n,o)}function ge(s,a){return s.length<2?0:s.reduce((i,o)=>i+(o-a)**2,0)/(s.length-1)}function Be(s,a,i,o,e,c){const r=Math.sqrt(i/s+c/o),t=a-e;if(!Number.isFinite(r)||r===0)return{pValue:1,effectSize:0,delta:t};const f=t/r,n=Math.sqrt(Math.max(0,(i+c)/2)),u=n===0?t:t/n;return{pValue:we(2*(1-Se(Math.abs(f)))),effectSize:u,delta:t}}function tt(s,a){const i=ne(s),o=ne(a);return Be(s.length,i,ge(s,i),a.length,o,ge(a,o))}const Fe=250;function Ne(s,a,i){const o=r=>r.filter(t=>q(i?.find(f=>f.testNumber===t)));if(a)return{testNumbers:o(a.slice().sort((r,t)=>r-t))};const e=new Set;let c=!1;e:for(const r of s)for(const t of Object.keys(r.testValues??{})){const f=Number(t);if(!e.has(f)&&(e.add(f),e.size>Fe)){c=!0;break e}}if(c){const r=`Test-value analysis was skipped: more than ${Fe} tests were found in the die data, so no test findings were computed. Pass testNumbers: [...] to analyse specific tests.`;return console.warn(`[wafermap] analyzeWaferMap: ${r}`),{testNumbers:[],warning:{code:"test-count-capped",message:r,severity:"warning"}}}return{testNumbers:o([...e].sort((r,t)=>r-t))}}function U(s,a,i,o){const e=Ne(s,o.testNumbers,i);if(e.warning)return{findings:[],warning:e.warning};const c=e.testNumbers,r=new Map;for(let p=0;p<a.length;p++)for(const b of a[p].dieKeys)r.set(b,p);const t=a.length,f=[],n=[];for(const p of s){const b=r.get(T(p));b!==void 0&&(f.push(p),n.push(b))}const u=[],l=new Float64Array(t),h=new Float64Array(t),g=new Float64Array(t);for(const p of c){l.fill(0),h.fill(0),g.fill(0);let b=0,y=0,M=0,k;for(let m=0;m<f.length;m++){const $=f[m].testValues?.[p];if($===void 0)continue;k===void 0&&(k=$);const w=$-k,v=n[m];l[v]+=1,h[v]+=w,g[v]+=w*w,b+=1,y+=w,M+=w*w}if(k!==void 0)for(let m=0;m<t;m++){const S=l[m],$=b-S;if(S<o.minimumSampleSize||$<o.minimumSampleSize)continue;const w=h[m]/S,D=(y-h[m])/$,z=w+k,R=D+k,F=S>1?Math.max(0,g[m]-S*w*w)/(S-1):0,d=$>1?Math.max(0,M-g[m]-$*D*D)/($-1):0,{pValue:N,effectSize:B,delta:V}=Be(S,z,F,$,R,d),P=a[m],{label:H,unit:G,...X}=et(p,i),J=R!==0?V/Math.abs(R):void 0;u.push({id:`test:${p}:${P.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:p,label:H,unit:G,...X},comparison:{family:P.family,left:P.label,right:Y(P.family)},effect:{direction:V===0?"different":V>0?"higher":"lower",absoluteDelta:V,relativeDelta:J,effectSize:B},stats:{method:"welch-z-approx",pValue:N,sampleSizeLeft:S,sampleSizeRight:$},summary:Me(P.label,H,V,J,P.family,G),highlight:{kind:"region",regionFamily:P.family,regionKeys:[P.key],dieKeys:[...P.dieKeys]}})}}return fe(u),{activeTestNumbers:c,findings:u.filter(p=>{const b=p.stats.adjustedPValue??p.stats.pValue??1,y=Math.abs(p.effect.effectSize??0);return b<=o.significanceLevel&&y>=o.minimumEffectSize}).map(p=>({...p,severity:le(p.stats.adjustedPValue??p.stats.pValue??1,p.effect.effectSize??0)}))}}function nt(s,a,i,o){if(!i?.length)return[];const e=i.filter(t=>q(t)&&(t.limitLow!==void 0||t.limitHigh!==void 0));if(!e.length)return[];const c=[],r=new Map(s.map(t=>[T(t),t]));for(const t of e){const f=t.testNumber;for(const n of a){const u=new Map;for(const h of n){const g=[];for(const p of h.dieKeys){const b=r.get(p);b&&g.push(b)}u.set(h.key,g)}const l=[];for(const h of n){const g=u.get(h.key),p=[];for(const[z,R]of u)if(z!==h.key)for(const F of R)p.push(F);const b=z=>z.testValues?.[f]!==void 0,y=z=>{const R=z.testValues?.[f];return R===void 0?!1:t.limitLow!==void 0&&R<t.limitLow||t.limitHigh!==void 0&&R>t.limitHigh},M=g.filter(b),k=p.filter(b);if(M.length<o.minimumSampleSize||k.length<o.minimumSampleSize)continue;const m=M.filter(y).length,S=k.filter(y).length,$=m/M.length,w=S/k.length,v=$-w,D=A(m,M.length,S,k.length);l.push({id:`specLimit:${f}:${h.key}`,level:"wafer",severity:"info",variable:{kind:"test",index:f,label:x(t,f),unit:t.unit,...te(t)},comparison:{family:h.family,left:h.label,right:Y(h.family)},effect:{direction:v===0?"different":v>0?"higher":"lower",absoluteDelta:v,relativeDelta:w===0?void 0:v/w,effectSize:v},stats:{method:"two-proportion-z",pValue:D,sampleSizeLeft:M.length,sampleSizeRight:k.length},summary:`${h.label} spec-fail rate for ${x(t,f)} is ${(Math.abs(v)*100).toFixed(1)} pp ${v>0?"higher":"lower"} than the rest of the wafer`,highlight:{kind:"region",regionFamily:h.family,regionKeys:[h.key],dieKeys:[...h.dieKeys]}})}fe(l),c.push(...l.filter(h=>{const g=h.stats.adjustedPValue??h.stats.pValue??1,p=Math.abs(h.effect.absoluteDelta??0),b=Math.abs(h.effect.relativeDelta??0);return g<=o.significanceLevel&&(p>=o.minimumEffectSize||b>=o.minimumRelativeEffect)}).map(h=>({...h,severity:le(h.stats.adjustedPValue??h.stats.pValue??1,h.effect.effectSize??0)})))}}return c}function O(s){return s.highlight.regionKeys??[]}function it(s){const a=s.variable.bin??s.variable.index??"";return`${s.comparison.family}\0${s.variable.kind}\0${a}\0${s.effect.direction}`}function _(s,a){const i=O(s)[0]??"",o=I(i);return o.family==="ring"?o.ring??0:o.family==="sector"?ye(a).indexOf(o.sector??""):0}function st(s,a,i){if(a==="quadrant"){const r=new Map;for(const u of s){const l=I(O(u)[0]??"").quadrant;l&&r.set(l,u)}const t=[...r.keys()],f=new Set,n=[];for(const u of t){if(f.has(u))continue;const l=[],h=[u];for(f.add(u);h.length;){const g=h.pop();l.push(g);for(const p of t)!f.has(p)&&qe(g,p)&&(f.add(p),h.push(p))}n.push(l.map(g=>r.get(g)))}return n}const o=[...s].sort((r,t)=>_(r,i)-_(t,i)),e=[];let c=[];for(const r of o){if(c.length===0){c.push(r);continue}const t=_(c[c.length-1],i);_(r,i)-t===1?c.push(r):(e.push(c),c=[r])}if(c.length&&e.push(c),a==="sector"&&e.length>1){const t=ye(i).length,f=_(e[0][0],i),n=_(e[e.length-1][e[e.length-1].length-1],i);if(f===0&&n===t-1){const u=e.pop();e[0]=[...u,...e[0]]}}return e}function ot(s,a){const i=s.map(r=>I(O(r)[0]??"").ring).filter(r=>r!==void 0).sort((r,t)=>r-t),o=i[0],e=i[i.length-1];let c="";return o===e&&o===1&&a>1?c=" (core)":o===e&&o===a&&a>1&&(c=" (edge)"),o===e?`Ring ${o}${c}`:`Rings ${o}\u2013${e}`}function at(s,a){const i=s.map(o=>I(O(o)[0]??"").sector??"");return i.length===1?`Sector ${i[0]}`:`Sectors ${i[0]}\u2013${i[i.length-1]}`}function rt(s){const a=s.map(i=>I(O(i)[0]??"").quadrant??"");return a.length===1?a[0]:a.length===2?`Quadrants ${a[0]} & ${a[1]}`:`Quadrants ${a.slice(0,-1).join(", ")} & ${a[a.length-1]}`}function me(s){return[...new Set(s)]}function ft(s,a){const i=s[0],o=i.comparison.family,e=i.variable.kind,c=me(s.flatMap(m=>m.highlight.dieKeys??[])),r=me(s.flatMap(O)),t=new Set(c),f=o==="ring"?ot(s,a.ringCount):o==="sector"?at(s,a.sectorCount):rt(s),n=e==="softBin"?a.softEligibleDies:e==="test"?a.testDies:a.eligibleDies,u=n.filter(m=>t.has(T(m))),l=n.filter(m=>!t.has(T(m)));let h,g,p,b,y;if(e==="test"){const m=i.variable.index,S=d=>d.testValues?.[m],$=u.map(S).filter(d=>d!==void 0),w=l.map(S).filter(d=>d!==void 0),{pValue:v,effectSize:D,delta:z}=tt($,w),R=ne(w),F=R!==0?z/Math.abs(R):void 0;h={direction:z===0?"different":z>0?"higher":"lower",absoluteDelta:z,relativeDelta:F,effectSize:D},g={method:"welch-z-approx",pValue:v,sampleSizeLeft:$.length,sampleSizeRight:w.length},p=le(v,D),b=Me(f,i.variable.label,z,F,o,i.variable.unit),y=`test:${m}`}else if(e==="yield"){const m=new Set(a.passBins),S=u.filter(B=>B.hbin!==void 0),$=l.filter(B=>B.hbin!==void 0),w=S.length,v=$.length,D=S.filter(B=>m.has(B.hbin)).length,z=$.filter(B=>m.has(B.hbin)).length,R=w>0?D/w:0,F=v>0?z/v:0,d=R-F,N=A(D,w,z,v);h={direction:d===0?"different":d>0?"higher":"lower",absoluteDelta:d,relativeDelta:F===0?void 0:d/F,effectSize:d},g={method:"two-proportion-z",pValue:N,sampleSizeLeft:w,sampleSizeRight:v},p=ie(N,d,h.relativeDelta),b=$e(f,d,o),y="yield"}else if(e==="functionalTest"){const m=i.variable.index,S=a.testDefs?.find(N=>N.testNumber===m);let $=0,w=0,v=0,D=0;for(const N of u){const B=ee(N,m,S);B!==void 0&&(w++,B&&$++)}for(const N of l){const B=ee(N,m,S);B!==void 0&&(D++,B&&v++)}const z=w>0?$/w:0,R=D>0?v/D:0,F=z-R,d=A($,w,v,D);h={direction:F===0?"different":F>0?"higher":"lower",absoluteDelta:F,relativeDelta:R===0?void 0:F/R,effectSize:F},g={method:"two-proportion-z",pValue:d,sampleSizeLeft:w,sampleSizeRight:D},p=ie(d,F,h.relativeDelta),b=ze(f,x(S,m),F,o),y=`functional:${m}`}else{const m=i.variable.bin,S=d=>e==="softBin"?d.sbin:d.hbin,$=u.filter(d=>S(d)===m).length,w=l.filter(d=>S(d)===m).length,v=$/u.length,D=w/l.length,z=v-D,R=A($,u.length,w,l.length);h={direction:z===0?"different":z>0?"higher":"lower",absoluteDelta:z,relativeDelta:D===0?void 0:z/D,effectSize:z},g={method:"two-proportion-z",pValue:R,sampleSizeLeft:u.length,sampleSizeRight:l.length},p=ie(R,z,h.relativeDelta);const F=e==="softBin"?a.sbinDefs:a.hbinDefs;b=ke(f,Re(m,F,e==="softBin"?"SBin":"HBin"),z,o),y=`${e}:${m}`}const M=me(r.map(m=>m.replace(`${o}:`,""))).join("-"),k=e==="hardBin"||e==="softBin"?{kind:"bin",bin:i.variable.bin,regionKeys:r,dieKeys:c}:{kind:"region",regionFamily:o,regionKeys:r,dieKeys:c};return{...i,id:`${y}:${o}:${M}`,severity:p,comparison:{family:o,left:f,right:Y(o)},effect:h,stats:g,summary:b,highlight:k,relatedIds:s.map(m=>m.id)}}function lt(s,a){const i=new Set(["ring","quadrant","sector"]),o=[],e=new Map;for(const r of s){if(!i.has(r.comparison.family)){o.push(r);continue}const t=it(r),f=e.get(t)??[];f.push(r),e.set(t,f)}const c=[];for(const r of e.values()){if(r.length===1){c.push(r[0]);continue}const t=r[0].comparison.family;for(const f of st(r,t,a.sectorCount))c.push(f.length===1?f[0]:ft(f,a))}return[...o,...c]}function ct(s){const a=new Map,i=new Map;for(const e of s){const c=T(e);e.hbin!==void 0&&e.hbin!==null&&(a.get(e.hbin)??a.set(e.hbin,new Set).get(e.hbin)).add(c),e.sbin!==void 0&&e.sbin!==null&&(i.get(e.sbin)??i.set(e.sbin,new Set).get(e.sbin)).add(c)}const o=new Set;for(const[e,c]of a)for(const[r,t]of i){if(c.size!==t.size)continue;let f=!0;for(const n of c)if(!t.has(n)){f=!1;break}f&&o.add(`${e}:${r}`)}return o}function ut(s){return`${s.comparison.family}\0${s.comparison.left}`}function dt(s,a,i){const o=(t,f)=>{t.absorbedIds=[...t.absorbedIds??[],f.id]},e=new Map;for(const t of s){const f=ut(t);(e.get(f)??e.set(f,[]).get(f)).push(t)}const c=ct(a),r=new Set;for(const t of e.values()){for(const n of t)if(!(n.variable.kind!=="hardBin"||n.variable.bin===void 0))for(const u of t){if(u.variable.kind!=="softBin"||u.variable.bin===void 0||r.has(u.id)||!c.has(`${n.variable.bin}:${u.variable.bin}`)||u.effect.direction!==n.effect.direction)continue;o(n,u),r.add(u.id);const l=n.variable.label.replace(/^HBin\s+/,""),h=u.variable.label.replace(/^SBin\s+/,""),g=l===h?`HBin and SBin ${l} (same dies)`:`${n.variable.label} and ${u.variable.label} (same dies)`;n.summary=n.summary.replace(n.variable.label,g),n.variable.label=g;break}if(i.length!==1)continue;const f=t.find(n=>n.variable.kind==="yield");if(f){for(const n of t)if(!(n===f||r.has(n.id)||!((n.variable.kind==="hardBin"||n.variable.kind==="softBin")&&n.variable.bin===i[0]))){o(f,n),r.add(n.id);for(const l of n.absorbedIds??[])f.absorbedIds=[...f.absorbedIds??[],l]}}}}export function analyzeWaferMap(s,a={}){const{resolved:i,warnings:o}=We(a),e=Le(s),c={...i,passBins:e.passBins??j.passBins,ringCount:e.ringCount??j.ringCount},r=e.isLotStack,t=e.aggrMethod,f=!r||t==="mode"||t==="countBin"||t==="percent",n=e.dies.filter(d=>Ue(d)),u=n.filter(be),l=Qe(c,n.length),h=e.dies.filter(d=>ae(d)),g=h.filter(be),p=Ae(g,e.wafer,l.ringCount),b=xe(g,e.wafer,l.ringCount),y=je(g,e.reticleConfig),M=He(h,!1),k=Oe(g,e.wafer,l.sectorCount),m=[];f&&m.push(...W(n,p,l.passBins,l),...W(n,b,l.passBins,l),...W(n,y,l.passBins,l),...W(n,M,l.passBins,l),...W(n,k,l.passBins,l)),f&&m.push(...L(n,p,"hard",e.hbinDefs,"hardBin",l),...L(n,b,"hard",e.hbinDefs,"hardBin",l),...L(n,y,"hard",e.hbinDefs,"hardBin",l),...L(n,M,"hard",e.hbinDefs,"hardBin",l),...L(n,k,"hard",e.hbinDefs,"hardBin",l));{const d=n.filter(N=>N.sbin!==void 0);m.push(...L(d,p,"soft",e.sbinDefs,"softBin",l),...L(d,b,"soft",e.sbinDefs,"softBin",l),...L(d,y,"soft",e.sbinDefs,"softBin",l),...L(d,M,"soft",e.sbinDefs,"softBin",l),...L(d,k,"soft",e.sbinDefs,"softBin",l))}const S=[...o];let $;if(l.enableTestValueAnalysis){const d=U(n,p,e.testDefs,l),N=U(n,b,e.testDefs,l),B=U(n,y,e.testDefs,l),V=U(n,M,e.testDefs,l),P=U(n,k,e.testDefs,l);m.push(...d.findings,...N.findings,...B.findings,...V.findings,...P.findings),d.warning&&S.push(d.warning),$=d.activeTestNumbers,m.push(...nt(n,[p,b,y,M,k],e.testDefs,l)),m.push(...Q(n,p,e.testDefs,l),...Q(n,b,e.testDefs,l),...Q(n,y,e.testDefs,l),...Q(n,M,e.testDefs,l),...Q(n,k,e.testDefs,l))}{const d=Ge(r,f,e.testDefs);(!r||f||d!==void 0)&&m.push(...Ie(u,e.wafer,{...l,isFailingDie:d}))}const w=lt(m,{eligibleDies:n,softEligibleDies:n.filter(d=>d.sbin!==void 0),testDies:n,passBins:l.passBins,ringCount:l.ringCount,sectorCount:l.sectorCount,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs});m.length=0,m.push(...w);let v;if(f){const d=Ye(u,e.wafer,{passBins:l.passBins,ringCount:l.ringCount});if(v=d??void 0,d!==null&&d.pattern!=="random"&&d.pattern!=="none"){let Ve=function(C){const E=O(C).map(K=>I(K).ring).filter(K=>K!==void 0);if(E.length===0)return!0;const he=E.some(K=>K===Ce),pe=E.some(K=>K===1);return Z==="edge-ring"||Z==="edge-local"?he:Z==="center"?pe:Z==="donut"?!he&&!pe:!0};const B={center:"Center cluster",donut:"Donut","edge-ring":"Edge-ring","edge-local":"Edge-local",scratch:"Scratch","near-full":"Near-full"}[d.pattern]??d.pattern,V=d.confidence==="high"?"unusual":d.confidence==="medium"?"notable":"info",P=d.features,H=C=>`${(C*100).toFixed(0)}%`,G=d.pattern==="edge-ring"||d.pattern==="edge-local"?`${H(P.edgeRdd)} of edge dies failing`:d.pattern==="center"||d.pattern==="donut"?`centroid at ${H(P.centroidDistNorm)} of wafer radius from centre`:d.pattern==="scratch"?`linear score ${P.linearScore.toFixed(2)}, eccentricity ${P.eccentricity.toFixed(2)}`:`${H(P.globalRdd)} of dies failing`,X=n.filter(C=>{const E=C.hbin??C.sbin;return E!==void 0&&!new Set(l.passBins).has(E)}),J={"edge-ring":["ring","edge-arc"],"edge-local":["edge-arc","sector","quadrant"],center:["ring","cluster"],donut:["ring"],scratch:["cluster","sector","quadrant"],"near-full":["ring"]},Pe=new Set(J[d.pattern]??[]),Z=d.pattern,Ce=l.ringCount,oe=[];for(const C of m){if(!Pe.has(C.comparison.family)||C.comparison.family==="ring"&&!Ve(C))continue;oe.push(C.id);const E=C.comparison.family==="ring"||C.comparison.family==="quadrant"||C.comparison.family==="sector";V!=="info"&&E&&(C.severity="info")}m.push({id:`spatial-pattern:${d.pattern}`,level:"wafer",severity:V,variable:{kind:"spatialPattern",label:B},comparison:{family:"spatial-pattern",left:B,right:"Wafer"},effect:{direction:"different",effectSize:P.globalRdd},stats:{method:"geometry",sampleSizeLeft:X.length,sampleSizeRight:n.length},summary:`Spatial pattern: ${B.toLowerCase()} (${d.confidence} confidence) \u2014 ${G}${d.note?` [${d.note}]`:""}`,highlight:{kind:"dies",dieKeys:X.map(C=>T(C))},relatedIds:oe.length>0?oe:void 0})}}m.sort((d,N)=>{const B=d.stats.adjustedPValue??d.stats.pValue??1,V=N.stats.adjustedPValue??N.stats.pValue??1;return B!==V?B-V:Math.abs(N.effect.absoluteDelta??0)-Math.abs(d.effect.absoluteDelta??0)});const D=Xe(e.dies,n.length,e.yield.yieldPercent);r&&(D.isLotStack=!0,t&&(D.aggregationMethod=t));const z=Je(e.dies,e.testDefs);z&&(D.testSpecYield=z);const R=computeFunctionalYield(e.dies,e.testDefs);if(R&&(D.functionalYield=R),v&&(D.spatialPattern=v),$===void 0&&l.computePerTestStats){const d=Ne(n,l.testNumbers,e.testDefs);d.warning&&S.push(d.warning),$=d.testNumbers}if($?.length){const d=Ze(e.dies,$,e.testDefs,l.minimumSampleSize);d&&(D.perTestStats=d);const N=Ee([{dies:e.dies}],e.testDefs,$);N&&(D.capability=N)}Object.assign(D,Ke(e.dies,e.testDefs));const F=Te([{dies:e.dies,wafer:e.wafer,passBins:l.passBins,ringCount:l.ringCount}]);return F&&(D.regionYield=F),S.length>0&&(D.warnings=S),dt(m,n,l.passBins),{level:"wafer",hasNotableFindings:m.some(d=>d.severity==="notable"||d.severity==="unusual"),findings:m,wafer:e.wafer.metadata??void 0,stats:D}}