@wafertools/wafermap 0.26.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -2
- package/CHANGELOG.md +670 -0
- package/README.md +46 -3
- package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
- package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
- package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
- package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
- package/dist/packages/canvas-adapter/charts/binCluster.d.ts +0 -1
- package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
- package/dist/packages/canvas-adapter/charts/boxplot.d.ts +21 -2
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -11
- package/dist/packages/canvas-adapter/charts/capability.js +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -10
- package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
- package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -2
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +0 -1
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
- package/dist/packages/canvas-adapter/charts/scatter.d.ts +6 -1
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
- package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
- package/dist/packages/canvas-adapter/charts/trend.d.ts +37 -0
- package/dist/packages/canvas-adapter/charts/trend.js +1 -0
- package/dist/packages/canvas-adapter/dieList.js +7 -7
- package/dist/packages/canvas-adapter/hitGrid.d.ts +22 -0
- package/dist/packages/canvas-adapter/hitGrid.js +1 -0
- package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
- package/dist/packages/canvas-adapter/identityHeader.js +1 -0
- package/dist/packages/canvas-adapter/index.d.ts +2 -1
- package/dist/packages/canvas-adapter/index.js +1 -1
- package/dist/packages/canvas-adapter/insightsTab.d.ts +51 -5
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/maplessSummary.d.ts +5 -1
- package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +110 -18
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +233 -37
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +352 -15
- package/dist/packages/canvas-adapter/toolbar.js +2 -2
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +115 -41
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/canvas-adapter/warnings.d.ts +21 -0
- package/dist/packages/canvas-adapter/warnings.js +1 -1
- package/dist/packages/core/dies.d.ts +11 -0
- package/dist/packages/core/dies.js +1 -1
- package/dist/packages/core/utils.d.ts +11 -0
- package/dist/packages/core/utils.js +1 -1
- package/dist/packages/renderer/binColors.d.ts +80 -0
- package/dist/packages/renderer/binColors.js +1 -0
- package/dist/packages/renderer/buildView.d.ts +100 -6
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +112 -7
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/colorMap.d.ts +16 -15
- package/dist/packages/renderer/colorMap.js +1 -1
- package/dist/packages/renderer/colorSchemes.d.ts +45 -26
- package/dist/packages/renderer/colorSchemes.js +1 -1
- package/dist/packages/renderer/index.d.ts +3 -1
- package/dist/packages/renderer/index.js +1 -1
- package/dist/packages/stats/analyzeWaferLot.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/binPareto.d.ts +15 -0
- package/dist/packages/stats/binPareto.js +1 -1
- package/dist/packages/stats/clusterDetection.js +1 -1
- package/dist/packages/stats/connectedComponents.d.ts +14 -0
- package/dist/packages/stats/connectedComponents.js +1 -0
- package/dist/packages/stats/correlation.d.ts +26 -0
- package/dist/packages/stats/correlation.js +1 -1
- package/dist/packages/stats/facets.js +1 -1
- package/dist/packages/stats/filterFindings.d.ts +18 -0
- package/dist/packages/stats/filterFindings.js +1 -1
- package/dist/packages/stats/findingsNarrative.js +1 -1
- package/dist/packages/stats/histogram.d.ts +47 -2
- package/dist/packages/stats/histogram.js +1 -1
- package/dist/packages/stats/index.d.ts +6 -0
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
- package/dist/packages/stats/mergeTestDefs.js +1 -0
- package/dist/packages/stats/metadataColumns.d.ts +20 -4
- package/dist/packages/stats/metadataColumns.js +1 -1
- package/dist/packages/stats/patternClassification.js +1 -1
- package/dist/packages/stats/population.d.ts +23 -0
- package/dist/packages/stats/population.js +1 -0
- package/dist/packages/stats/renderFindingsReport.js +15 -15
- package/dist/packages/stats/renderSummaryReport.js +35 -32
- package/dist/packages/stats/reportHtml.js +38 -13
- package/dist/packages/stats/testPassRate.d.ts +85 -0
- package/dist/packages/stats/testPassRate.js +1 -0
- package/dist/packages/stats/trend.d.ts +40 -0
- package/dist/packages/stats/trend.js +1 -0
- package/dist/packages/stats/types.d.ts +117 -6
- package/dist/packages/stats/yield.js +1 -1
- package/llms.txt +6 -0
- package/package.json +9 -5
- package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
- package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
|
@@ -3,11 +3,29 @@ import type { Die, PositionedDie } from '../core/dies.js';
|
|
|
3
3
|
import type { Reticle } from '../core/reticle.js';
|
|
4
4
|
import type { DieMetadata, WaferMetadata } from '../core/metadata.js';
|
|
5
5
|
import { type Affine } from '../core/transforms.js';
|
|
6
|
+
import { type BinColors } from './binColors.js';
|
|
6
7
|
import type { TestDef, BinDef, MetadataFieldDef, ReticleConfig } from './buildWaferMap.js';
|
|
7
8
|
/** Resolve a toolbar activeTest cursor to the canonical test number for getDieTestValue. */
|
|
8
9
|
export declare function resolveTestNumber(activeTest: number, testDefs?: TestDef[]): {
|
|
9
10
|
testNumber: number;
|
|
10
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* The distinct metadata values a `metadata`-mode view will colour, in the exact
|
|
14
|
+
* order colour indices are assigned to them.
|
|
15
|
+
*
|
|
16
|
+
* The one implementation of that ordering, because two existed and they only
|
|
17
|
+
* agreed by luck: `buildView` derived it per view while `renderWaferGallery`'s
|
|
18
|
+
* lot-wide legend strip derived its own from `String(raw)` over every card —
|
|
19
|
+
* a different value derivation (`metadataCategoricalValue` formats numbers and
|
|
20
|
+
* rejects objects) as well as a different population. A comment asked the two to
|
|
21
|
+
* stay in step; nothing made them.
|
|
22
|
+
*
|
|
23
|
+
* Natural order, so D0, D1, D2, D10 reads correctly instead of the
|
|
24
|
+
* lexicographic D0, D1, D10, D2. Partial and edge-excluded dies are skipped:
|
|
25
|
+
* they never reach the metadata fill branch, so letting them rank values here
|
|
26
|
+
* would shift colours for values no visible die shows.
|
|
27
|
+
*/
|
|
28
|
+
export declare function collectMetadataValues(dies: readonly Die[], key: string): string[];
|
|
11
29
|
/** Find a TestDef by its testNumber. Returns undefined when testDefs is absent or no match. */
|
|
12
30
|
export declare function findTestDef(testDefs: TestDef[] | undefined, testNumber: number): TestDef | undefined;
|
|
13
31
|
/** Return sorted unique test numbers present across a set of dies (from testValues keys). */
|
|
@@ -75,14 +93,54 @@ export interface ViewOverlay {
|
|
|
75
93
|
lineWidth: number;
|
|
76
94
|
fill?: string;
|
|
77
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* A renderer-agnostic description of one drawn wafer map: everything `toCanvas`
|
|
98
|
+
* needs, in physical (mm) coordinates, with no canvas or DOM dependency.
|
|
99
|
+
*
|
|
100
|
+
* This is the contract between `buildView` and a renderer — see the `pipeline`
|
|
101
|
+
* example for the low-level path. Hosts using `buildWaferMap` + `renderWaferMap`
|
|
102
|
+
* never need it: that pipeline builds and re-builds the view internally, and the
|
|
103
|
+
* copy hung on `WaferMapResult.view` is marked `@internal` precisely because
|
|
104
|
+
* reading it there means reaching past the API that owns it.
|
|
105
|
+
*
|
|
106
|
+
* Treat the geometry arrays as read-only. `dies` and `hoverPoints` are
|
|
107
|
+
* **index-parallel** — `hoverPoints[i]` is the transformed centre of `dies[i]` —
|
|
108
|
+
* and code that resolves a die from a screen position depends on that staying
|
|
109
|
+
* true.
|
|
110
|
+
*/
|
|
78
111
|
export interface View {
|
|
112
|
+
/** Die rectangles to fill, in draw order, already carrying their resolved colour. */
|
|
79
113
|
rectangles: ViewRect[];
|
|
114
|
+
/**
|
|
115
|
+
* Transformed centre of each die, index-parallel with {@link dies}. Separate
|
|
116
|
+
* from `rectangles` because a die always has a centre for hit-testing and
|
|
117
|
+
* viewport fitting even when it is not drawn (a partial die with
|
|
118
|
+
* `showPartialDies: false`).
|
|
119
|
+
*/
|
|
80
120
|
hoverPoints: ViewHoverPoint[];
|
|
121
|
+
/** Text to draw — die labels and any axis/indicator strings the view generated. */
|
|
81
122
|
texts: ViewText[];
|
|
123
|
+
/** Non-die geometry: wafer outline, notch, ring/quadrant boundaries, reticle grid, XY indicator. */
|
|
82
124
|
overlays: ViewOverlay[];
|
|
125
|
+
/** The plot mode this view was built for. Colours in `rectangles` follow it. */
|
|
83
126
|
plotMode: PlotMode;
|
|
84
|
-
|
|
127
|
+
/** Bin palette used for hard/soft-bin modes, as registered with `registerBinColorScheme`. */
|
|
128
|
+
binColorScheme: string;
|
|
129
|
+
/** Value gradient used for value and stacked modes, as registered with `registerValueColorScheme`. */
|
|
130
|
+
valueColorScheme: string;
|
|
131
|
+
/**
|
|
132
|
+
* Every bin's resolved colour, both bin types — the single source for bin
|
|
133
|
+
* colour. Die fills are built from it, and legends, panels and charts must
|
|
134
|
+
* read it rather than re-deriving a colour, or they can disagree with the map.
|
|
135
|
+
*/
|
|
136
|
+
binColors: BinColors;
|
|
137
|
+
/** Wafer-level metadata carried through from the wafer, or `null`. */
|
|
85
138
|
metadata: WaferMetadata | null;
|
|
139
|
+
/**
|
|
140
|
+
* The positioned dies this view was built from, index-parallel with
|
|
141
|
+
* {@link hoverPoints}. Original grid coordinates are preserved on each die —
|
|
142
|
+
* never derive display coordinates from these.
|
|
143
|
+
*/
|
|
86
144
|
dies: Die[];
|
|
87
145
|
/** Actual [min, max] of the value data used for color normalization. */
|
|
88
146
|
valueRange: [number, number];
|
|
@@ -220,8 +278,22 @@ export interface ViewOptions {
|
|
|
220
278
|
showQuadrantBoundaries?: boolean;
|
|
221
279
|
showXYIndicator?: boolean;
|
|
222
280
|
dieGap?: number;
|
|
223
|
-
/**
|
|
224
|
-
|
|
281
|
+
/** Bin palette for `hardBin`/`softBin` — any name registered via `registerBinColorScheme()`. Default `'default'`. */
|
|
282
|
+
binColorScheme?: string;
|
|
283
|
+
/** Value gradient for `value` and the stacked modes — any name registered via `registerValueColorScheme()`. Default `'default'`. */
|
|
284
|
+
valueColorScheme?: string;
|
|
285
|
+
/** Honour `BinDef.color` where a bin definition supplies one. Default true. See `resolveBinColors`. */
|
|
286
|
+
useDefinedBinColors?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* Bin colours resolved over a WIDER population than these dies — a host
|
|
289
|
+
* showing several wafers together passes one `resolveBinColors` result to
|
|
290
|
+
* every map so a bin is the same colour on each (`renderWaferGallery` does
|
|
291
|
+
* this for you). The bin counterpart of `metadataValueOrder`.
|
|
292
|
+
*
|
|
293
|
+
* Ignored, and colours resolved from these dies instead, when it lacks any
|
|
294
|
+
* bin present here: a stale assignment must never leave a real bin uncoloured.
|
|
295
|
+
*/
|
|
296
|
+
binColors?: BinColors;
|
|
225
297
|
highlightBin?: number;
|
|
226
298
|
/** Dim every die except this metadata value, `'metadata'` mode's analogue of `highlightBin`. */
|
|
227
299
|
highlightMetadataValue?: string;
|
|
@@ -246,6 +318,27 @@ export interface ViewOptions {
|
|
|
246
318
|
test: number;
|
|
247
319
|
range: [number, number];
|
|
248
320
|
};
|
|
321
|
+
/**
|
|
322
|
+
* The full, ordered list of metadata values to assign colours from, for
|
|
323
|
+
* `metadata` plot mode — the exact counterpart of `valueRange`'s `{ test,
|
|
324
|
+
* range }` form, and applied only when `key` matches `activeMetadataKey`.
|
|
325
|
+
*
|
|
326
|
+
* Without it, each view assigns colours by index into the values present on
|
|
327
|
+
* ITS OWN dies. That is right for a standalone map and wrong for a gallery:
|
|
328
|
+
* a wafer that never exhibited one defect category has a shorter list, so
|
|
329
|
+
* every later value shifts up a colour and the card disagrees with the
|
|
330
|
+
* lot-wide legend beside it about what that colour means. A host showing
|
|
331
|
+
* several wafers together passes the lot-wide order here so every card
|
|
332
|
+
* indexes into the same list.
|
|
333
|
+
*
|
|
334
|
+
* Values present on these dies but absent from the list are appended (in
|
|
335
|
+
* natural order) rather than dropped, so a short or stale list can only cost
|
|
336
|
+
* the shared ordering, never leave real dies uncoloured.
|
|
337
|
+
*/
|
|
338
|
+
metadataValueOrder?: {
|
|
339
|
+
key: string;
|
|
340
|
+
values: string[];
|
|
341
|
+
};
|
|
249
342
|
/**
|
|
250
343
|
* Controls the default colorbar range when the active testDef has spec limits.
|
|
251
344
|
* `'spec'` (default when limits present): colorbar spans [limitLow, limitHigh].
|
|
@@ -255,8 +348,8 @@ export interface ViewOptions {
|
|
|
255
348
|
/**
|
|
256
349
|
* Which bins count as passing. Default `[1]`.
|
|
257
350
|
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
351
|
+
* Sets `ViewRect.binFail` and decides which bins take the palette's pass
|
|
352
|
+
* colours (see `resolveBinColors`) — never the bin number. Must match the
|
|
260
353
|
* `passBins` given to `buildWaferMap` / `analyzeWaferMap`, or the map will
|
|
261
354
|
* disagree with the yield figure beside it.
|
|
262
355
|
*/
|
|
@@ -370,7 +463,6 @@ export interface HoverTextOptions {
|
|
|
370
463
|
aggrMethod?: string;
|
|
371
464
|
/** Number of wafers in the stack, used to annotate a count with its share of the lot. */
|
|
372
465
|
lotSize?: number;
|
|
373
|
-
waferMeta?: WaferMetadata | null;
|
|
374
466
|
/** Active test number (value mode only) — leads the tooltip and gets an out-of-spec note. */
|
|
375
467
|
activeTest?: number;
|
|
376
468
|
/** Reticle geometry, when configured — appended as a "Reticle (col, row)" line below Die (x, y). */
|
|
@@ -425,6 +517,8 @@ export declare function generateTextOverlay(dies: PositionedDie[], txCoords: Flo
|
|
|
425
517
|
/** Active `die.metadata` key + its resolved value→color map, `'metadata'` mode only. */
|
|
426
518
|
activeMetadataKey?: string;
|
|
427
519
|
metadataColorMap?: Map<string, string> | null;
|
|
520
|
+
/** Mirrors `View.allIntegerValues` — see the colorbar's own `tickFmt` override in toCanvas.ts. */
|
|
521
|
+
allIntegerValues?: boolean;
|
|
428
522
|
}): ViewText[];
|
|
429
523
|
/**
|
|
430
524
|
* Build a renderer-agnostic scene from a wafer, dies, and display options.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{hasPosition as Zt}from"../core/dies.js";import{getReticleCell as te}from"../core/reticle.js";import{metadataCategoricalValue as ee,metadataDisplayValue as ne}from"../core/metadata.js";import{affineRotation as dt,affineMirror as Rt,affineCompose as it,affinePoint as Y,affineVector as ht,affineSwapsAxes as ae}from"../core/transforms.js";import{contrastTextColor as G,SPEC_PASS_FILL as pt,SPEC_FAIL_LOW as ie,SPEC_FAIL_HIGH as mt}from"./colorMap.js";import{getColorScheme as se}from"./colorSchemes.js";import{getDieTestValue as R,getTestPassStatus as Q,isParametricTest as yt}from"./buildWaferMap.js";import{fmt as gt,fmtColorbarAxis as bt,fmtAggregationMethod as Ct}from"./fmt.js";import{metadataValueColor as oe}from"./colorMap.js";import{compareNatural as re,clamp01 as Nt}from"../core/utils.js";import{prettyKey as Ht}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function findTestDef(t,e){return t?.find(a=>a.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,a)=>e-a)}const le="#d3d6db",Ot="#e8e9ea",ce="#aab0ba",j="#d6d9dd";function st(t,e){return e?ee(t.metadata?.[e]):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function ue(t,e,a){const{x:i,y:s}=t.center,n=dt(t.orientation,i,s),o=it(Rt(e?.x??!1,e?.y??!1,i,s),dt(t.orientation,i,s)),r=it(Rt(a?.flipX??!1,a?.flipY??!1,i,s),dt(a?.rotation??0,i,s));return{bakedToScreen:r,physicalToScreen:it(r,n),gridToScreen:it(r,o)}}function W(t,e=!1){return{points:[t],closed:e}}function fe(t,e){const{center:a,radius:i,notch:s}=t;let n=a.x+i*Math.cos(e),o=a.y+i*Math.sin(e);if(!s)return{x:n,y:o};const r=Math.sqrt(i**2-(s.length/2)**2),u=s.length/2,b=n-a.x,y=o-a.y;return s.type==="bottom"&&y<-r?(o=a.y-r,n=a.x+Math.max(-u,Math.min(u,b))):s.type==="top"&&y>r?(o=a.y+r,n=a.x+Math.max(-u,Math.min(u,b))):s.type==="left"&&b<-r?(n=a.x-r,o=a.y+Math.max(-u,Math.min(u,y))):s.type==="right"&&b>r&&(n=a.x+r,o=a.y+Math.max(-u,Math.min(u,y))),{x:n,y:o}}function At(t,e){return t.map(e).join(" / ")}function de(t,e=256){const a=Array.from({length:e},(i,s)=>t(s/(e-1)));return i=>a[Math.min(e-1,Math.max(0,Math.round(i*(e-1))))]}function he(t,e){const a=Math.max(1,Math.min(t.width,t.height)),i=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(a*.55,i*1.8))))}function pe(t,e,a){if(e?.length){const i=e.flatMap(s=>{const n=s.testNumber;if(!yt(s)){const u=Q(t,n,s);return u===void 0?[]:[{key:n,label:s.name,value:u?"Pass":"Fail"}]}const o=R(t,n);if(o===void 0)return[];const r=t.testPass?.[n]===!1?" <i>(recorded fail)</i>":"";return[{key:n,label:s.name,value:`${gt(o,s.unit,a)}${r}`}]});if(i.length)return i}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:gt(s,void 0,a)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,a={}){const{testDefs:i,hbinDefs:s,sbinDefs:n,fallbackFormat:o,aggrMethod:r,lotSize:u,waferMeta:b,activeTest:y,reticleConfig:B,metadataFields:V}=a,v=s?new Map(s.map(c=>[c.bin,c])):null,T=n?new Map(n.map(c=>[c.bin,c])):null,m=[`Die (${t.x}, ${t.y})`];if(B&&Zt(t)){const c=te(t,B);m.push(`Reticle (${c.column}, ${c.row})`)}if(e==="stackedValues"){const c=R(t,0);if(c!==void 0){const f=i?.[0],h=f?.testNumber,g=f?.name??(h!=null?`Test ${h}`:"Value"),M=r?` (${r})`:"";m.push(`${g}${M}: ${gt(c,f?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const c=R(t,0),f=e==="stackedSoftBins"?t.sbin:t.hbin;if(c!==void 0){const h=e==="stackedSoftBins"?T:v,g=f!==void 0?h?.get(f)?.name?`${f} \xB7 ${h.get(f).name}`:`Bin ${f}`:"Bin";let M;if(r==="percent")M=`${c.toFixed(0)}%`;else{const H=u?` (${(c/u*100).toFixed(0)}% of lot)`:"";M=`${c}${H}`}const p=r?` [${Ct(r)}]`:"";m.push(`${g}: ${M}${p}`)}}else{const c=pe(t,i,o);if(e==="value"&&c.length){const{testNumber:f}=resolveTestNumber(y??0,i),h=findTestDef(i,f),g=R(t,f);let M=c.findIndex(E=>E.key===f);M<0&&(M=0);const p=c[M];let H=`<b>${p.label}: ${p.value}</b>`;if(p.key===f&&h){const E=classifySpec(g,h);(E==="failLow"||E==="failHigh")&&(H+=" <i>(out of spec)</i>")}m.push(H);const A=c.length-1;A>0&&m.push(`<i>+${A} more test${A===1?"":"s"}</i>`)}else if(c.length){const f=c.length;m.push(`<i>${f} test result${f===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const f=[];if(t.hbin!==void 0){const h=v?.get(t.hbin)?.name,g=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);f.push(`HBin: ${g}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,g=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);f.push(`SBin: ${g}`)}m.push(f.join(" \xB7 "))}}t.retestCount!==void 0&&m.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&m.push(`Site: ${t.siteNum}`),t.partId!==void 0&&m.push(`Part ID: ${t.partId}`),t.partial&&m.push("<i>partial die</i>"),t.probeIndex!==void 0&&m.push(`Probe: #${t.probeIndex}`);const S={...b??{},...t.metadata??{}};for(const[c,f]of Object.entries(S)){const h=ne(f);if(h===void 0)continue;const g=V?.find(M=>M.key===c)?.label??Ht(c);m.push(`${g}: ${h}`)}return m.join("<br>")}export function buildMapTitle(t,e="engineering",a){const i=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=n=>{const o=a?.length===1?a[0]:void 0;return o===void 0?`${n} Bin`:o.name?`${n} Bin ${o.bin} \xB7 ${o.name}`:`${n} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:i};case"stackedSoftBins":return{primary:s("Soft"),secondary:i};case"metadata":{const n=t.metadataFields?.find(r=>r.key===t.activeMetadataKey),o=t.activeMetadataKey?Ht(t.activeMetadataKey):"Metadata";return{primary:n?.label??o,secondary:""}}case"stackedValues":{const n=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:r}=bt(o,n?.name,n?.unit,e),u=r||"Value",b=t.aggrMethod?` \xB7 ${Ct(t.aggrMethod)}`:"";return{primary:`${u}${b}`,secondary:i}}default:{const n=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:r}=bt(o,n?.name,n?.unit,e),u=r||n?.name;if(t.passFailDisplay!=="off"){const b=n?.testNumber??t.activeTest,y=u?`${u} \xB7 #${b}`:`Test ${b}`,B=t.passFailDisplay==="spec"?"Spec pass/fail":n!==void 0&&!yt(n)?"Functional pass/fail":"Tester pass/fail";return{primary:y,secondary:B}}return{primary:u??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,a){const{plotMode:i,colorFns:s,normalize:n,activeTest:o,valueRange:r,testDefs:u,fallbackFormat:b,passFailDisplay:y="off",activeMetadataKey:B,metadataColorMap:V}=a,v=findTestDef(u,o),{tickFmt:T}=bt(r[1],v?.name,v?.unit,b),{testNumber:m}=resolveTestNumber(o,u);return t.flatMap((S,c)=>{let f="",h="#111111";if(i==="value"&&y==="test"){const p=Q(S,m,v);if(p===void 0)return[];f=p?"P":"F",h=G(p?pt:mt)}else if(i==="value"){const p=R(S,m);if(p===void 0)return[];f=At([p],T),h=G(s.forValue(n(p)))}else if(i==="hardBin"||i==="softBin"){const p=i==="softBin"?S.sbin:S.hbin;if(p===void 0)return[];f=String(p),h=G(s.forBin(p))}else if(i==="metadata"){const p=st(S,B);if(p===void 0)return[];f=p,h=G(V?.get(f)??j)}else{const p=R(S,0);if(p===void 0)return[];f=At([p],T),h=G(s.forValue(n(p)))}const g=e?e[c*2]:S.physX,M=e?e[c*2+1]:S.physY;return[{x:g,y:M,text:f,fontSize:he(S,f),color:h,align:"center"}]})}function Et(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function me(t,e,a=720){const{center:i,radius:s,notch:n}=t,o=m=>Y(e,m.x,m.y);if(!(n!==void 0&&t.diameter>150)){const m=[];for(let S=0;S<=a;S++){const c=2*Math.PI*S/a;m.push(o(fe(t,c)))}return[{kind:"wafer-boundary",...W(m,!0),lineColor:"#888888",lineWidth:1}]}const u=1.25,b=n.length,y=Et(n.type),B=Math.atan2(b,s),V=s-u,v=[];let T=!1;for(let m=0;m<=a;m++){const S=2*Math.PI*m/a;let c=S-y;for(;c>Math.PI;)c-=2*Math.PI;for(;c<-Math.PI;)c+=2*Math.PI;if(Math.abs(c)<=B){if(!T){T=!0;const f={x:i.x+s*Math.cos(y-B),y:i.y+s*Math.sin(y-B)},h={x:i.x+V*Math.cos(y),y:i.y+V*Math.sin(y)},g={x:i.x+s*Math.cos(y+B),y:i.y+s*Math.sin(y+B)};v.push(o(f),o(h),o(g))}continue}v.push(o({x:i.x+s*Math.cos(S),y:i.y+s*Math.sin(S)}))}return[{kind:"wafer-boundary",...W(v,!0),lineColor:"#888888",lineWidth:1}]}function ye(t,e,a,i=360){const s=[],n=Math.max(1,Math.floor(a));for(let o=1;o<n;o++){const r=t.radius*o/n,u=[];for(let b=0;b<=i;b++){const y=2*Math.PI*b/i,B={x:t.center.x+r*Math.cos(y),y:t.center.y+r*Math.sin(y)};u.push(Y(e,B.x,B.y))}s.push({kind:"ring-boundary",...W(u,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function ge(t,e,a,i){const{x:s,y:n}=t.center,o=t.radius,r=(S,c)=>Y(e,S,c),u=a-s,b=Math.sqrt(Math.max(0,o*o-u*u)),y=r(a,n-b),B=r(a,n+b),V=i-n,v=Math.sqrt(Math.max(0,o*o-V*V)),T=r(s-v,i),m=r(s+v,i);return[{kind:"quadrant-boundary",...W([y,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...W([T,m]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function be(t,e){return t.map(a=>{const i=a.width/2,s=a.height/2;return{kind:"reticle",points:[[[a.x-i,a.y-s],[a.x+i,a.y-s],[a.x+i,a.y+s],[a.x-i,a.y+s]].map(([o,r])=>Y(e,o,r))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function xe(t){const e=t.filter(a=>a.probeIndex!==void 0).sort((a,i)=>(a.probeIndex??0)-(i.probeIndex??0));return e.length?[{kind:"probe-path",...W(e.map(a=>({x:a.physX,y:a.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function Me(t,e,a,i,s,n,o,r,u,b,y,B,V,v,T="off",m,S,c){const f=e.width-o,h=e.height-o,g=n?h:f,M=n?f:h,p=$=>s==="softBin"?$.sbin:$.hbin,H=$=>st($,m);if(e.partial){t.push({x:a,y:i,width:g,height:M,fill:le,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:a,y:i,width:g,height:M,fill:ce,type:"stacked",metadata:e.metadata});return}if(u!==void 0&&(s==="hardBin"||s==="softBin")&&p(e)!==u){t.push({x:a,y:i,width:g,height:M,fill:Ot,type:"hardBin",metadata:e.metadata});return}if(c!==void 0&&s==="metadata"&&H(e)!==c){t.push({x:a,y:i,width:g,height:M,fill:Ot,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(T==="test"){const J=Q(e,y,v),ot=J===void 0?j:J?pt:mt;t.push({x:a,y:i,width:g,height:M,fill:ot,type:"value",metadata:e.metadata});return}const $=R(e,y),D=classifySpec($,v);let _,U;$===void 0?_=j:T==="spec"?_=D==="failLow"?ie:D==="failHigh"?mt:pt:(_=r.forValue(b($)),(D==="failLow"||D==="failHigh")&&(U=D)),t.push({x:a,y:i,width:g,height:M,fill:_,type:"value",specMark:U,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const $=p(e),D=$!=null?r.forBin($):j,_=$!=null&&!V.has($)?!0:void 0;t.push({x:a,y:i,width:g,height:M,fill:D,type:s,binFail:_,metadata:e.metadata});return}if(s==="metadata"){const $=H(e),D=$!==void 0?S?.get($)??j:j;t.push({x:a,y:i,width:g,height:M,fill:D,type:"metadata",metadata:e.metadata});return}const A=R(e,0),E=A!==void 0?r.forValue(b(A)):j;t.push({x:a,y:i,width:g,height:M,fill:E,type:"value",metadata:e.metadata})}function Se(t,e,a){const i=t.radius*.15,s=ht(e,i,0),n=ht(e,0,i),o=s.x+n.x>=0?-1:1,r=s.y+n.y>=0?-1:1,u={x:t.center.x+o*t.radius*.9,y:t.center.y+r*t.radius*.9},b={x:u.x+s.x,y:u.y+s.y},y={x:u.x+n.x,y:u.y+n.y};return a.push({x:b.x+s.x*.35,y:b.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:y.x+n.x*.35,y:y.y+n.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...W([u,b]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...W([u,y]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,a={},i){const s=a.reticles??[],{plotMode:n="value",showDieLabels:o=!1,showReticle:r=!1,showProbePath:u=!1,ringCount:b=4,showRingBoundaries:y=!1,showQuadrantBoundaries:B=!1,showXYIndicator:V=!1,dieGap:v=1,colorScheme:T="default",passBins:m=[1],highlightBin:S,highlightMetadataValue:c,interactiveTransform:f,valueRange:h,testDefs:g,activeTest:M=0,activeMetadataKey:p,fallbackFormat:H="engineering",aggregationMethod:A,lotSize:E,dataAxisFlip:$,isLotStack:D=!1,logScale:_,colorbarRangeMode:U="spec",passFailDisplay:J,showPartialDies:ot=!0}=a,xt=J??"off",Mt=i?.hbinDefs,St=i?.sbinDefs,$t=i?.metadataFields,_t=$t?.find(l=>l.key===p),wt={x:($?.x??!1)!==(f?.flipX??!1),y:($?.y??!1)!==(f?.flipY??!1)},Wt=Mt?new Map(Mt.map(l=>[l.bin,l])):null,jt=St?new Map(St.map(l=>[l.bin,l])):null,rt=n==="softBin"?jt:Wt,lt=se(T),zt=new Set(m),kt={forValue:de(lt.forValue),forBin:T==="custom"&&rt?l=>rt.get(l)?.color??lt.forBin(l):lt.forBin};let{testNumber:O}=resolveTestNumber(M,g);if(!g?.length&&n==="value"&&!e.some(d=>d.testValues&&O in d.testValues)){const d=e.reduce((x,F)=>{if(!F.testValues)return x;const k=Object.keys(F.testValues).map(Number),q=k.length?Math.min(...k):void 0;return q!==void 0&&(x===void 0||q<x)?q:x},void 0);d!==void 0&&(O=d)}let Z;if(Array.isArray(h))Z=h;else if(h){const{testNumber:l}=resolveTestNumber(h.test,g);Z=l===O?h.range:void 0}const I=findTestDef(g,O),Bt=n==="value"&&I!==void 0&&!yt(I),Kt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let C="off";n==="value"&&(Bt?C="test":xt==="spec"&&Kt?C="spec":xt==="test"&&e.some(l=>Q(l,O,I)!==void 0)&&(C="test"));const vt=C==="spec"?"spec":U;let P,N,Tt=!1;if(Z)[P,N]=Z;else{const l=n==="stackedValues"||n==="stackedBins"||n==="stackedSoftBins",d=!l&&n==="value"&&vt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let x=1/0,F=-1/0,k=!0;for(const q of e){const X=l?R(q,0):R(q,O);X!==void 0&&(X<x&&(x=X),X>F&&(F=X),Number.isInteger(X)||(k=!1))}isFinite(x)||(k=!1),Tt=k,d?(P=I.limitLow!==void 0?I.limitLow:isFinite(x)?x:0,N=I.limitHigh!==void 0?I.limitHigh:isFinite(F)?F:1):(P=isFinite(x)?x:0,N=isFinite(F)?F:1)}P===N&&(N=P+1);const It=Bt?!1:_??I?.logScale??!1,Ft=It&&P>0&&N>0,qt=Ft;let tt;if(Ft){const l=Math.log10(P),d=Math.log10(N)-l;tt=x=>x<=0?0:Nt((Math.log10(x)-l)/d)}else tt=l=>Nt((l-P)/(N-P));const L=ue(t,$,f);let Lt=null;if(t.notch){const l=Et(t.notch.type),d=ht(L.physicalToScreen,Math.cos(l)*t.radius,Math.sin(l)*t.radius),x=Math.hypot(d.x,d.y);x>0&&(Lt={x:d.x/x,y:d.y/x})}let z=1/0;for(let l=0;l<e.length;l++){const d=e[l];d.width<z&&(z=d.width),d.height<z&&(z=d.height)}const Xt=z!==1/0?Math.min(v,z*.12):v,Vt=[],ct=[],Yt=L.bakedToScreen.a===1&&L.bakedToScreen.b===0&&L.bakedToScreen.c===0&&L.bakedToScreen.d===1&&L.bakedToScreen.e===0&&L.bakedToScreen.f===0;let w=null;if(!Yt){w=new Float64Array(e.length*2);for(let l=0;l<e.length;l++){const d=Y(L.bakedToScreen,e[l].physX,e[l].physY);w[l*2]=d.x,w[l*2+1]=d.y}}const Gt=ae(L.gridToScreen),Qt=n==="hardBin"||n==="softBin"||n==="stackedBins"||n==="stackedSoftBins",Ut=n==="softBin"||n==="stackedSoftBins";let et=Qt?new Map:void 0;const ut=n==="value"&&C==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ft=n==="value"&&C==="test"?{pass:0,fail:0}:void 0,Dt=n==="metadata";let nt=Dt?new Map:void 0,at=null;if(Dt&&p){const l=new Set;for(const x of e){if(x.partial||x.edgeExcluded)continue;const F=st(x,p);F!==void 0&&l.add(F)}const d=[...l].sort(re);at=new Map(d.map((x,F)=>[x,_t?.values?.find(k=>k.value===x)?.color??oe(F)]))}for(let l=0;l<e.length;l++){const d=e[l],x=w?w[l*2]:d.physX,F=w?w[l*2+1]:d.physY;if(ct.push({x,y:F}),et&&!d.partial&&!d.edgeExcluded){const k=Ut?d.sbin:d.hbin;k!=null&&et.set(k,(et.get(k)??0)+1)}if(ut&&!d.partial&&!d.edgeExcluded){const k=classifySpec(R(d,O),I);k&&ut[k]++}if(ft&&!d.partial&&!d.edgeExcluded){const k=Q(d,O,I);k!==void 0&&ft[k?"pass":"fail"]++}if(nt&&!d.partial&&!d.edgeExcluded){const k=st(d,p);k!==void 0&&nt.set(k,(nt.get(k)??0)+1)}d.partial&&!ot||Me(Vt,d,x,F,n,Gt,Xt,kt,S,tt,O,rt,zt,I,C,p,at,c)}const Pt=o?generateTextOverlay(e,w,{plotMode:n,colorFns:kt,normalize:tt,activeTest:M,valueRange:[P,N],testDefs:g,fallbackFormat:H,passFailDisplay:C,activeMetadataKey:p,metadataColorMap:at}):[],K=me(t,L.physicalToScreen);y&&K.push(...ye(t,L.physicalToScreen,b)),B&&K.push(...ge(t,L.physicalToScreen,t.center.x,t.center.y)),r&&K.push(...be(s,L.gridToScreen)),u&&K.push(...xe(e)),V&&K.push(...Se(t,L.gridToScreen,Pt));const Jt=ct.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Vt,hoverPoints:ct,texts:Pt,overlays:K,plotMode:n,colorScheme:T,metadata:t.metadata??null,dies:e,valueRange:[P,N],allIntegerValues:Tt,testDefs:g,activeTest:M,logScale:qt,logScaleRequested:It,aggrMethod:A,lotSize:E,axisFlip:wt,dataAxisFlip:{x:$?.x??!1,y:$?.y??!1},gridToScreen:L.gridToScreen,rotation:((t.orientation+(f?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:D,passFailDisplay:C,colorBySpec:C==="spec",colorbarRangeMode:vt,waferCenter:t.center,waferRadius:t.radius,notchDir:Lt,binCounts:et,metadataCounts:nt,metadataColorMap:at,activeMetadataKey:p,metadataFields:$t,specCounts:ut,passFailCounts:ft,dieBounds:Jt}}export{getDieKey}from"../core/dies.js";
|
|
1
|
+
import{hasPosition as ie}from"../core/dies.js";import{getReticleCell as se}from"../core/reticle.js";import{metadataCategoricalValue as oe,metadataDisplayValue as re}from"../core/metadata.js";import{affineRotation as mt,affineMirror as Lt,affineCompose as it,affinePoint as Y,affineVector as pt,affineSwapsAxes as le}from"../core/transforms.js";import{contrastTextColor as G,SPEC_PASS_FILL as yt,SPEC_FAIL_LOW as ce,SPEC_FAIL_HIGH as gt}from"./colorMap.js";import{getValueColorScheme as ue}from"./colorSchemes.js";import{resolveBinColors as fe,binColorsCover as de}from"./binColors.js";import{NO_DATA_FILL as j}from"./colorMap.js";import{diePassStatus as he}from"../core/dies.js";import{getDieTestValue as L,getTestPassStatus as Q,isParametricTest as bt}from"./buildWaferMap.js";import{fmt as xt,fmtColorbarAxis as Mt,fmtAggregationMethod as Pt}from"./fmt.js";import{metadataValueColor as me}from"./colorMap.js";import{compareNatural as pe,clamp01 as Rt}from"../core/utils.js";import{prettyKey as Nt}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function collectMetadataValues(t,e){const a=new Set;for(const n of t){if(n.partial||n.edgeExcluded)continue;const s=st(n,e);s!==void 0&&a.add(s)}return[...a].sort(pe)}export function findTestDef(t,e){return t?.find(a=>a.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,a)=>e-a)}const ye="#d3d6db",Ot="#e8e9ea",ge="#aab0ba";function st(t,e){return e?oe(t.metadata?.[e]):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function be(t,e,a){const{x:n,y:s}=t.center,i=mt(t.orientation,n,s),o=it(Lt(e?.x??!1,e?.y??!1,n,s),mt(t.orientation,n,s)),l=it(Lt(a?.flipX??!1,a?.flipY??!1,n,s),mt(a?.rotation??0,n,s));return{bakedToScreen:l,physicalToScreen:it(l,i),gridToScreen:it(l,o)}}function z(t,e=!1){return{points:[t],closed:e}}function xe(t,e){const{center:a,radius:n,notch:s}=t;let i=a.x+n*Math.cos(e),o=a.y+n*Math.sin(e);if(!s)return{x:i,y:o};const l=Math.sqrt(n**2-(s.length/2)**2),u=s.length/2,g=i-a.x,p=o-a.y;return s.type==="bottom"&&p<-l?(o=a.y-l,i=a.x+Math.max(-u,Math.min(u,g))):s.type==="top"&&p>l?(o=a.y+l,i=a.x+Math.max(-u,Math.min(u,g))):s.type==="left"&&g<-l?(i=a.x-l,o=a.y+Math.max(-u,Math.min(u,p))):s.type==="right"&&g>l&&(i=a.x+l,o=a.y+Math.max(-u,Math.min(u,p))),{x:i,y:o}}function Ht(t,e){return t.map(e).join(" / ")}function Me(t,e=256){const a=Array.from({length:e},(n,s)=>t(s/(e-1)));return n=>a[Math.min(e-1,Math.max(0,Math.round(n*(e-1))))]}function Se(t,e){const a=Math.max(1,Math.min(t.width,t.height)),n=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(a*.55,n*1.8))))}function ke(t,e,a){if(e?.length){const n=e.flatMap(s=>{const i=s.testNumber;if(!bt(s)){const u=Q(t,i,s);return u===void 0?[]:[{key:i,label:s.name,value:u?"Pass":"Fail"}]}const o=L(t,i);if(o===void 0)return[];const l=t.testPass?.[i]===!1?" <i>(recorded fail)</i>":"";return[{key:i,label:s.name,value:`${xt(o,s.unit,a)}${l}`}]});if(n.length)return n}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:xt(s,void 0,a)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,a={}){const{testDefs:n,hbinDefs:s,sbinDefs:i,fallbackFormat:o,aggrMethod:l,lotSize:u,activeTest:g,reticleConfig:p,metadataFields:B}=a,D=s?new Map(s.map(f=>[f.bin,f])):null,T=i?new Map(i.map(f=>[f.bin,f])):null,b=[`Die (${t.x}, ${t.y})`];if(p&&ie(t)){const f=se(t,p);b.push(`Reticle (${f.column}, ${f.row})`)}if(e==="stackedValues"){const f=L(t,0);if(f!==void 0){const d=n?.[0],h=d?.testNumber,k=d?.name??(h!=null?`Test ${h}`:"Value"),m=l?` (${l})`:"";b.push(`${k}${m}: ${xt(f,d?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const f=L(t,0),d=e==="stackedSoftBins"?t.sbin:t.hbin;if(f!==void 0){const h=e==="stackedSoftBins"?T:D,k=d!==void 0?h?.get(d)?.name?`${d} \xB7 ${h.get(d).name}`:`Bin ${d}`:"Bin";let m;if(l==="percent")m=`${f.toFixed(0)}%`;else{const C=u?` (${(f/u*100).toFixed(0)}% of stacked wafers)`:"";m=`${f}${C}`}const x=l?` [${Pt(l)}]`:"";b.push(`${k}: ${m}${x}`)}}else{const f=ke(t,n,o);if(e==="value"&&f.length){const{testNumber:d}=resolveTestNumber(g??0,n),h=findTestDef(n,d),k=L(t,d);let m=f.findIndex(y=>y.key===d);m<0&&(m=0);const x=f[m];let C=`<b>${x.label}: ${x.value}</b>`;if(x.key===d&&h){const y=classifySpec(k,h);(y==="failLow"||y==="failHigh")&&(C+=" <i>(out of spec)</i>")}b.push(C);const R=f.length-1;R>0&&b.push(`<i>+${R} more test${R===1?"":"s"}</i>`)}else if(f.length){const d=f.length;b.push(`<i>${d} test result${d===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const d=[];if(t.hbin!==void 0){const h=D?.get(t.hbin)?.name,k=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);d.push(`HBin: ${k}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,k=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);d.push(`SBin: ${k}`)}b.push(d.join(" \xB7 "))}}t.retestCount!==void 0&&b.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&b.push(`Site: ${t.siteNum}`),t.partId!==void 0&&b.push(`Part ID: ${t.partId}`),t.partial&&b.push("<i>partial die</i>"),t.probeIndex!==void 0&&b.push(`Probe: #${t.probeIndex}`);const $={...t.metadata??{}};for(const[f,d]of Object.entries($)){const h=re(d);if(h===void 0)continue;const k=B?.find(m=>m.key===f)?.label??Nt(f);b.push(`${k}: ${h}`)}return b.join("<br>")}export function buildMapTitle(t,e="engineering",a){const n=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=i=>{const o=a?.length===1?a[0]:void 0;return o===void 0?`${i} Bin`:o.name?`${i} Bin ${o.bin} \xB7 ${o.name}`:`${i} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:n};case"stackedSoftBins":return{primary:s("Soft"),secondary:n};case"metadata":{const i=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?Nt(t.activeMetadataKey):"Metadata";return{primary:i?.label??o,secondary:""}}case"stackedValues":{const i=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=Mt(o,i?.name,i?.unit,e),u=l||"Value",g=t.aggrMethod?` \xB7 ${Pt(t.aggrMethod)}`:"";return{primary:`${u}${g}`,secondary:n}}default:{const i=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=Mt(o,i?.name,i?.unit,e),u=l||i?.name;if(t.passFailDisplay!=="off"){const g=i?.testNumber??t.activeTest,p=u?`${u} \xB7 #${g}`:`Test ${g}`,B=t.passFailDisplay==="spec"?"Spec pass/fail":i!==void 0&&!bt(i)?"Functional pass/fail":"Tester pass/fail";return{primary:p,secondary:B}}return{primary:u??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,a){const{plotMode:n,colorFns:s,normalize:i,activeTest:o,valueRange:l,testDefs:u,fallbackFormat:g,passFailDisplay:p="off",activeMetadataKey:B,metadataColorMap:D,allIntegerValues:T}=a,b=findTestDef(u,o),{tickFmt:$}=Mt(l[1],b?.name,b?.unit,g),f=T?h=>String(Math.round(h)):$,{testNumber:d}=resolveTestNumber(o,u);return t.flatMap((h,k)=>{let m="",x="#111111";if(n==="value"&&p==="test"){const y=Q(h,d,b);if(y===void 0)return[];m=y?"P":"F",x=G(y?yt:gt)}else if(n==="value"){const y=L(h,d);if(y===void 0)return[];m=Ht([y],f),x=G(s.forValue(i(y)))}else if(n==="hardBin"||n==="softBin"){const y=n==="softBin"?h.sbin:h.hbin;if(y===void 0)return[];m=String(y),x=G(s.forBin(y))}else if(n==="metadata"){const y=st(h,B);if(y===void 0)return[];m=y,x=G(D?.get(m)??j)}else{const y=L(h,0);if(y===void 0)return[];m=Ht([y],f),x=G(s.forValue(i(y)))}const C=e?e[k*2]:h.physX,R=e?e[k*2+1]:h.physY;return[{x:C,y:R,text:m,fontSize:Se(h,m),color:x,align:"center"}]})}function At(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function ve(t,e,a=720){const{center:n,radius:s,notch:i}=t,o=$=>Y(e,$.x,$.y);if(!(i!==void 0&&t.diameter>150)){const $=[];for(let f=0;f<=a;f++){const d=2*Math.PI*f/a;$.push(o(xe(t,d)))}return[{kind:"wafer-boundary",...z($,!0),lineColor:"#888888",lineWidth:1}]}const u=1.25,g=i.length,p=At(i.type),B=Math.atan2(g,s),D=s-u,T=[];let b=!1;for(let $=0;$<=a;$++){const f=2*Math.PI*$/a;let d=f-p;for(;d>Math.PI;)d-=2*Math.PI;for(;d<-Math.PI;)d+=2*Math.PI;if(Math.abs(d)<=B){if(!b){b=!0;const h={x:n.x+s*Math.cos(p-B),y:n.y+s*Math.sin(p-B)},k={x:n.x+D*Math.cos(p),y:n.y+D*Math.sin(p)},m={x:n.x+s*Math.cos(p+B),y:n.y+s*Math.sin(p+B)};T.push(o(h),o(k),o(m))}continue}T.push(o({x:n.x+s*Math.cos(f),y:n.y+s*Math.sin(f)}))}return[{kind:"wafer-boundary",...z(T,!0),lineColor:"#888888",lineWidth:1}]}function $e(t,e,a,n=360){const s=[],i=Math.max(1,Math.floor(a));for(let o=1;o<i;o++){const l=t.radius*o/i,u=[];for(let g=0;g<=n;g++){const p=2*Math.PI*g/n,B={x:t.center.x+l*Math.cos(p),y:t.center.y+l*Math.sin(p)};u.push(Y(e,B.x,B.y))}s.push({kind:"ring-boundary",...z(u,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function Be(t,e,a,n){const{x:s,y:i}=t.center,o=t.radius,l=(f,d)=>Y(e,f,d),u=a-s,g=Math.sqrt(Math.max(0,o*o-u*u)),p=l(a,i-g),B=l(a,i+g),D=n-i,T=Math.sqrt(Math.max(0,o*o-D*D)),b=l(s-T,n),$=l(s+T,n);return[{kind:"quadrant-boundary",...z([p,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...z([b,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function Te(t,e){return t.map(a=>{const n=a.width/2,s=a.height/2;return{kind:"reticle",points:[[[a.x-n,a.y-s],[a.x+n,a.y-s],[a.x+n,a.y+s],[a.x-n,a.y+s]].map(([o,l])=>Y(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function Ie(t){const e=t.filter(a=>a.probeIndex!==void 0).sort((a,n)=>(a.probeIndex??0)-(n.probeIndex??0));return e.length?[{kind:"probe-path",...z(e.map(a=>({x:a.physX,y:a.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function Ve(t,e,a,n,s,i,o,l,u,g,p,B,D,T,b="off",$,f,d){const h=e.width-o,k=e.height-o,m=i?k:h,x=i?h:k,C=S=>s==="softBin"?S.sbin:S.hbin,R=S=>st(S,$);if(e.partial){t.push({x:a,y:n,width:m,height:x,fill:ye,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:a,y:n,width:m,height:x,fill:ge,type:"stacked",metadata:e.metadata});return}if(u!==void 0&&(s==="hardBin"||s==="softBin")&&C(e)!==u){t.push({x:a,y:n,width:m,height:x,fill:Ot,type:"hardBin",metadata:e.metadata});return}if(d!==void 0&&s==="metadata"&&R(e)!==d){t.push({x:a,y:n,width:m,height:x,fill:Ot,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(b==="test"){const w=Q(e,p,T),ot=w===void 0?j:w?yt:gt;t.push({x:a,y:n,width:m,height:x,fill:ot,type:"value",metadata:e.metadata});return}const S=L(e,p),N=classifySpec(S,T);let _,U;S===void 0?_=j:b==="spec"?_=N==="failLow"?ce:N==="failHigh"?gt:yt:(_=l.forValue(g(S)),(N==="failLow"||N==="failHigh")&&(U=N)),t.push({x:a,y:n,width:m,height:x,fill:_,type:"value",specMark:U,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const S=C(e),N=S!=null?l.forBin(S):j,_=S!=null&&he(e,D)===!1?!0:void 0;t.push({x:a,y:n,width:m,height:x,fill:N,type:s,binFail:_,metadata:e.metadata});return}if(s==="metadata"){const S=R(e),N=S!==void 0?f?.get(S)??j:j;t.push({x:a,y:n,width:m,height:x,fill:N,type:"metadata",metadata:e.metadata});return}const y=L(e,0),X=y!==void 0?l.forValue(g(y)):j;t.push({x:a,y:n,width:m,height:x,fill:X,type:"value",metadata:e.metadata})}function Ce(t,e,a){const n=t.radius*.15,s=pt(e,n,0),i=pt(e,0,n),o=s.x+i.x>=0?-1:1,l=s.y+i.y>=0?-1:1,u={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},g={x:u.x+s.x,y:u.y+s.y},p={x:u.x+i.x,y:u.y+i.y};return a.push({x:g.x+s.x*.35,y:g.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:p.x+i.x*.35,y:p.y+i.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...z([u,g]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...z([u,p]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,a={},n){const s=a.reticles??[],{plotMode:i="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:u=!1,ringCount:g=4,showRingBoundaries:p=!1,showQuadrantBoundaries:B=!1,showXYIndicator:D=!1,dieGap:T=1,binColorScheme:b="default",valueColorScheme:$="default",useDefinedBinColors:f=!0,binColors:d,passBins:h=[1],highlightBin:k,highlightMetadataValue:m,interactiveTransform:x,valueRange:C,metadataValueOrder:R,testDefs:y,activeTest:X=0,activeMetadataKey:S,fallbackFormat:N="engineering",aggregationMethod:_,lotSize:U,dataAxisFlip:w,isLotStack:ot=!1,logScale:Et,colorbarRangeMode:_t="spec",passFailDisplay:wt,showPartialDies:Wt=!0}=a,St=wt??"off",rt=n?.hbinDefs,lt=n?.sbinDefs,kt=n?.metadataFields,jt=kt?.find(c=>c.key===S),zt={x:(w?.x??!1)!==(x?.flipX??!1),y:(w?.y??!1)!==(x?.flipY??!1)},Kt=rt?new Map(rt.map(c=>[c.bin,c])):null,qt=lt?new Map(lt.map(c=>[c.bin,c])):null,Xt=i==="softBin"?qt:Kt,Yt=new Set(h),ct=d&&de(d,e)?d:fe(e,{passBins:h,binColorScheme:b,hbinDefs:rt,sbinDefs:lt,useDefinedBinColors:f}),Gt=i==="softBin"?ct.soft:ct.hard,vt={forValue:Me(ue($).forValue),forBin:c=>Gt.get(c)??j};let{testNumber:H}=resolveTestNumber(X,y);if(!y?.length&&i==="value"&&!e.some(r=>r.testValues&&H in r.testValues)){const r=e.reduce((M,V)=>{if(!V.testValues)return M;const v=Object.keys(V.testValues).map(Number),P=v.length?Math.min(...v):void 0;return P!==void 0&&(M===void 0||P<M)?P:M},void 0);r!==void 0&&(H=r)}let J;if(Array.isArray(C))J=C;else if(C){const{testNumber:c}=resolveTestNumber(C.test,y);J=c===H?C.range:void 0}const I=findTestDef(y,H),$t=i==="value"&&I!==void 0&&!bt(I),Qt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let A="off";i==="value"&&($t?A="test":St==="spec"&&Qt?A="spec":St==="test"&&e.some(c=>Q(c,H,I)!==void 0)&&(A="test"));const Bt=A==="spec"?"spec":_t,ut=i==="stackedValues"||i==="stackedBins"||i==="stackedSoftBins";let O,E,Z=!1;if(J){[O,E]=J;let c=!0,r=!1;for(const M of e){const V=ut?L(M,0):L(M,H);if(V!==void 0&&(r=!0,!Number.isInteger(V))){c=!1;break}}Z=r&&c}else{const c=!ut&&i==="value"&&Bt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let r=1/0,M=-1/0,V=!0;for(const v of e){const P=ut?L(v,0):L(v,H);P!==void 0&&(P<r&&(r=P),P>M&&(M=P),Number.isInteger(P)||(V=!1))}isFinite(r)||(V=!1),Z=V,c?(O=I.limitLow!==void 0?I.limitLow:isFinite(r)?r:0,E=I.limitHigh!==void 0?I.limitHigh:isFinite(M)?M:1):(O=isFinite(r)?r:0,E=isFinite(M)?M:1)}O===E&&(E=O+1);const Tt=$t?!1:Et??I?.logScale??!1,It=Tt&&O>0&&E>0,Ut=It;let tt;if(It){const c=Math.log10(O),r=Math.log10(E)-c;tt=M=>M<=0?0:Rt((Math.log10(M)-c)/r)}else tt=c=>Rt((c-O)/(E-O));const F=be(t,w,x);let Vt=null;if(t.notch){const c=At(t.notch.type),r=pt(F.physicalToScreen,Math.cos(c)*t.radius,Math.sin(c)*t.radius),M=Math.hypot(r.x,r.y);M>0&&(Vt={x:r.x/M,y:r.y/M})}let K=1/0;for(let c=0;c<e.length;c++){const r=e[c];r.width<K&&(K=r.width),r.height<K&&(K=r.height)}const Jt=K!==1/0?Math.min(T,K*.12):T,Ct=[],ft=[],Zt=F.bakedToScreen.a===1&&F.bakedToScreen.b===0&&F.bakedToScreen.c===0&&F.bakedToScreen.d===1&&F.bakedToScreen.e===0&&F.bakedToScreen.f===0;let W=null;if(!Zt){W=new Float64Array(e.length*2);for(let c=0;c<e.length;c++){const r=Y(F.bakedToScreen,e[c].physX,e[c].physY);W[c*2]=r.x,W[c*2+1]=r.y}}const te=le(F.gridToScreen),ee=i==="hardBin"||i==="softBin"||i==="stackedBins"||i==="stackedSoftBins",ne=i==="softBin"||i==="stackedSoftBins";let et=ee?new Map:void 0;const dt=i==="value"&&A==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ht=i==="value"&&A==="test"?{pass:0,fail:0}:void 0,Ft=i==="metadata";let nt=Ft?new Map:void 0,at=null;if(Ft&&S){const c=collectMetadataValues(e,S),r=R?.key===S?R.values:void 0,M=r?[...r,...c.filter(V=>!r.includes(V))]:c;at=new Map(M.map((V,v)=>[V,jt?.values?.find(P=>P.value===V)?.color??me(v)]))}for(let c=0;c<e.length;c++){const r=e[c],M=W?W[c*2]:r.physX,V=W?W[c*2+1]:r.physY;if(ft.push({x:M,y:V}),et&&!r.partial&&!r.edgeExcluded){const v=ne?r.sbin:r.hbin;v!=null&&et.set(v,(et.get(v)??0)+1)}if(dt&&!r.partial&&!r.edgeExcluded){const v=classifySpec(L(r,H),I);v&&dt[v]++}if(ht&&!r.partial&&!r.edgeExcluded){const v=Q(r,H,I);v!==void 0&&ht[v?"pass":"fail"]++}if(nt&&!r.partial&&!r.edgeExcluded){const v=st(r,S);v!==void 0&&nt.set(v,(nt.get(v)??0)+1)}r.partial&&!Wt||Ve(Ct,r,M,V,i,te,Jt,vt,k,tt,H,Xt,Yt,I,A,S,at,m)}const Dt=o?generateTextOverlay(e,W,{plotMode:i,colorFns:vt,normalize:tt,activeTest:X,valueRange:[O,E],testDefs:y,fallbackFormat:N,passFailDisplay:A,activeMetadataKey:S,metadataColorMap:at,allIntegerValues:Z}):[],q=ve(t,F.physicalToScreen);p&&q.push(...$e(t,F.physicalToScreen,g)),B&&q.push(...Be(t,F.physicalToScreen,t.center.x,t.center.y)),l&&q.push(...Te(s,F.gridToScreen)),u&&q.push(...Ie(e)),D&&q.push(...Ce(t,F.gridToScreen,Dt));const ae=ft.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Ct,hoverPoints:ft,texts:Dt,overlays:q,plotMode:i,binColorScheme:b,valueColorScheme:$,binColors:ct,metadata:t.metadata??null,dies:e,valueRange:[O,E],allIntegerValues:Z,testDefs:y,activeTest:X,logScale:Ut,logScaleRequested:Tt,aggrMethod:_,lotSize:U,axisFlip:zt,dataAxisFlip:{x:w?.x??!1,y:w?.y??!1},gridToScreen:F.gridToScreen,rotation:((t.orientation+(x?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:ot,passFailDisplay:A,colorBySpec:A==="spec",colorbarRangeMode:Bt,waferCenter:t.center,waferRadius:t.radius,notchDir:Vt,binCounts:et,metadataCounts:nt,metadataColorMap:at,activeMetadataKey:S,metadataFields:kt,specCounts:dt,passFailCounts:ht,dieBounds:ae}}export{getDieKey}from"../core/dies.js";
|
|
@@ -252,8 +252,10 @@ export interface BinDef {
|
|
|
252
252
|
/** Human-readable bin name, e.g. `"Pass"` or `"Contact Open"`. */
|
|
253
253
|
name: string;
|
|
254
254
|
/**
|
|
255
|
-
* Optional CSS
|
|
256
|
-
*
|
|
255
|
+
* Optional CSS colour for this bin — e.g. a site's standard bin colour sheet.
|
|
256
|
+
* Wins over the bin colour scheme for this bin (unless the viewer turns
|
|
257
|
+
* "Use colours from bin definitions" off); every bin without one still takes
|
|
258
|
+
* a palette colour. See `resolveBinColors`.
|
|
257
259
|
*/
|
|
258
260
|
color?: string;
|
|
259
261
|
}
|
|
@@ -299,6 +301,21 @@ export interface WaferMapInputBase {
|
|
|
299
301
|
* Set to an empty array to suppress yield calculation.
|
|
300
302
|
*/
|
|
301
303
|
passBins?: number[];
|
|
304
|
+
/**
|
|
305
|
+
* Wafer diameters (mm) treated as standard when sanity-checking an *inferred*
|
|
306
|
+
* diameter — see {@link STANDARD_WAFER_DIAMETERS_MM} for the default
|
|
307
|
+
* (100/125/150/200/300) and the measurements behind it.
|
|
308
|
+
*
|
|
309
|
+
* **Replaces** the default rather than adding to it, so spread it to extend:
|
|
310
|
+
* `standardDiameters: [...STANDARD_WAFER_DIAMETERS_MM, 76.2]` for a line that
|
|
311
|
+
* also runs 3-inch. Pass `[]` to disable the check entirely, for genuinely
|
|
312
|
+
* non-standard substrates (panels, reclaim, odd R&D shapes) — that is the
|
|
313
|
+
* intended opt-out, rather than suppressing every geometry advisory.
|
|
314
|
+
*
|
|
315
|
+
* Only consulted when the diameter was inferred; a diameter you supply is
|
|
316
|
+
* never second-guessed.
|
|
317
|
+
*/
|
|
318
|
+
standardDiameters?: number[];
|
|
302
319
|
/**
|
|
303
320
|
* How to handle multiple `DieResult` entries for the same die position (retests).
|
|
304
321
|
*
|
|
@@ -420,22 +437,47 @@ export interface WaferWarning {
|
|
|
420
437
|
* you asserted — it reports this so you can correct one of them. Only raised
|
|
421
438
|
* when the pitch was supplied: pitch is a free scaling parameter, so with an
|
|
422
439
|
* inferred pitch "the dies don't fit" is not a statement about your data.
|
|
423
|
-
* - `'
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
440
|
+
* - `'non-standard-diameter'` — the wafer diameter was INFERRED from the die
|
|
441
|
+
* extent (a pitch was supplied, a diameter was not) and came out off the
|
|
442
|
+
* standard wafer-size ladder (SEMI M1: 100/150/200/300 mm and the smaller
|
|
443
|
+
* legacy sizes). Silicon only comes in those sizes, so a result like 210 mm
|
|
444
|
+
* is evidence the inference went wrong — almost always because the outer
|
|
445
|
+
* dies were never probed, so the die extent understates the wafer. Every die
|
|
446
|
+
* is then placed against a wafer that is too small, which moves dies between
|
|
447
|
+
* rings and changes ring and edge findings. Supply `waferConfig.diameter`.
|
|
448
|
+
*
|
|
449
|
+
* There is deliberately no matching advisory for an inferred *pitch*: that
|
|
450
|
+
* is derived to fit the supplied diameter, so it is always self-consistent
|
|
451
|
+
* and there is nothing to check it against.
|
|
452
|
+
* - `'diameter-exceeds-die-extent'` — a SUPPLIED `waferConfig.diameter` that the
|
|
453
|
+
* probed dies fill less than 75% of. The mirror of `geometry-conflict`, which
|
|
454
|
+
* asks whether the dies *fit*; this asks whether they *fill*. An over-large
|
|
455
|
+
* wafer is not harmless: ring bands are equal-radius, so it crushes dies into
|
|
456
|
+
* the inner rings and empties the outer ones — at a 10× diameter every die
|
|
457
|
+
* lands in ring 1 — and ring/quadrant/edge findings then describe the assumed
|
|
458
|
+
* wafer, not the probed area. A genuinely partial map looks identical and is
|
|
459
|
+
* equally worth reporting, so the message names both causes.
|
|
427
460
|
* - `'test-count-capped'` — raised by `analyzeWaferMap`: more tests were found in
|
|
428
461
|
* the die data than the analysis cap allows, so test-value analysis was skipped
|
|
429
462
|
* entirely and NO test findings were produced. Pass `testNumbers` to scope it.
|
|
463
|
+
* - `'analysis-option-corrected'` — raised by `analyzeWaferMap`: a numeric option
|
|
464
|
+
* was outside the range that can produce a meaningful analysis and was clamped.
|
|
465
|
+
* The message names each correction. Only reachable from untyped callers now
|
|
466
|
+
* that the statistical thresholds are internal, but a wrong `ringCount` or
|
|
467
|
+
* `sectorCount` still gets here from TypeScript.
|
|
430
468
|
* - `'edge-exclusion-exceeds-radius'` — `waferConfig.edgeExclusion` is larger than
|
|
431
469
|
* the resolved wafer radius (most likely when the diameter was itself inferred
|
|
432
470
|
* from sparse/partial data). The excluded band is clamped to the whole wafer
|
|
433
471
|
* rather than silently producing a smaller, wrong ring.
|
|
472
|
+
* - `'bin-colors-shared'` — raised by the renderers for the bin map on screen:
|
|
473
|
+
* some bins are drawn in the same colour as another bin (more bins than the
|
|
474
|
+
* bin colour scheme has distinct colours, or a `BinDef.color` repeats one).
|
|
475
|
+
* Every die is drawn correctly; colour alone just cannot separate those bins.
|
|
434
476
|
*
|
|
435
477
|
* The union is intentionally open to string so future advisory codes can be
|
|
436
478
|
* added without a breaking change; switch with a `default` branch.
|
|
437
479
|
*/
|
|
438
|
-
code: 'partial-coverage' | 'geometry-conflict' | '
|
|
480
|
+
code: 'partial-coverage' | 'geometry-conflict' | 'non-standard-diameter' | 'diameter-exceeds-die-extent' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | 'analysis-option-corrected' | (string & {});
|
|
439
481
|
/** Human-readable explanation, suitable for direct display. */
|
|
440
482
|
message: string;
|
|
441
483
|
/**
|
|
@@ -453,7 +495,14 @@ export interface WaferWarning {
|
|
|
453
495
|
confidence?: number;
|
|
454
496
|
}
|
|
455
497
|
export interface WaferMapResult {
|
|
498
|
+
/** Resolved wafer geometry — diameter, radius, centre, notch, orientation. */
|
|
456
499
|
wafer: Wafer;
|
|
500
|
+
/**
|
|
501
|
+
* Every die, carrying its original grid coordinates (`x`/`y`), physical
|
|
502
|
+
* position, bin/test data and eligibility flags. Dies with no reported
|
|
503
|
+
* position are included here — guard with `hasPosition` before using
|
|
504
|
+
* coordinates.
|
|
505
|
+
*/
|
|
457
506
|
dies: Die[];
|
|
458
507
|
/**
|
|
459
508
|
* The initial plot mode selected by `buildWaferMap` — `'value'` when test values are
|
|
@@ -468,6 +517,17 @@ export interface WaferMapResult {
|
|
|
468
517
|
* @internal Renderer-agnostic draw list consumed by `renderWaferMap` and `toCanvas`.
|
|
469
518
|
* Not part of the public API — access the named fields on `WaferMapResult` instead.
|
|
470
519
|
*/
|
|
520
|
+
/**
|
|
521
|
+
* @internal The initial draw list. Kept on the result because `analyzeWaferMap`
|
|
522
|
+
* discriminates its input on `'view' in input`, and because `renderWaferMap`
|
|
523
|
+
* reads `dataAxisFlip` from it — not because a host should.
|
|
524
|
+
*
|
|
525
|
+
* Read the promoted top-level fields instead (`plotMode`, `metadata`,
|
|
526
|
+
* `isLotStack`, `hbinDefs`, `sbinDefs`, `testDefs`): this one is rebuilt on
|
|
527
|
+
* every option change, so anything you cache from it goes stale immediately.
|
|
528
|
+
* Hosts driving the low-level pipeline should call `buildView` themselves,
|
|
529
|
+
* which is the supported way to hold a {@link View}.
|
|
530
|
+
*/
|
|
471
531
|
view: View;
|
|
472
532
|
/** Reticle configuration used to generate the overlay and reticle-local groupings. */
|
|
473
533
|
reticleConfig?: ReticleConfig;
|
|
@@ -479,15 +539,26 @@ export interface WaferMapResult {
|
|
|
479
539
|
* proportionally correct but not in physical mm.
|
|
480
540
|
*/
|
|
481
541
|
units: 'mm' | 'normalized';
|
|
542
|
+
/**
|
|
543
|
+
* How much of the geometry had to be guessed, and how confident each guess is.
|
|
544
|
+
* `confidence` runs 0–1, where 1 means the value was supplied rather than
|
|
545
|
+
* inferred. Worth surfacing when a map looks wrong: a low `wafer` or `diePitch`
|
|
546
|
+
* confidence usually means sparse or partial data, and is the first thing to
|
|
547
|
+
* check before suspecting the data itself. See `warnings` for the advisories
|
|
548
|
+
* these confidences drive.
|
|
549
|
+
*/
|
|
482
550
|
inference: {
|
|
551
|
+
/** Wafer diameter/centre. `method` names how it was derived, e.g. `'inferred-from-xy'`. */
|
|
483
552
|
wafer: {
|
|
484
553
|
confidence: number;
|
|
485
554
|
method: string;
|
|
486
555
|
};
|
|
556
|
+
/** Die pitch. `units` is `'mm'` when a real physical scale was established, `'normalized'` when not. */
|
|
487
557
|
diePitch: {
|
|
488
558
|
confidence: number;
|
|
489
559
|
units: 'mm' | 'normalized';
|
|
490
560
|
};
|
|
561
|
+
/** The die grid's origin and step. */
|
|
491
562
|
grid: {
|
|
492
563
|
confidence: number;
|
|
493
564
|
};
|
|
@@ -574,5 +645,39 @@ export declare function getTestPassStatus(die: Pick<Die, 'testValues' | 'testPas
|
|
|
574
645
|
* availability checks.
|
|
575
646
|
*/
|
|
576
647
|
export declare function dieHasTestData(die: Pick<Die, 'testValues' | 'testPass'>): boolean;
|
|
648
|
+
/**
|
|
649
|
+
* Nominal wafer diameters in mm, used to sanity-check an *inferred* diameter.
|
|
650
|
+
* Override per build with {@link WaferMapInputBase.standardDiameters}.
|
|
651
|
+
*
|
|
652
|
+
* This is what makes an inferred diameter checkable at all. When a die pitch is
|
|
653
|
+
* supplied but no diameter, the wafer is sized from the die extent — right only
|
|
654
|
+
* if the probed grid reaches the wafer edge, and silently wrong whenever the
|
|
655
|
+
* outer dies were never probed. Silicon comes in standard sizes, so landing off
|
|
656
|
+
* the ladder is evidence the assumption failed. (The library already leans on
|
|
657
|
+
* SEMI M1 for notch chord lengths — see `standardNotchLength` in core/wafer.ts.)
|
|
658
|
+
*
|
|
659
|
+
* **The size of this list is a real trade-off, measured, not guessed.** Every
|
|
660
|
+
* entry is one more value a wrong inference can hide behind; every omission
|
|
661
|
+
* wrongly accuses someone actually running that size. Across 63 fixtures
|
|
662
|
+
* (76.2–300 mm, square and rectangular die, 50–94% coverage):
|
|
663
|
+
*
|
|
664
|
+
* | table | caught | missed | false alarms |
|
|
665
|
+
* |---|---|---|---|
|
|
666
|
+
* | 100/150/200/300 | 31 | 8 | 3 |
|
|
667
|
+
* | **100/125/150/200/300 (this default)** | 31 | 8 | 3 |
|
|
668
|
+
* | + 76.2 | 28 | 11 | 0 |
|
|
669
|
+
* | + 25.4/50.8/450 | 26 | 13 | 0 |
|
|
670
|
+
*
|
|
671
|
+
* A miss is silent, which is what every one of these cases did before this check
|
|
672
|
+
* existed — so it costs nothing against the status quo. A false alarm is a new
|
|
673
|
+
* harm: it tells someone whose diameter is correct that it is suspect, with no
|
|
674
|
+
* way to clear it. Hosts running 3-inch or other legacy sizes should add them
|
|
675
|
+
* via `standardDiameters` rather than live with that.
|
|
676
|
+
*
|
|
677
|
+
* 25.4/50.8 (1"/2") and 450 mm are excluded: none are in current production
|
|
678
|
+
* flows, and including 50.8 demonstrably created a miss — a 76.2 mm wafer
|
|
679
|
+
* mis-inferring to 50 mm went undetected.
|
|
680
|
+
*/
|
|
681
|
+
export declare const STANDARD_WAFER_DIAMETERS_MM: readonly number[];
|
|
577
682
|
export declare function buildWaferMap(input: DieResult[] | WaferMapInput, options?: WaferMapOptions): WaferMapResult;
|
|
578
683
|
//# sourceMappingURL=buildWaferMap.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as fe,getDieKey as B,hasPosition as k}from"../core/dies.js";import{applyOrientation as H,transformDies as de}from"../core/transforms.js";import{affineRotation as ce,affineMirror as le,affineCompose as ue,affinePoint as he}from"../core/transforms.js";import{inferWaferFromXY as pe}from"../core/inference/wafer.js";import{resolveGridPitch as me}from"../core/inference/pitch.js";import{assignGridIndices as ye}from"../core/inference/grid.js";import{generateReticleGrid as ge}from"../core/reticle.js";import{buildView as U}from"./buildView.js";import{modeOf as xe}from"../core/utils.js";import{aggregateValues as q,aggregateBinCounts as we}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function be(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],testDefs:t.testDefs,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Me(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function De(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let u=1/0,c=-1/0,a=1/0,l=-1/0;for(const s of t)s.x<u&&(u=s.x),s.x>c&&(c=s.x),s.y<a&&(a=s.y),s.y>l&&(l=s.y);return{offsetX:Math.round((c+u)/2),offsetY:Math.round((l+a)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Oe(t,n,e,u,c,a){if(a)return{colMidX:(Math.round(a.x)-n)*u,colMidY:(Math.round(a.y)-e)*c,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let l=1/0,s=-1/0,h=1/0,p=-1/0;for(const i of t){const d=Math.round(i.x)-n,o=Math.round(i.y)-e;d<l&&(l=d),d>s&&(s=d),o<h&&(h=o),o>p&&(p=o)}return{colMidX:(l+s)/2*u,colMidY:(h+p)/2*c,anchored:!1}}function Se(t,n,e){let u=1/0,c=-1/0,a=1/0,l=-1/0,s=0,h=0;for(const d of t){const o=Math.round(d.x)-n,r=Math.round(d.y)-e;s+=o,h+=r,o<u&&(u=o),o>c&&(c=o),r<a&&(a=r),r>l&&(l=r)}const p=t.length,i=(d,o,r)=>{const m=(r-o)/2;return m<=0?0:Math.abs(d/p-(o+r)/2)/m};return i(s,u,c)>.11||i(h,a,l)>.11}function ke(t,n){let e=t?.xAxisDirection==="left",u=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(u=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:u}}function ve(t,n){const{method:e,targetBin:u}=t,c=t.results.map(a=>a.filter(k));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const a=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),l=new Set;for(const i of c)for(const d of i)if(d.testValues)for(const o of Object.keys(d.testValues)){const r=Number(o);a.has(r)||l.add(r)}if(l.size===0)return q(c,e);const s=[...l],h=s.map(i=>q(c,e,i)),p=new Map;for(let i=0;i<s.length;i++){const d=s[i];for(const o of h[i]){const r=B(o);p.has(r)||p.set(r,{template:o,testValues:{}});const m=p.get(r),y=o.testValues?.[0];y!==void 0&&(m.testValues[d]=y)}}return[...p.values()].map(({template:i,testValues:d})=>({...i,testValues:Object.keys(d).length>0?d:void 0}))}if(e==="countBin"||e==="percent"){if(u===void 0)return[];const a=we(c,u,"hard");if(e==="countBin")return a;const l=c.length;return a.map(s=>({...s,testValues:{0:l>0?(s.testValues?.[0]??0)/l*100:0}}))}if(e==="mode"){const a=new Map;for(const s of c)for(const h of s){const p=B(h);a.has(p)||a.set(p,[]),a.get(p).push(h)}const l=[];for(const[s,h]of a){const p=s.split(","),i=Number(p[0]),d=Number(p[1]),o=h.map(m=>m.hbin).filter(m=>m!==void 0),r=xe(o);r!==null&&l.push({x:i,y:d,hbin:r})}return l}return[]}function J(t){const n=t.filter(k),e=n.length,u=n.filter(a=>a.edgeExcluded).length,c=n.filter(a=>dieHasTestData(a)||a.hbin!==void 0||a.sbin!==void 0).length;return{filledDies:c,totalDies:e,edgeExcludedDies:u,unpositionedDies:t.length-n.length,ratio:e>0?c/e:0}}function Q(t,n,e="exclude"){const u=new Set(n),a=t.filter(r=>!r.partial).filter(r=>r.edgeExcluded).length,l=t.filter(r=>r.partial).length;let s=0,h=0,p=!1;for(const r of t){if(!fe(r))continue;const m=r.hbin??r.sbin;m!==void 0&&(p=!0,u.has(m)?s++:h++)}const i=s+h,d=p&&i>0?s/i*100:null;let o=null;if(e==="denominator-only"){const r=i+a;o=p&&r>0?s/r*100:null}return{passDies:s,failDies:h,edgeExcludedDies:a,partialDies:l,totalDies:i,yieldPercent:d,yieldPercentGross:o}}function Z(t,n,e,u,c,a=0,l=0,s=0,h=0,p=0,i=!1,d=!1){if(!t)return[];const o=t.anchorDie??{x:0,y:0},r=ge(n,{width:t.width,height:t.height,diePitchX:u,diePitchY:c,anchorDie:{x:o.x-a,y:o.y-l},gridOrigin:{x:-s,y:-h}}),m=ue(le(i,d,n.center.x,n.center.y),ce(p,n.center.x,n.center.y));return r.filter(y=>{const v=y.width/2,w=y.height/2,b=[[y.x-v,y.y-w],[y.x+v,y.y-w],[y.x+v,y.y+w],[y.x-v,y.y+w]].map(([g,M])=>he(m,g,M)),C=Math.min(...b.map(g=>g.x)),P=Math.max(...b.map(g=>g.x)),Y=Math.min(...b.map(g=>g.y)),F=Math.max(...b.map(g=>g.y));return e.some(g=>g.physX>=C&&g.physX<P&&g.physY>=Y&&g.physY<F)})}function Re(t,n,e){const c=Math.max(0,n.radius-e)**2;return t.map(a=>{const l=a.physX-n.center.x,s=a.physY-n.center.y;return l*l+s*s>c?{...a,edgeExcluded:!0}:a})}function Ce(t,n,e){const u=t.filter(k),c=t.filter(i=>!k(i)),a=new Map;for(const i of u){let d=a.get(i.x);d||(d=new Map,a.set(i.x,d)),d.set(i.y,(d.get(i.y)??0)+1)}const l=new Set(e);function s(i,d){const o=i.hbin,r=d.hbin;if(o===void 0||r===void 0)return!1;const m=l.has(o),y=l.has(r);return n==="best"?y!==m?y:r<o:y!==m?m:r>o}const h=new Map;for(const i of u){const d=B(i),o=h.get(d);n==="first"&&o||(n==="best"||n==="worst")&&o&&!s(o,i)||h.set(d,i)}return[...Array.from(h.values()).map(i=>{const o=a.get(i.x)?.get(i.y)??1;return o>1?{...i,retestCount:o}:i}),...c]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const u=t.testPass?.[n];if(u!==void 0)return u;if(e!==void 0&&!isParametricTest(e)){const c=t.testValues?.[n];if(c===0||c===1)return c===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function T(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function ee(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function te(t){return(t.warnings??[]).map(n=>({code:Pe(n),message:n,severity:"error",confidence:t.wafer.confidence}))}function Pe(t){return t.includes(ne)?"geometry-conflict":t.includes(ie)?"inferred-pitch":"partial-coverage"}const ne="do not fit inside the supplied",ie="The die pitch was inferred as";export function buildWaferMap(t,n){const e=be(t),{debug:u,...c}=n??{},a=e.lotStackOpts?ve(e.lotStackOpts,e.testDefs):e.results;for(let f=0;f<Math.min(5,a.length);f++){const x=a[f];if(typeof x.x=="string"||typeof x.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const f of a)if(f.x==null!=(f.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(f.x)}, y=${String(f.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const l=Ce(a,e.retestPolicy,e.passBins),s={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let f=e.explicitDies;const x=l.filter(k),R=l.filter(A=>!k(A));if(x.length>0){const A=new Map(x.map(S=>[B(S),S]));f=f.map(S=>{const z=A.get(B(S));return z?T(S,z):S})}const V=e.waferOpts?.diameter??300,E=j({diameter:V,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});E.orientation!==0&&(f=H(f,E));const K=Z(e.reticleOpts,E,f,1,1,0,0,0,0,E.orientation),re=c.showReticle??e.reticleOpts!==void 0,L=U(E,f,{...c,reticles:K,showReticle:re,plotMode:ee(l,c),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ae=R.map((A,S)=>T({id:`unpositioned_${S}`,width:f[0]?.width??1,height:f[0]?.height??1},A)),_=[...f,...ae];return{wafer:E,dies:_,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:s,warnings:te(s),plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:J(_),yield:Q(_,e.passBins,e.edgeDieYieldMode),reticles:K,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const h=l.filter(k),p=l.filter(f=>!k(f)),i=h.map(f=>({x:f.x,y:f.y})),d=me(i,e.dieOpts,e.waferOpts?.diameter);s.diePitch={confidence:d.confidence,units:d.units};const{pitchX:o,pitchY:r}=d,m=d.units,y=Me(h,e.dieOpts),v=ye(i);s.grid={confidence:v.confidence};const{offsetX:w,offsetY:b}=De(i,y,v),{flipX:C,flipY:P}=ke(e.dieOpts,y),{colMidX:Y,colMidY:F,anchored:g}=Oe(i,w,b,o,r,e.waferOpts?.center);let M=e.waferOpts?.diameter;const $=i.map(f=>({x:(Math.round(f.x)-w)*o-Y,y:(Math.round(f.y)-b)*r-F})),X=$.length>0?Math.max(...$.map(({x:f,y:x})=>Math.hypot(Math.abs(f)+o/2,Math.abs(x)+r/2))):0;if(M===void 0)if(i.length>0)if(m==="mm"){const f=pe($,{minRadius:X});M=f.diameter,s.wafer={confidence:f.confidence,method:f.method}}else M=X*2,s.wafer={confidence:d.confidence*.8,method:"extent"};else M=m==="mm"?300:30,s.wafer={confidence:0,method:"default"};if(!g&&i.length>0&&Se(i,w,b)&&(s.wafer.method="inferred-partial",(s.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0){const f=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(f&&X>M/2+1e-9){const x=$.filter(({x:R,y:V})=>Math.hypot(Math.abs(R)+o/2,Math.abs(V)+r/2)>M/2+1e-9).length;(s.warnings??=[]).push(`${x} of ${$.length} probed die positions ${ne} ${M} mm wafer at the supplied die pitch of ${o} \xD7 ${r} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(X*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}else f||(s.warnings??=[]).push(`${ie} ${o.toFixed(3)} \xD7 ${r.toFixed(3)} mm by assuming the die grid fits within the ${M} mm wafer. The assumed aspect ratio may not match the true die shape whenever edge dies are absent from the data (a reticle-complete map, or partial dies filtered out upstream). Supply dieConfig.width and dieConfig.height \u2014 the die pitch, not the diameter, is what fixes placement.`)}const O=j({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata}),Ee={width:o,height:r};let D=h.map(f=>{const x=Math.round(f.x)-w,R=Math.round(f.y)-b,V={id:`${x}_${R}`,x,y:R,physX:x*o-Y,physY:R*r-F,width:o,height:r,insideWafer:!0,partial:!1};return T(V,f)});(w!==0||b!==0)&&(D=D.map(f=>({...f,x:f.x+w,y:f.y+b,id:`${f.x+w}_${f.y+b}`}))),D=H(D,O),(C||P)&&(D=de(D,{flipX:C,flipY:P},O.center));const N=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(D=Re(D,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&N.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${O.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const G=Z(e.reticleOpts,O,D,o,r,w,b,Y,F,O.orientation,C,P),se=c.showReticle??e.reticleOpts!==void 0,I=U(O,D,{...c,reticles:G,showReticle:se,plotMode:ee(l,c),testDefs:e.testDefs,dataAxisFlip:{x:C,y:P},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),oe=p.map((f,x)=>T({id:`unpositioned_${x}`,width:o,height:r},f)),W=[...D,...oe];return{wafer:O,dies:W,view:I,reticleConfig:e.reticleOpts,units:m,inference:s,warnings:[...te(s),...N],plotMode:I.plotMode,metadata:I.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:J(W),yield:Q(W,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:I.aggrMethod,lotSize:I.lotSize}}
|
|
1
|
+
import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as ce,getDieKey as V,hasPosition as E}from"../core/dies.js";import{applyOrientation as q,transformDies as le}from"../core/transforms.js";import{affineRotation as ue,affineMirror as he,affineCompose as pe,affinePoint as me}from"../core/transforms.js";import{inferWaferFromXY as ge}from"../core/inference/wafer.js";import{resolveGridPitch as ye}from"../core/inference/pitch.js";import{assignGridIndices as we}from"../core/inference/grid.js";import{generateReticleGrid as xe}from"../core/reticle.js";import{buildView as H}from"./buildView.js";import{modeOf as De}from"../core/utils.js";import{aggregateValues as U,aggregateBinCounts as be}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function Me(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],standardDiameters:t.standardDiameters,testDefs:t.testDefs?.length?t.testDefs:void 0,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Oe(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Re(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let u=1/0,c=-1/0,a=1/0,l=-1/0;for(const s of t)s.x<u&&(u=s.x),s.x>c&&(c=s.x),s.y<a&&(a=s.y),s.y>l&&(l=s.y);return{offsetX:Math.round((c+u)/2),offsetY:Math.round((l+a)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Se(t,n,e,u,c,a){if(a)return{colMidX:(Math.round(a.x)-n)*u,colMidY:(Math.round(a.y)-e)*c,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let l=1/0,s=-1/0,h=1/0,p=-1/0;for(const i of t){const f=Math.round(i.x)-n,r=Math.round(i.y)-e;f<l&&(l=f),f>s&&(s=f),r<h&&(h=r),r>p&&(p=r)}return{colMidX:(l+s)/2*u,colMidY:(h+p)/2*c,anchored:!1}}function Ee(t,n,e){let u=1/0,c=-1/0,a=1/0,l=-1/0,s=0,h=0;for(const f of t){const r=Math.round(f.x)-n,d=Math.round(f.y)-e;s+=r,h+=d,r<u&&(u=r),r>c&&(c=r),d<a&&(a=d),d>l&&(l=d)}const p=t.length,i=(f,r,d)=>{const m=(d-r)/2;return m<=0?0:Math.abs(f/p-(r+d)/2)/m};return i(s,u,c)>.11||i(h,a,l)>.11}function ve(t,n){let e=t?.xAxisDirection==="left",u=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(u=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:u}}function ke(t,n){const{method:e,targetBin:u}=t,c=t.results.map(a=>a.filter(E));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const a=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),l=new Set;for(const i of c)for(const f of i)if(f.testValues)for(const r of Object.keys(f.testValues)){const d=Number(r);a.has(d)||l.add(d)}if(l.size===0)return U(c,e);const s=[...l],h=s.map(i=>U(c,e,i)),p=new Map;for(let i=0;i<s.length;i++){const f=s[i];for(const r of h[i]){const d=V(r);p.has(d)||p.set(d,{template:r,testValues:{}});const m=p.get(d),g=r.testValues?.[0];g!==void 0&&(m.testValues[f]=g)}}return[...p.values()].map(({template:i,testValues:f})=>({...i,testValues:Object.keys(f).length>0?f:void 0}))}if(e==="countBin"||e==="percent"){if(u===void 0)return[];const a=be(c,u,"hard");if(e==="countBin")return a;const l=c.length;return a.map(s=>({...s,testValues:{0:l>0?(s.testValues?.[0]??0)/l*100:0}}))}if(e==="mode"){const a=new Map;for(const s of c)for(const h of s){const p=V(h);a.has(p)||a.set(p,[]),a.get(p).push(h)}const l=[];for(const[s,h]of a){const p=s.split(","),i=Number(p[0]),f=Number(p[1]),r=h.map(m=>m.hbin).filter(m=>m!==void 0),d=De(r);d!==null&&l.push({x:i,y:f,hbin:d})}return l}return[]}function J(t){const n=t.filter(E),e=n.length,u=n.filter(a=>a.edgeExcluded).length,c=n.filter(a=>dieHasTestData(a)||a.hbin!==void 0||a.sbin!==void 0).length;return{filledDies:c,totalDies:e,edgeExcludedDies:u,unpositionedDies:t.length-n.length,ratio:e>0?c/e:0}}function Q(t,n,e="exclude"){const u=new Set(n),a=t.filter(d=>!d.partial).filter(d=>d.edgeExcluded).length,l=t.filter(d=>d.partial).length;let s=0,h=0,p=!1;for(const d of t){if(!ce(d))continue;const m=d.hbin??d.sbin;m!==void 0&&(p=!0,u.has(m)?s++:h++)}const i=s+h,f=p&&i>0?s/i*100:null;let r=null;if(e==="denominator-only"){const d=i+a;r=p&&d>0?s/d*100:null}return{passDies:s,failDies:h,edgeExcludedDies:a,partialDies:l,totalDies:i,yieldPercent:f,yieldPercentGross:r}}function Z(t,n,e,u,c,a=0,l=0,s=0,h=0,p=0,i=!1,f=!1){if(!t)return[];const r=t.anchorDie??{x:0,y:0},d=xe(n,{width:t.width,height:t.height,diePitchX:u,diePitchY:c,anchorDie:{x:r.x-a,y:r.y-l},gridOrigin:{x:-s,y:-h}}),m=pe(he(i,f,n.center.x,n.center.y),ue(p,n.center.x,n.center.y));return d.filter(g=>{const v=g.width/2,D=g.height/2,b=[[g.x-v,g.y-D],[g.x+v,g.y-D],[g.x+v,g.y+D],[g.x-v,g.y+D]].map(([w,x])=>me(m,w,x)),k=Math.min(...b.map(w=>w.x)),A=Math.max(...b.map(w=>w.x)),F=Math.min(...b.map(w=>w.y)),P=Math.max(...b.map(w=>w.y));return e.some(w=>w.physX>=k&&w.physX<A&&w.physY>=F&&w.physY<P)})}function Ae(t,n,e){const c=Math.max(0,n.radius-e)**2;return t.map(a=>{const l=a.physX-n.center.x,s=a.physY-n.center.y;return l*l+s*s>c?{...a,edgeExcluded:!0}:a})}function Ce(t,n,e){const u=t.filter(E),c=t.filter(i=>!E(i)),a=new Map;for(const i of u){let f=a.get(i.x);f||(f=new Map,a.set(i.x,f)),f.set(i.y,(f.get(i.y)??0)+1)}const l=new Set(e);function s(i,f){const r=i.hbin,d=f.hbin;if(r===void 0||d===void 0)return!1;const m=l.has(r),g=l.has(d);return n==="best"?g!==m?g:d<r:g!==m?m:d>r}const h=new Map;for(const i of u){const f=V(i),r=h.get(f);n==="first"&&r||(n==="best"||n==="worst")&&r&&!s(r,i)||h.set(f,i)}return[...Array.from(h.values()).map(i=>{const r=a.get(i.x)?.get(i.y)??1;return r>1?{...i,retestCount:r}:i}),...c]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const u=t.testPass?.[n];if(u!==void 0)return u;if(e!==void 0&&!isParametricTest(e)){const c=t.testValues?.[n];if(c===0||c===1)return c===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function L(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function ee(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function te(t){return(t.warnings??[]).map(n=>{const e=Ie(n);return{code:e,message:n,severity:e==="non-standard-diameter"||e==="diameter-exceeds-die-extent"?"warning":"error",confidence:t.wafer.confidence}})}function Ie(t){return t.includes(ne)?"geometry-conflict":t.includes(ie)?"non-standard-diameter":t.includes(se)?"diameter-exceeds-die-extent":"partial-coverage"}const ne="do not fit inside the supplied",ie="Inferred wafer diameter",se="The probed dies reach only",Fe=.75,Pe=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const _e=.02;function oe(t,n){return n.reduce((e,u)=>Math.abs(u-t)<Math.abs(e-t)?u:e)}function Ye(t,n){if(!n.length)return!0;const e=oe(t,n);return Math.abs(e-t)/e<=_e}function $e(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=Me(t),{debug:u,...c}=n??{},a=e.lotStackOpts?ke(e.lotStackOpts,e.testDefs):e.results;for(let o=0;o<Math.min(5,a.length);o++){const y=a[o];if(typeof y.x=="string"||typeof y.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const o of a)if(o.x==null!=(o.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(o.x)}, y=${String(o.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const l=Ce(a,e.retestPolicy,e.passBins),s={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let o=e.explicitDies;const y=l.filter(E),R=l.filter(T=>!E(T));if(y.length>0){const T=new Map(y.map(S=>[V(S),S]));o=o.map(S=>{const G=T.get(V(S));return G?L(S,G):S})}const $=e.waferOpts?.diameter??300,I=j({diameter:$,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});I.orientation!==0&&(o=q(o,I));const z=Z(e.reticleOpts,I,o,1,1,0,0,0,0,I.orientation),de=c.showReticle??e.reticleOpts!==void 0,B=H(I,o,{...c,reticles:z,showReticle:de,plotMode:ee(l,c),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),fe=R.map((T,S)=>L({id:`unpositioned_${S}`,width:o[0]?.width??1,height:o[0]?.height??1},T)),W=[...o,...fe];return{wafer:I,dies:W,view:B,reticleConfig:e.reticleOpts,units:"mm",inference:s,warnings:te(s),plotMode:B.plotMode,metadata:B.metadata,isLotStack:!1,dataCoverage:J(W),yield:Q(W,e.passBins,e.edgeDieYieldMode),reticles:z,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const h=l.filter(E),p=l.filter(o=>!E(o)),i=h.map(o=>({x:o.x,y:o.y})),f=ye(i,e.dieOpts,e.waferOpts?.diameter);s.diePitch={confidence:f.confidence,units:f.units};const{pitchX:r,pitchY:d}=f,m=f.units,g=Oe(h,e.dieOpts),v=we(i);s.grid={confidence:v.confidence};const{offsetX:D,offsetY:b}=Re(i,g,v),{flipX:k,flipY:A}=ve(e.dieOpts,g),{colMidX:F,colMidY:P,anchored:w}=Se(i,D,b,r,d,e.waferOpts?.center);let x=e.waferOpts?.diameter;const _=i.map(o=>({x:(Math.round(o.x)-D)*r-F,y:(Math.round(o.y)-b)*d-P})),C=_.length>0?Math.max(..._.map(({x:o,y})=>Math.hypot(Math.abs(o)+r/2,Math.abs(y)+d/2))):0;if(x===void 0)if(i.length>0)if(m==="mm"){const o=ge(_,{minRadius:C});x=o.diameter,s.wafer={confidence:o.confidence,method:o.method};const y=$e(e.standardDiameters);if(!Ye(x,y)){const R=oe(x,y);(s.warnings??=[]).push(`${ie} ${x.toFixed(1)} mm is not a standard wafer size (nearest is ${R} mm). The diameter was derived from the die extent, which assumes the probed grid reaches the wafer edge \u2014 a non-standard result usually means it does not, because the outer dies were never probed. Every die is then placed against a wafer that is too small, which moves dies between rings and changes ring and edge findings. Supply waferConfig.diameter.`)}}else x=C*2,s.wafer={confidence:f.confidence*.8,method:"extent"};else x=m==="mm"?300:30,s.wafer={confidence:0,method:"default"};if(!w&&i.length>0&&Ee(i,D,b)&&(s.wafer.method="inferred-partial",(s.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&C>0&&i.length>=Pe){const o=C/(x/2);o<Fe&&(s.warnings??=[]).push(`${se} ${(o*100).toFixed(0)}% of the ${x} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`)}if(e.waferOpts?.diameter!==void 0){const o=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(o&&C>x/2+1e-9){const y=_.filter(({x:R,y:$})=>Math.hypot(Math.abs(R)+r/2,Math.abs($)+d/2)>x/2+1e-9).length;(s.warnings??=[]).push(`${y} of ${_.length} probed die positions ${ne} ${x} mm wafer at the supplied die pitch of ${r} \xD7 ${d} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(C*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}}const O=j({diameter:x,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let M=h.map(o=>{const y=Math.round(o.x)-D,R=Math.round(o.y)-b,$={id:`${y}_${R}`,x:y,y:R,physX:y*r-F,physY:R*d-P,width:r,height:d,insideWafer:!0,partial:!1};return L($,o)});(D!==0||b!==0)&&(M=M.map(o=>({...o,x:o.x+D,y:o.y+b,id:`${o.x+D}_${o.y+b}`}))),M=q(M,O),(k||A)&&(M=le(M,{flipX:k,flipY:A},O.center));const X=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(M=Ae(M,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&X.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${O.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const K=Z(e.reticleOpts,O,M,r,d,D,b,F,P,O.orientation,k,A),re=c.showReticle??e.reticleOpts!==void 0,Y=H(O,M,{...c,reticles:K,showReticle:re,plotMode:ee(l,c),testDefs:e.testDefs,dataAxisFlip:{x:k,y:A},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ae=p.map((o,y)=>L({id:`unpositioned_${y}`,width:r,height:d},o)),N=[...M,...ae];return{wafer:O,dies:N,view:Y,reticleConfig:e.reticleOpts,units:m,inference:s,warnings:[...te(s),...X],plotMode:Y.plotMode,metadata:Y.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:J(N),yield:Q(N,e.passBins,e.edgeDieYieldMode),reticles:K,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:Y.aggrMethod,lotSize:Y.lotSize}}
|
|
@@ -6,24 +6,29 @@ export declare const SPEC_PASS_FILL = "#2ecc71";
|
|
|
6
6
|
export declare const SPEC_FAIL_LOW = "#3498db";
|
|
7
7
|
export declare const SPEC_FAIL_HIGH = "#e74c3c";
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
* Fill for a die with no value for what is being plotted — no bin in a bin
|
|
10
|
+
* mode, no reading in a value mode. Shared by the map and every surface that
|
|
11
|
+
* draws a "no data" swatch, so the two can never disagree.
|
|
12
|
+
*/
|
|
13
|
+
export declare const NO_DATA_FILL = "#d6d9dd";
|
|
14
|
+
/**
|
|
15
|
+
* Categorical fallback palette for `'metadata'` mode values beyond
|
|
16
|
+
* METADATA_PALETTE's fixed slots. Index 0 is a no-data grey sentinel.
|
|
17
|
+
* Indices 1–63 are perceptually spread colours generated via golden-angle
|
|
18
|
+
* HSL stepping.
|
|
19
|
+
*
|
|
20
|
+
* No longer used for bins: bin colours are rank-assigned from a registered bin
|
|
21
|
+
* palette by `resolveBinColors` (binColors.ts). Hashing a bin number into this
|
|
22
|
+
* list is what made two different bins share a colour.
|
|
14
23
|
*/
|
|
15
24
|
export declare const BIN_PALETTE: readonly string[];
|
|
16
25
|
/** Wang hash — maps any integer to a well-distributed unsigned 32-bit value. Shared with colorSchemes. */
|
|
17
26
|
export declare function wangHash(n: number): number;
|
|
18
|
-
/** Categorical colour for a hard bin. No-data handling is the caller's responsibility. */
|
|
19
|
-
export declare function hardBinColor(bin: number): string;
|
|
20
27
|
/** Linear interpolation across RGB keypoints for t ∈ [0, 1]. */
|
|
21
28
|
export declare function lerpKp(kp: readonly [number, number, number][], t: number): string;
|
|
22
29
|
export declare const VIRIDIS: readonly [number, number, number][];
|
|
23
30
|
/** Map t ∈ [0, 1] to a Viridis RGB colour string. */
|
|
24
31
|
export declare function valueToViridis(t: number): string;
|
|
25
|
-
/** Categorical colour for a soft bin. No-data handling is the caller's responsibility. */
|
|
26
|
-
export declare function softBinColor(bin: number): string;
|
|
27
32
|
/**
|
|
28
33
|
* Categorical colour for the `index`-th distinct value of an active
|
|
29
34
|
* `'metadata'` field, where `index` comes from sorting the field's distinct
|
|
@@ -31,14 +36,10 @@ export declare function softBinColor(bin: number): string;
|
|
|
31
36
|
* iteration order). Ordered assignment, not hashing, for the first
|
|
32
37
|
* `METADATA_PALETTE.length` slots — this maximizes distinctness for the
|
|
33
38
|
* common case of a handful of categories, unlike a hash which doesn't
|
|
34
|
-
* optimize for a *known* small set. Falls back to
|
|
35
|
-
*
|
|
36
|
-
* salt) for wafers with an unusually large category count.
|
|
39
|
+
* optimize for a *known* small set. Falls back to hashing into `BIN_PALETTE`
|
|
40
|
+
* for wafers with an unusually large category count.
|
|
37
41
|
*/
|
|
38
42
|
export declare function metadataValueColor(index: number): string;
|
|
39
|
-
/** Categorical greyscale shades for hard bins. Index 0 = no data. */
|
|
40
|
-
export declare const HARD_BIN_GREY: readonly string[];
|
|
41
|
-
export declare function hardBinGreyscale(bin: number): string;
|
|
42
43
|
/** Map t ∈ [0, 1] to a greyscale rgb string (range 30–230 to avoid pure black/white). */
|
|
43
44
|
export declare function valueToGreyscale(t: number): string;
|
|
44
45
|
/** Return '#000000' or '#ffffff' for maximum contrast against the given colour. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{clamp01 as
|
|
1
|
+
import{clamp01 as s}from"../core/utils.js";export const SPEC_PASS_FILL="#2ecc71",SPEC_FAIL_LOW="#3498db",SPEC_FAIL_HIGH="#e74c3c",NO_DATA_FILL="#d6d9dd",BIN_PALETTE=["#95a5a6","#1eb84b","#932cdd","#b8a51e","#2cbfdd","#b81e71","#58dd2c","#251eb8","#dd672c","#1eb87f","#ce2cdd","#98b81e","#2c84dd","#b81e3e","#2cdd3b","#581eb8","#dda22c","#1eb8b2","#dd2cb0","#64b81e","#2c49dd","#b8321e","#2cdd76","#8c1eb8","#dcdd2c","#1e8bb8","#dd2c75","#31b81e","#4b2cdd","#b8651e","#2cddb1","#b81eb1","#a1dd2c","#1e57b8","#dd2c3a","#1eb83f","#852cdd","#b8991e","#2ccddd","#b81e7e","#66dd2c","#1e24b8","#dd5a2c","#1eb872","#c02cdd","#a4b81e","#2c92dd","#b81e4a","#2cdd2e","#4c1eb8","#dd942c","#1eb8a6","#dd2cbe","#70b81e","#2c57dd","#b8261e","#2cdd69","#801eb8","#ddcf2c","#1e97b8","#dd2c83","#3db81e","#3d2cdd","#b8591e"];export function wangHash(e){let c=e|0;return c=Math.imul(c^c>>>16,73244475),c=Math.imul(c^c>>>16,73244475),(c^c>>>16)>>>0}const u=BIN_PALETTE.length-1;export function lerpKp(e,c){const n=s(c)*(e.length-1),d=Math.floor(n),t=Math.min(d+1,e.length-1),a=n-d,b=Math.round(e[d][0]+a*(e[t][0]-e[d][0])),o=Math.round(e[d][1]+a*(e[t][1]-e[d][1])),l=Math.round(e[d][2]+a*(e[t][2]-e[d][2]));return`rgb(${b},${o},${l})`}export const VIRIDIS=[[68,1,84],[59,82,139],[33,145,140],[94,201,98],[253,231,37]];export function valueToViridis(e){return lerpKp(VIRIDIS,e)}const f=["#4e79a7","#f28e2b","#e15759","#76b7b2","#59a14f","#edc948","#b07aa1","#ff9da7","#9c755f","#bab0ac"],i=668265263;export function metadataValueColor(e){return e<f.length?f[e]:BIN_PALETTE[wangHash(e^i)%u+1]}export function valueToGreyscale(e){const c=Math.round(s(e)*200+30);return`rgb(${c},${c},${c})`}export function contrastTextColor(e){let c=0,r=0,n=0;const d=e.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);if(d)c=+d[1],r=+d[2],n=+d[3];else{const b=e.replace("#","");c=parseInt(b.slice(0,2),16),r=parseInt(b.slice(2,4),16),n=parseInt(b.slice(4,6),16)}const t=b=>{const o=b/255;return o<=.03928?o/12.92:((o+.055)/1.055)**2.4};return .2126*t(c)+.7152*t(r)+.0722*t(n)>.179?"#000000":"#ffffff"}
|