@wafertools/wafermap 0.30.2 → 0.30.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/AGENTS.md +16 -5
  2. package/CHANGELOG.md +432 -0
  3. package/README.md +6 -3
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +48 -0
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +4 -1
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +4 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +82 -8
  12. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  14. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  15. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/scatter.d.ts +5 -1
  18. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  19. package/dist/packages/canvas-adapter/charts/sweep.d.ts +31 -0
  20. package/dist/packages/canvas-adapter/charts/sweep.js +1 -0
  21. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  22. package/dist/packages/canvas-adapter/charts/trend.d.ts +3 -1
  23. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  24. package/dist/packages/canvas-adapter/dieList.js +2 -2
  25. package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
  26. package/dist/packages/canvas-adapter/drilldown.js +1 -0
  27. package/dist/packages/canvas-adapter/icons.js +1 -1
  28. package/dist/packages/canvas-adapter/insightsTab.d.ts +65 -3
  29. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  30. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  31. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  32. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  33. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  34. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  35. package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
  36. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  37. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  38. package/dist/packages/canvas-adapter/userGuideHtml.js +162 -37
  39. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  40. package/dist/packages/canvas-adapter/version.js +1 -1
  41. package/dist/packages/canvas-adapter/warnings.js +1 -1
  42. package/dist/packages/core/dies.d.ts +1 -1
  43. package/dist/packages/renderer/axisTicks.d.ts +32 -0
  44. package/dist/packages/renderer/axisTicks.js +1 -0
  45. package/dist/packages/renderer/binColors.js +1 -1
  46. package/dist/packages/renderer/buildView.d.ts +11 -37
  47. package/dist/packages/renderer/buildView.js +1 -1
  48. package/dist/packages/renderer/buildWaferMap.d.ts +97 -12
  49. package/dist/packages/renderer/buildWaferMap.js +1 -1
  50. package/dist/packages/renderer/deprecate.d.ts +13 -5
  51. package/dist/packages/renderer/deprecate.js +1 -1
  52. package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
  53. package/dist/packages/renderer/derivedTests/apply.js +1 -0
  54. package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
  55. package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
  56. package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
  57. package/dist/packages/renderer/derivedTests/parser.js +2 -0
  58. package/dist/packages/renderer/fmt.d.ts +24 -2
  59. package/dist/packages/renderer/fmt.js +1 -1
  60. package/dist/packages/renderer/index.d.ts +1 -0
  61. package/dist/packages/renderer/index.js +1 -1
  62. package/dist/packages/renderer/spec.d.ts +66 -0
  63. package/dist/packages/renderer/spec.js +1 -0
  64. package/dist/packages/renderer/testLabel.d.ts +154 -0
  65. package/dist/packages/renderer/testLabel.js +1 -0
  66. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  67. package/dist/packages/stats/capability.d.ts +8 -2
  68. package/dist/packages/stats/capability.js +1 -1
  69. package/dist/packages/stats/correlation.d.ts +4 -1
  70. package/dist/packages/stats/correlation.js +1 -1
  71. package/dist/packages/stats/deprecated.d.ts +4 -1
  72. package/dist/packages/stats/deprecated.js +1 -1
  73. package/dist/packages/stats/index.d.ts +2 -2
  74. package/dist/packages/stats/index.js +1 -1
  75. package/dist/packages/stats/mergeTestDefs.d.ts +1 -1
  76. package/dist/packages/stats/mergeTestDefs.js +1 -1
  77. package/dist/packages/stats/renderFindingsReport.js +8 -28
  78. package/dist/packages/stats/renderSummaryReport.js +19 -28
  79. package/dist/packages/stats/reportHtml.d.ts +27 -0
  80. package/dist/packages/stats/reportHtml.js +35 -14
  81. package/dist/packages/stats/sweep.d.ts +186 -0
  82. package/dist/packages/stats/sweep.js +1 -0
  83. package/dist/packages/stats/sweepXFromName.d.ts +27 -0
  84. package/dist/packages/stats/sweepXFromName.js +1 -0
  85. package/dist/packages/stats/testPassRate.js +1 -1
  86. package/dist/packages/stats/types.d.ts +37 -4
  87. package/llms.txt +3 -2
  88. package/package.json +1 -1
@@ -5,6 +5,9 @@ import type { DieMetadata, WaferMetadata } from '../core/metadata.js';
5
5
  import { type Affine } from '../core/transforms.js';
6
6
  import { type BinColors } from './binColors.js';
7
7
  import type { TestDef, BinDef, MetadataFieldDef, ReticleConfig } from './buildWaferMap.js';
8
+ import { classifySpec, type SpecCategory } from './spec.js';
9
+ export { classifySpec };
10
+ export type { SpecCategory };
8
11
  /** Resolve a toolbar activeTest cursor to the canonical test number for getDieTestValue. */
9
12
  export declare function resolveTestNumber(activeTest: number, testDefs?: TestDef[]): {
10
13
  testNumber: number;
@@ -447,26 +450,6 @@ interface ColorFns {
447
450
  forValue: (t: number) => string;
448
451
  forBin: (bin: number) => string;
449
452
  }
450
- /** A die's spec classification for `value` mode against the active test's limits. */
451
- export type SpecCategory = 'pass' | 'failHigh' | 'failLow';
452
- /**
453
- * Classify a value against the active test's spec limits, using the SAME rules as the value-mode
454
- * die colouring in `pushDieRectangles`. Returns null when there is no value. Shared by the colour
455
- * branch and the spec-count tally so the two never diverge.
456
- *
457
- * Out-of-spec *classification* depends ONLY on whether limits are defined — never on
458
- * `colorbarRangeMode`. An out-of-spec die is always flagged when limits exist, regardless of
459
- * how the colorbar is scaled. The *form* of the indication, decided in `pushDieRectangles`,
460
- * depends only on the effective `passFailDisplay`: under `'spec'` (pass/fail mode) the die gets a solid
461
- * green/blue/red categorical fill; in normal value/gradient mode it keeps the value gradient
462
- * fill (like every other die, so the distribution stays readable and out-of-spec colours don't
463
- * collide with the scheme) and is flagged with a ▽/△ marker (`ViewRect.specMark`). Either way
464
- * the die is never drawn as plain in-spec — the silent correctness bug this guards against.
465
- */
466
- export declare function classifySpec(value: number | undefined, activeTestDef: {
467
- limitLow?: number;
468
- limitHigh?: number;
469
- } | undefined): SpecCategory | null;
470
453
  /** Options for {@link buildHoverText}. All fields are optional. */
471
454
  export interface HoverTextOptions {
472
455
  testDefs?: TestDef[];
@@ -499,24 +482,15 @@ export declare function buildHoverText(die: Die, plotMode: PlotMode, opts?: Hove
499
482
  export interface MapTitleParts {
500
483
  primary: string;
501
484
  secondary: string;
485
+ /**
486
+ * The derived-test key, `"† Derived, not measured"`, when the test the
487
+ * map shows is derived — drawn on its own line below `secondary`, because
488
+ * `primary` then carries the `†` and a glyph must never appear without its
489
+ * key. Absent for a measured test. Deliberately NOT part of the export file
490
+ * name, which reads `primary` and `secondary` only.
491
+ */
492
+ note?: string;
502
493
  }
503
- /**
504
- * Build the on-canvas map title for any plot mode, derived from the View (+ optional bin defs so a
505
- * single-bin stacked card can name its bin). Returns a primary/secondary split so the renderer can
506
- * place the key identifier above the scale and the supporting context below it — keeping each line
507
- * short and never obscured.
508
- *
509
- * Primary / secondary per mode:
510
- * - value → "{name} ({unit})" / —
511
- * - stackedValues → "{name} ({unit}) · {method}" / "stacked ({n} wafers)"
512
- * - hardBin/softBin → "Hard Bin" / "Soft Bin" / —
513
- * - stackedBins → "Hard Bin {bin}" (or "Hard Bin") / "stacked ({n} wafers)"
514
- * - stackedSoftBins → "Soft Bin {bin}" (or "Soft Bin") / "stacked ({n} wafers)"
515
- *
516
- * @param fallbackFormat unitless value formatting — matches the colorbar tick formatting.
517
- * @param binDefs the active bin defs (hbinDefs for hard modes, sbinDefs for soft) — used to name
518
- * the bin on single-bin stacked cards.
519
- */
520
494
  export declare function buildMapTitle(view: View, fallbackFormat?: 'si' | 'engineering', binDefs?: BinDef[]): MapTitleParts;
521
495
  export declare function generateTextOverlay(dies: PositionedDie[], txCoords: Float64Array | null, options: {
522
496
  plotMode: PlotMode;
@@ -1 +1 @@
1
- import{hasPosition as oe}from"../core/dies.js";import{getReticleCell as re}from"../core/reticle.js";import{metadataCategoricalValue as le,metadataDisplayValue as ce}from"../core/metadata.js";import{affineRotation as pt,affineMirror as Rt,affineCompose as st,affinePoint as G,affineVector as yt,affineSwapsAxes as ue}from"../core/transforms.js";import{contrastTextColor as Q,SPEC_PASS_FILL as gt,SPEC_FAIL_LOW as fe,SPEC_FAIL_HIGH as bt}from"./colorMap.js";import{resolveValueColorFn as de}from"./colorSchemes.js";import{resolveBinColors as he,binColorsCover as me}from"./binColors.js";import{NO_DATA_FILL as z}from"./colorMap.js";import{diePassStatus as pe}from"../core/dies.js";import{getDieTestValue as P,getTestPassStatus as U,isParametricTest as xt}from"./buildWaferMap.js";import{fmt as Mt,fmtColorbarAxis as St,fmtAggregationMethod as Nt}from"./fmt.js";import{metadataValueColor as ye}from"./colorMap.js";import{clamp01 as Ot,compareNatural as ge,escHtml as H,minOf as be}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 collectMetadataValues(t,e){const a=new Set;for(const n of t){if(n.partial||n.edgeExcluded)continue;const s=ot(n,e);s!==void 0&&a.add(s)}return[...a].sort(ge)}export function findTestDef(t,e){return t?.find(a=>a.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,a)=>e-a)}const xe="#d3d6db",At="#e8e9ea",Me="#aab0ba";function ot(t,e){return e?le(t.metadata?.[e]):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function Se(t,e,a){const{x:n,y:s}=t.center,i=pt(t.orientation,n,s),o=st(Rt(e?.x??!1,e?.y??!1,n,s),pt(t.orientation,n,s)),l=st(Rt(a?.flipX??!1,a?.flipY??!1,n,s),pt(a?.rotation??0,n,s));return{bakedToScreen:l,physicalToScreen:st(l,i),gridToScreen:st(l,o)}}function K(t,e=!1){return{points:[t],closed:e}}function ke(t,e){const{center:a,radius:n,notch:s}=t;let i=a.x+n*Math.cos(e),o=a.y+n*Math.sin(e);if(!s)return{x:i,y:o};const l=Math.sqrt(n**2-(s.length/2)**2),d=s.length/2,g=i-a.x,p=o-a.y;return s.type==="bottom"&&p<-l?(o=a.y-l,i=a.x+Math.max(-d,Math.min(d,g))):s.type==="top"&&p>l?(o=a.y+l,i=a.x+Math.max(-d,Math.min(d,g))):s.type==="left"&&g<-l?(i=a.x-l,o=a.y+Math.max(-d,Math.min(d,p))):s.type==="right"&&g>l&&(i=a.x+l,o=a.y+Math.max(-d,Math.min(d,p))),{x:i,y:o}}function Et(t,e){return t.map(e).join(" / ")}function ve(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 $e(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 Be(t,e,a){if(e?.length){const n=e.flatMap(s=>{const i=s.testNumber;if(!xt(s)){const l=U(t,i,s);return l===void 0?[]:[{key:i,label:s.name,value:l?"Pass":"Fail"}]}const o=P(t,i);return o===void 0?[]:[{key:i,label:s.name,value:Mt(o,s.unit,a),recordedFail:t.testPass?.[i]===!1}]});if(n.length)return n}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:Mt(s,void 0,a)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,a={}){const{testDefs:n,hbinDefs:s,sbinDefs:i,fallbackFormat:o,aggrMethod:l,lotSize:d,activeTest:g,reticleConfig:p,metadataFields:B}=a,D=s?new Map(s.map(u=>[u.bin,u])):null,T=i?new Map(i.map(u=>[u.bin,u])):null,b=[H(`Die (${t.x}, ${t.y})`)];if(p&&oe(t)){const u=re(t,p);b.push(H(`Reticle (${u.column}, ${u.row})`))}if(e==="stackedValues"){const u=P(t,0);if(u!==void 0){const f=n?.[0],h=f?.testNumber,k=f?.name??(h!=null?`Test ${h}`:"Value"),m=l?` (${l})`:"";b.push(H(`${k}${m}: ${Mt(u,f?.unit,o)}`))}}else if(e==="stackedBins"||e==="stackedSoftBins"){const u=P(t,0),f=e==="stackedSoftBins"?t.sbin:t.hbin;if(u!==void 0){const h=e==="stackedSoftBins"?T:D,k=f!==void 0?h?.get(f)?.name?`${f} \xB7 ${h.get(f).name}`:`Bin ${f}`:"Bin";let m;if(l==="percent")m=`${u.toFixed(0)}%`;else{const C=d?` (${(u/d*100).toFixed(0)}% of stacked wafers)`:"";m=`${u}${C}`}const x=l?` [${Nt(l)}]`:"";b.push(H(`${k}: ${m}${x}`))}}else{const u=Be(t,n,o);if(e==="value"&&u.length){const{testNumber:f}=resolveTestNumber(g??0,n),h=findTestDef(n,f),k=P(t,f);let m=u.findIndex(y=>y.key===f);m<0&&(m=0);const x=u[m];let C=`<b>${H(`${x.label}: ${x.value}`)}${x.recordedFail?" <i>(recorded fail)</i>":""}</b>`;if(x.key===f&&h){const y=classifySpec(k,h);(y==="failLow"||y==="failHigh")&&(C+=" <i>(out of spec)</i>")}b.push(C);const R=u.length-1;R>0&&b.push(`<i>+${R} more test${R===1?"":"s"}</i>`)}else if(u.length){const f=u.length;b.push(`<i>${f} test result${f===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const f=[];if(t.hbin!==void 0){const h=D?.get(t.hbin)?.name,k=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);f.push(`HBin: ${k}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,k=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);f.push(`SBin: ${k}`)}b.push(f.map(H).join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&b.push(H(`Retests: ${t.retestCount}`)),t.siteNum!==void 0&&b.push(H(`Site: ${t.siteNum}`)),t.partId!==void 0&&b.push(H(`Part ID: ${t.partId}`)),t.partial&&b.push("<i>partial die</i>"),t.probeIndex!==void 0&&b.push(H(`Probe: #${t.probeIndex}`));const $={...t.metadata??{}};for(const[u,f]of Object.entries($)){const h=ce(f);if(h===void 0)continue;const k=B?.find(m=>m.key===u)?.label??Ht(u);b.push(H(`${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?Ht(t.activeMetadataKey):"Metadata";return{primary:i?.label??o,secondary:""}}case"stackedValues":{const i=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=St(o,i?.name,i?.unit,e),d=l||"Value",g=t.aggrMethod?` \xB7 ${Nt(t.aggrMethod)}`:"";return{primary:`${d}${g}`,secondary:n}}default:{const i=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=St(o,i?.name,i?.unit,e),d=l||i?.name;if(t.passFailDisplay!=="off"){const g=i?.testNumber??t.activeTest,p=d?`${d} \xB7 #${g}`:`Test ${g}`,B=t.passFailDisplay==="spec"?"Spec pass/fail":i!==void 0&&!xt(i)?"Functional pass/fail":"Tester pass/fail";return{primary:p,secondary:B}}return{primary:d??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,a){const{plotMode:n,colorFns:s,normalize:i,activeTest:o,valueRange:l,testDefs:d,fallbackFormat:g,passFailDisplay:p="off",activeMetadataKey:B,metadataColorMap:D,allIntegerValues:T}=a,b=findTestDef(d,o),{tickFmt:$}=St(l[1],b?.name,b?.unit,g),u=T?h=>String(Math.round(h)):$,{testNumber:f}=resolveTestNumber(o,d);return t.flatMap((h,k)=>{let m="",x="#111111";if(n==="value"&&p==="test"){const y=U(h,f,b);if(y===void 0)return[];m=y?"P":"F",x=Q(y?gt:bt)}else if(n==="value"){const y=P(h,f);if(y===void 0)return[];m=Et([y],u),x=Q(s.forValue(i(y)))}else if(n==="hardBin"||n==="softBin"){const y=n==="softBin"?h.sbin:h.hbin;if(y===void 0)return[];m=String(y),x=Q(s.forBin(y))}else if(n==="metadata"){const y=ot(h,B);if(y===void 0)return[];m=y,x=Q(D?.get(m)??z)}else{const y=P(h,0);if(y===void 0)return[];m=Et([y],u),x=Q(s.forValue(i(y)))}const C=e?e[k*2]:h.physX,R=e?e[k*2+1]:h.physY;return[{x:C,y:R,text:m,fontSize:$e(h,m),color:x,align:"center"}]})}function _t(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function Te(t,e,a=720){const{center:n,radius:s,notch:i}=t,o=$=>G(e,$.x,$.y);if(!(i!==void 0&&t.diameter>150)){const $=[];for(let u=0;u<=a;u++){const f=2*Math.PI*u/a;$.push(o(ke(t,f)))}return[{kind:"wafer-boundary",...K($,!0),lineColor:"#888888",lineWidth:1}]}const d=1.25,g=i.length,p=_t(i.type),B=Math.atan2(g,s),D=s-d,T=[];let b=!1;for(let $=0;$<=a;$++){const u=2*Math.PI*$/a;let f=u-p;for(;f>Math.PI;)f-=2*Math.PI;for(;f<-Math.PI;)f+=2*Math.PI;if(Math.abs(f)<=B){if(!b){b=!0;const h={x:n.x+s*Math.cos(p-B),y:n.y+s*Math.sin(p-B)},k={x:n.x+D*Math.cos(p),y:n.y+D*Math.sin(p)},m={x:n.x+s*Math.cos(p+B),y:n.y+s*Math.sin(p+B)};T.push(o(h),o(k),o(m))}continue}T.push(o({x:n.x+s*Math.cos(u),y:n.y+s*Math.sin(u)}))}return[{kind:"wafer-boundary",...K(T,!0),lineColor:"#888888",lineWidth:1}]}function Ie(t,e,a,n=360){const s=[],i=Math.max(1,Math.floor(a));for(let o=1;o<i;o++){const l=t.radius*o/i,d=[];for(let g=0;g<=n;g++){const p=2*Math.PI*g/n,B={x:t.center.x+l*Math.cos(p),y:t.center.y+l*Math.sin(p)};d.push(G(e,B.x,B.y))}s.push({kind:"ring-boundary",...K(d,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function Ve(t,e,a,n){const{x:s,y:i}=t.center,o=t.radius,l=(u,f)=>G(e,u,f),d=a-s,g=Math.sqrt(Math.max(0,o*o-d*d)),p=l(a,i-g),B=l(a,i+g),D=n-i,T=Math.sqrt(Math.max(0,o*o-D*D)),b=l(s-T,n),$=l(s+T,n);return[{kind:"quadrant-boundary",...K([p,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...K([b,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function Ce(t,e){return t.map(a=>{const n=a.width/2,s=a.height/2;return{kind:"reticle",points:[[[a.x-n,a.y-s],[a.x+n,a.y-s],[a.x+n,a.y+s],[a.x-n,a.y+s]].map(([o,l])=>G(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function Fe(t){const e=t.filter(a=>a.probeIndex!==void 0).sort((a,n)=>(a.probeIndex??0)-(n.probeIndex??0));return e.length?[{kind:"probe-path",...K(e.map(a=>({x:a.physX,y:a.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function De(t,e,a,n,s,i,o,l,d,g,p,B,D,T,b="off",$,u,f){const h=e.width-o,k=e.height-o,m=i?k:h,x=i?h:k,C=S=>s==="softBin"?S.sbin:S.hbin,R=S=>ot(S,$);if(e.partial){t.push({x:a,y:n,width:m,height:x,fill:xe,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:a,y:n,width:m,height:x,fill:Me,type:"stacked",metadata:e.metadata});return}if(d!==void 0&&(s==="hardBin"||s==="softBin")&&C(e)!==d){t.push({x:a,y:n,width:m,height:x,fill:At,type:"hardBin",metadata:e.metadata});return}if(f!==void 0&&s==="metadata"&&R(e)!==f){t.push({x:a,y:n,width:m,height:x,fill:At,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(b==="test"){const W=U(e,p,T),rt=W===void 0?z:W?gt:bt;t.push({x:a,y:n,width:m,height:x,fill:rt,type:"value",metadata:e.metadata});return}const S=P(e,p),N=classifySpec(S,T);let w,J;S===void 0?w=z:b==="spec"?w=N==="failLow"?fe:N==="failHigh"?bt:gt:(w=l.forValue(g(S)),(N==="failLow"||N==="failHigh")&&(J=N)),t.push({x:a,y:n,width:m,height:x,fill:w,type:"value",specMark:J,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const S=C(e),N=S!=null?l.forBin(S):z,w=S!=null&&pe(e,D)===!1?!0:void 0;t.push({x:a,y:n,width:m,height:x,fill:N,type:s,binFail:w,metadata:e.metadata});return}if(s==="metadata"){const S=R(e),N=S!==void 0?u?.get(S)??z:z;t.push({x:a,y:n,width:m,height:x,fill:N,type:"metadata",metadata:e.metadata});return}const y=P(e,0),Y=y!==void 0?l.forValue(g(y)):z;t.push({x:a,y:n,width:m,height:x,fill:Y,type:"value",metadata:e.metadata})}function Pe(t,e,a){const n=t.radius*.15,s=yt(e,n,0),i=yt(e,0,n),o=s.x+i.x>=0?-1:1,l=s.y+i.y>=0?-1:1,d={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},g={x:d.x+s.x,y:d.y+s.y},p={x:d.x+i.x,y:d.y+i.y};return a.push({x:g.x+s.x*.35,y:g.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:p.x+i.x*.35,y:p.y+i.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...K([d,g]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...K([d,p]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,a={},n){const s=a.reticles??[],{plotMode:i="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:d=!1,ringCount:g=4,showRingBoundaries:p=!1,showQuadrantBoundaries:B=!1,showXYIndicator:D=!1,binColorScheme:T="default",valueColorScheme:b="default",reverseValueScheme:$=!1,useDefinedBinColors:u=!0,binColors:f,passBins:h=[1],highlightBin:k,highlightMetadataValue:m,interactiveTransform:x,valueRange:C,metadataValueOrder:R,testDefs:y,activeTest:Y=0,activeMetadataKey:S,fallbackFormat:N="engineering",aggregationMethod:w,lotSize:J,dataAxisFlip:W,isLotStack:rt=!1,logScale:wt,colorbarRangeMode:Wt="spec",passFailDisplay:jt,showPartialDies:zt=!0}=a,kt=jt??"off",lt=n?.hbinDefs,ct=n?.sbinDefs,vt=n?.metadataFields,Kt=vt?.find(c=>c.key===S),qt={x:(W?.x??!1)!==(x?.flipX??!1),y:(W?.y??!1)!==(x?.flipY??!1)},Xt=lt?new Map(lt.map(c=>[c.bin,c])):null,Yt=ct?new Map(ct.map(c=>[c.bin,c])):null,Gt=i==="softBin"?Yt:Xt,Qt=new Set(h),ut=f&&me(f,e)?f:he(e,{passBins:h,binColorScheme:T,hbinDefs:lt,sbinDefs:ct,useDefinedBinColors:u}),Ut=i==="softBin"?ut.soft:ut.hard,$t={forValue:ve(de(b,$)),forBin:c=>Ut.get(c)??z};let{testNumber:A}=resolveTestNumber(Y,y);if(!y?.length&&i==="value"&&!e.some(r=>r.testValues&&A in r.testValues)){const r=e.reduce((M,V)=>{if(!V.testValues)return M;const v=Object.keys(V.testValues).map(Number),L=v.length?be(v):void 0;return L!==void 0&&(M===void 0||L<M)?L:M},void 0);r!==void 0&&(A=r)}let Z;if(Array.isArray(C))Z=C;else if(C){const{testNumber:c}=resolveTestNumber(C.test,y);Z=c===A?C.range:void 0}const I=findTestDef(y,A),Bt=i==="value"&&I!==void 0&&!xt(I),Jt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let _="off";i==="value"&&(Bt?_="test":kt==="spec"&&Jt?_="spec":kt==="test"&&e.some(c=>U(c,A,I)!==void 0)&&(_="test"));const Tt=_==="spec"?"spec":Wt,ft=i==="stackedValues"||i==="stackedBins"||i==="stackedSoftBins";let O,E,tt=!1;if(Z){[O,E]=Z;let c=!0,r=!1;for(const M of e){const V=ft?P(M,0):P(M,A);if(V!==void 0&&(r=!0,!Number.isInteger(V))){c=!1;break}}tt=r&&c}else{const c=!ft&&i==="value"&&Tt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let r=1/0,M=-1/0,V=!0;for(const v of e){const L=ft?P(v,0):P(v,A);L!==void 0&&(L<r&&(r=L),L>M&&(M=L),Number.isInteger(L)||(V=!1))}isFinite(r)||(V=!1),tt=V,c?(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 It=Bt?!1:wt??I?.logScale??!1,Vt=It&&O>0&&E>0,Zt=Vt;let et;if(Vt){const c=Math.log10(O),r=Math.log10(E)-c;et=M=>M<=0?0:Ot((Math.log10(M)-c)/r)}else et=c=>Ot((c-O)/(E-O));const F=Se(t,W,x);let Ct=null;if(t.notch){const c=_t(t.notch.type),r=yt(F.physicalToScreen,Math.cos(c)*t.radius,Math.sin(c)*t.radius),M=Math.hypot(r.x,r.y);M>0&&(Ct={x:r.x/M,y:r.y/M})}let q=1/0;for(let c=0;c<e.length;c++){const r=e[c];r.width<q&&(q=r.width),r.height<q&&(q=r.height)}const Ft=1,te=q!==1/0?Math.min(Ft,q*.12):Ft,Dt=[],dt=[],ee=F.bakedToScreen.a===1&&F.bakedToScreen.b===0&&F.bakedToScreen.c===0&&F.bakedToScreen.d===1&&F.bakedToScreen.e===0&&F.bakedToScreen.f===0;let j=null;if(!ee){j=new Float64Array(e.length*2);for(let c=0;c<e.length;c++){const r=G(F.bakedToScreen,e[c].physX,e[c].physY);j[c*2]=r.x,j[c*2+1]=r.y}}const ne=ue(F.gridToScreen),ae=i==="hardBin"||i==="softBin"||i==="stackedBins"||i==="stackedSoftBins",ie=i==="softBin"||i==="stackedSoftBins";let nt=ae?new Map:void 0;const ht=i==="value"&&_==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,mt=i==="value"&&_==="test"?{pass:0,fail:0}:void 0,Pt=i==="metadata";let at=Pt?new Map:void 0,it=null;if(Pt&&S){const c=collectMetadataValues(e,S),r=R?.key===S?R.values:void 0,M=r?[...r,...c.filter(V=>!r.includes(V))]:c;it=new Map(M.map((V,v)=>[V,Kt?.values?.find(L=>L.value===V)?.color??ye(v)]))}for(let c=0;c<e.length;c++){const r=e[c],M=j?j[c*2]:r.physX,V=j?j[c*2+1]:r.physY;if(dt.push({x:M,y:V}),nt&&!r.partial&&!r.edgeExcluded){const v=ie?r.sbin:r.hbin;v!=null&&nt.set(v,(nt.get(v)??0)+1)}if(ht&&!r.partial&&!r.edgeExcluded){const v=classifySpec(P(r,A),I);v&&ht[v]++}if(mt&&!r.partial&&!r.edgeExcluded){const v=U(r,A,I);v!==void 0&&mt[v?"pass":"fail"]++}if(at&&!r.partial&&!r.edgeExcluded){const v=ot(r,S);v!==void 0&&at.set(v,(at.get(v)??0)+1)}r.partial&&!zt||De(Dt,r,M,V,i,ne,te,$t,k,et,A,Gt,Qt,I,_,S,it,m)}const Lt=o?generateTextOverlay(e,j,{plotMode:i,colorFns:$t,normalize:et,activeTest:Y,valueRange:[O,E],testDefs:y,fallbackFormat:N,passFailDisplay:_,activeMetadataKey:S,metadataColorMap:it,allIntegerValues:tt}):[],X=Te(t,F.physicalToScreen);p&&X.push(...Ie(t,F.physicalToScreen,g)),B&&X.push(...Ve(t,F.physicalToScreen,t.center.x,t.center.y)),l&&X.push(...Ce(s,F.gridToScreen)),d&&X.push(...Fe(e)),D&&X.push(...Pe(t,F.gridToScreen,Lt));const se=dt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Dt,hoverPoints:dt,texts:Lt,overlays:X,plotMode:i,binColorScheme:T,valueColorScheme:b,reverseValueScheme:$,binColors:ut,metadata:t.metadata??null,dies:e,valueRange:[O,E],allIntegerValues:tt,testDefs:y,activeTest:Y,logScale:Zt,logScaleRequested:It,aggrMethod:w,lotSize:J,axisFlip:qt,dataAxisFlip:{x:W?.x??!1,y:W?.y??!1},gridToScreen:F.gridToScreen,rotation:((t.orientation+(x?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:rt,passFailDisplay:_,colorbarRangeMode:Tt,waferCenter:t.center,waferRadius:t.radius,notchDir:Ct,binCounts:nt,metadataCounts:at,metadataColorMap:it,activeMetadataKey:S,metadataFields:vt,specCounts:ht,passFailCounts:mt,dieBounds:se}}export{getDieKey}from"../core/dies.js";
1
+ import{hasPosition as de}from"../core/dies.js";import{getReticleCell as he}from"../core/reticle.js";import{metadataCategoricalValue as me,metadataDisplayValue as pe}from"../core/metadata.js";import{affineRotation as bt,affineMirror as Ht,affineCompose as st,affinePoint as G,affineVector as xt,affineSwapsAxes as ye}from"../core/transforms.js";import{contrastTextColor as Q,SPEC_PASS_FILL as Mt,SPEC_FAIL_LOW as ge,SPEC_FAIL_HIGH as St}from"./colorMap.js";import{resolveValueColorFn as be}from"./colorSchemes.js";import{resolveBinColors as xe,binColorsCover as Me}from"./binColors.js";import{NO_DATA_FILL as z}from"./colorMap.js";import{diePassStatus as Se}from"../core/dies.js";import{getDieTestValue as P,getTestPassStatus as U,isParametricTest as vt}from"./buildWaferMap.js";import{classifySpec as ot}from"./spec.js";export{ot as classifySpec};import{fmt as kt,fmtColorbarAxis as $t,fmtAggregationMethod as _t}from"./fmt.js";import{metadataValueColor as ve}from"./colorMap.js";import{clamp01 as Kt,compareNatural as ke,escHtml as L,minOf as $e}from"../core/utils.js";import{prettyKey as Wt}from"../core/utils.js";import{testLabel as rt,markedTestLabel as lt,derivedTestNote as jt,isDerivedTest as Be,DERIVED_MARK as Te,DERIVED_KEY as Ie}from"./testLabel.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function collectMetadataValues(t,e){const i=new Set;for(const a of t){if(a.partial||a.edgeExcluded)continue;const s=ct(a,e);s!==void 0&&i.add(s)}return[...i].sort(ke)}export function findTestDef(t,e){return t?.find(i=>i.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,i)=>e-i)}const De="#d3d6db",wt="#e8e9ea",Ve="#aab0ba";function ct(t,e){return e?me(t.metadata?.[e]):void 0}function Ce(t,e,i){const{x:a,y:s}=t.center,n=bt(t.orientation,a,s),o=st(Ht(e?.x??!1,e?.y??!1,a,s),bt(t.orientation,a,s)),l=st(Ht(i?.flipX??!1,i?.flipY??!1,a,s),bt(i?.rotation??0,a,s));return{bakedToScreen:l,physicalToScreen:st(l,n),gridToScreen:st(l,o)}}function q(t,e=!1){return{points:[t],closed:e}}function Fe(t,e){const{center:i,radius:a,notch:s}=t;let n=i.x+a*Math.cos(e),o=i.y+a*Math.sin(e);if(!s)return{x:n,y:o};const l=Math.sqrt(a**2-(s.length/2)**2),d=s.length/2,g=n-i.x,p=o-i.y;return s.type==="bottom"&&p<-l?(o=i.y-l,n=i.x+Math.max(-d,Math.min(d,g))):s.type==="top"&&p>l?(o=i.y+l,n=i.x+Math.max(-d,Math.min(d,g))):s.type==="left"&&g<-l?(n=i.x-l,o=i.y+Math.max(-d,Math.min(d,p))):s.type==="right"&&g>l&&(n=i.x+l,o=i.y+Math.max(-d,Math.min(d,p))),{x:n,y:o}}function zt(t,e){return t.map(e).join(" / ")}function Pe(t,e=256){const i=Array.from({length:e},(a,s)=>t(s/(e-1)));return a=>i[Math.min(e-1,Math.max(0,Math.round(a*(e-1))))]}function Le(t,e){const i=Math.max(1,Math.min(t.width,t.height)),a=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(i*.55,a*1.8))))}function Re(t,e,i){if(e?.length){const a=e.flatMap(s=>{const n=s.testNumber;if(!vt(s)){const l=U(t,n,s);return l===void 0?[]:[{key:n,label:lt(s,n),value:l?"Pass":"Fail"}]}const o=P(t,n);return o===void 0?[]:[{key:n,label:lt(s,n),value:kt(o,s.unit,i),recordedFail:t.testPass?.[n]===!1}]});if(a.length)return a}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([a,s])=>({key:Number(a),label:rt(void 0,Number(a)),value:kt(s,void 0,i)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([a,s])=>({key:Number(a),label:rt(void 0,Number(a)),value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,i={}){const{testDefs:a,hbinDefs:s,sbinDefs:n,fallbackFormat:o,aggrMethod:l,lotSize:d,activeTest:g,reticleConfig:p,metadataFields:B}=i,F=s?new Map(s.map(f=>[f.bin,f])):null,T=n?new Map(n.map(f=>[f.bin,f])):null,b=[L(`Die (${t.x}, ${t.y})`)];if(p&&de(t)){const f=he(t,p);b.push(L(`Reticle (${f.column}, ${f.row})`))}if(e==="stackedValues"){const f=P(t,0);if(f!==void 0){const u=a?.[0],h=u?lt(u,u.testNumber):"Value",v=l?` (${l})`:"";b.push(L(`${h}${v}: ${kt(f,u?.unit,o)}`));const m=jt(u);m&&b.push(`<i>${L(m)}</i>`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const f=P(t,0),u=e==="stackedSoftBins"?t.sbin:t.hbin;if(f!==void 0){const h=e==="stackedSoftBins"?T:F,v=u!==void 0?h?.get(u)?.name?`${u} \xB7 ${h.get(u).name}`:`Bin ${u}`:"Bin";let m;if(l==="percent")m=`${f.toFixed(0)}%`;else{const V=d?` (${(f/d*100).toFixed(0)}% of stacked wafers)`:"";m=`${f}${V}`}const x=l?` [${_t(l)}]`:"";b.push(L(`${v}: ${m}${x}`))}}else{const f=Re(t,a,o);if(e==="value"&&f.length){const{testNumber:u}=resolveTestNumber(g??0,a),h=findTestDef(a,u),v=P(t,u);let m=f.findIndex(A=>A.key===u);m<0&&(m=0);const x=f[m];let V=`<b>${L(`${x.label}: ${x.value}`)}${x.recordedFail?" <i>(recorded fail)</i>":""}</b>`;if(x.key===u&&h){const A=ot(v,h);(A==="failLow"||A==="failHigh")&&(V+=" <i>(outside test limits)</i>")}b.push(V);const E=jt(findTestDef(a,x.key));E&&b.push(`<i>${L(E)}</i>`);const y=f.length-1;y>0&&b.push(`<i>+${y} more test${y===1?"":"s"}</i>`)}else if(f.length){const u=f.length;b.push(`<i>${u} test result${u===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const u=[];if(t.hbin!==void 0){const h=F?.get(t.hbin)?.name,v=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);u.push(`HBin: ${v}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,v=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);u.push(`SBin: ${v}`)}b.push(u.map(L).join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&b.push(L(`Retests: ${t.retestCount}`)),t.siteNum!==void 0&&b.push(L(`Site: ${t.siteNum}`)),t.partId!==void 0&&b.push(L(`Part ID: ${t.partId}`)),t.partial&&b.push("<i>partial die</i>"),t.probeIndex!==void 0&&b.push(L(`Probe: #${t.probeIndex}`));const $={...t.metadata??{}};for(const[f,u]of Object.entries($)){const h=pe(u);if(h===void 0)continue;const v=B?.find(m=>m.key===f)?.label??Wt(f);b.push(L(`${v}: ${h}`))}return b.join("<br>")}function Bt(t){return t?.name?lt(t,t.testNumber):void 0}function Tt(t){return Be(t)?{note:`${Te} ${Ie}`}:{}}export function buildMapTitle(t,e="engineering",i){const a=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=n=>{const o=i?.length===1?i[0]:void 0;return o===void 0?`${n} Bin`:o.name?`${n} Bin ${o.bin} \xB7 ${o.name}`:`${n} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:a};case"stackedSoftBins":return{primary:s("Soft"),secondary:a};case"metadata":{const n=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?Wt(t.activeMetadataKey):"Metadata";return{primary:n?.label??o,secondary:""}}case"stackedValues":{const n=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=$t(o,Bt(n),n?.unit,e),d=l||"Value",g=t.aggrMethod?` \xB7 ${_t(t.aggrMethod)}`:"";return{primary:`${d}${g}`,secondary:a,...Tt(n)}}default:{const n=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=$t(o,Bt(n),n?.unit,e),d=l||Bt(n);if(t.passFailDisplay!=="off"){const g=n?.testNumber??t.activeTest,p=d?`${d} \xB7 #${g}`:rt(void 0,g),B=t.passFailDisplay==="spec"?"Limit pass/fail":n!==void 0&&!vt(n)?"Functional pass/fail":"Tester pass/fail";return{primary:p,secondary:B,...Tt(n)}}return{primary:d??rt(void 0,t.activeTest),secondary:"",...Tt(n)}}}}export function generateTextOverlay(t,e,i){const{plotMode:a,colorFns:s,normalize:n,activeTest:o,valueRange:l,testDefs:d,fallbackFormat:g,passFailDisplay:p="off",activeMetadataKey:B,metadataColorMap:F,allIntegerValues:T}=i,b=findTestDef(d,o),{tickFmt:$}=$t(l[1],b?.name,b?.unit,g),f=T?h=>String(Math.round(h)):$,{testNumber:u}=resolveTestNumber(o,d);return t.flatMap((h,v)=>{let m="",x="#111111";if(a==="value"&&p==="test"){const y=U(h,u,b);if(y===void 0)return[];m=y?"P":"F",x=Q(y?Mt:St)}else if(a==="value"){const y=P(h,u);if(y===void 0)return[];m=zt([y],f),x=Q(s.forValue(n(y)))}else if(a==="hardBin"||a==="softBin"){const y=a==="softBin"?h.sbin:h.hbin;if(y===void 0)return[];m=String(y),x=Q(s.forBin(y))}else if(a==="metadata"){const y=ct(h,B);if(y===void 0)return[];m=y,x=Q(F?.get(m)??z)}else{const y=P(h,0);if(y===void 0)return[];m=zt([y],f),x=Q(s.forValue(n(y)))}const V=e?e[v*2]:h.physX,E=e?e[v*2+1]:h.physY;return[{x:V,y:E,text:m,fontSize:Le(h,m),color:x,align:"center"}]})}function qt(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function Ne(t,e,i=720){const{center:a,radius:s,notch:n}=t,o=$=>G(e,$.x,$.y);if(!(n!==void 0&&t.diameter>150)){const $=[];for(let f=0;f<=i;f++){const u=2*Math.PI*f/i;$.push(o(Fe(t,u)))}return[{kind:"wafer-boundary",...q($,!0),lineColor:"#888888",lineWidth:1}]}const d=1.25,g=n.length,p=qt(n.type),B=Math.atan2(g,s),F=s-d,T=[];let b=!1;for(let $=0;$<=i;$++){const f=2*Math.PI*$/i;let u=f-p;for(;u>Math.PI;)u-=2*Math.PI;for(;u<-Math.PI;)u+=2*Math.PI;if(Math.abs(u)<=B){if(!b){b=!0;const h={x:a.x+s*Math.cos(p-B),y:a.y+s*Math.sin(p-B)},v={x:a.x+F*Math.cos(p),y:a.y+F*Math.sin(p)},m={x:a.x+s*Math.cos(p+B),y:a.y+s*Math.sin(p+B)};T.push(o(h),o(v),o(m))}continue}T.push(o({x:a.x+s*Math.cos(f),y:a.y+s*Math.sin(f)}))}return[{kind:"wafer-boundary",...q(T,!0),lineColor:"#888888",lineWidth:1}]}function Oe(t,e,i,a=360){const s=[],n=Math.max(1,Math.floor(i));for(let o=1;o<n;o++){const l=t.radius*o/n,d=[];for(let g=0;g<=a;g++){const p=2*Math.PI*g/a,B={x:t.center.x+l*Math.cos(p),y:t.center.y+l*Math.sin(p)};d.push(G(e,B.x,B.y))}s.push({kind:"ring-boundary",...q(d,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function Ee(t,e,i,a){const{x:s,y:n}=t.center,o=t.radius,l=(f,u)=>G(e,f,u),d=i-s,g=Math.sqrt(Math.max(0,o*o-d*d)),p=l(i,n-g),B=l(i,n+g),F=a-n,T=Math.sqrt(Math.max(0,o*o-F*F)),b=l(s-T,a),$=l(s+T,a);return[{kind:"quadrant-boundary",...q([p,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...q([b,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function Ae(t,e){return t.map(i=>{const a=i.width/2,s=i.height/2;return{kind:"reticle",points:[[[i.x-a,i.y-s],[i.x+a,i.y-s],[i.x+a,i.y+s],[i.x-a,i.y+s]].map(([o,l])=>G(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function He(t){const e=t.filter(i=>i.probeIndex!==void 0).sort((i,a)=>(i.probeIndex??0)-(a.probeIndex??0));return e.length?[{kind:"probe-path",...q(e.map(i=>({x:i.physX,y:i.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function _e(t,e,i,a,s,n,o,l,d,g,p,B,F,T,b="off",$,f,u){const h=e.width-o,v=e.height-o,m=n?v:h,x=n?h:v,V=S=>s==="softBin"?S.sbin:S.hbin,E=S=>ct(S,$);if(e.partial){t.push({x:i,y:a,width:m,height:x,fill:De,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:i,y:a,width:m,height:x,fill:Ve,type:"stacked",metadata:e.metadata});return}if(d!==void 0&&(s==="hardBin"||s==="softBin")&&V(e)!==d){t.push({x:i,y:a,width:m,height:x,fill:wt,type:"hardBin",metadata:e.metadata});return}if(u!==void 0&&s==="metadata"&&E(e)!==u){t.push({x:i,y:a,width:m,height:x,fill:wt,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(b==="test"){const j=U(e,p,T),ut=j===void 0?z:j?Mt:St;t.push({x:i,y:a,width:m,height:x,fill:ut,type:"value",metadata:e.metadata});return}const S=P(e,p),N=ot(S,T);let W,J;S===void 0?W=z:b==="spec"?W=N==="failLow"?ge:N==="failHigh"?St:Mt:(W=l.forValue(g(S)),(N==="failLow"||N==="failHigh")&&(J=N)),t.push({x:i,y:a,width:m,height:x,fill:W,type:"value",specMark:J,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const S=V(e),N=S!=null?l.forBin(S):z,W=S!=null&&Se(e,F)===!1?!0:void 0;t.push({x:i,y:a,width:m,height:x,fill:N,type:s,binFail:W,metadata:e.metadata});return}if(s==="metadata"){const S=E(e),N=S!==void 0?f?.get(S)??z:z;t.push({x:i,y:a,width:m,height:x,fill:N,type:"metadata",metadata:e.metadata});return}const y=P(e,0),A=y!==void 0?l.forValue(g(y)):z;t.push({x:i,y:a,width:m,height:x,fill:A,type:"value",metadata:e.metadata})}function Ke(t,e,i){const a=t.radius*.15,s=xt(e,a,0),n=xt(e,0,a),o=s.x+n.x>=0?-1:1,l=s.y+n.y>=0?-1:1,d={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},g={x:d.x+s.x,y:d.y+s.y},p={x:d.x+n.x,y:d.y+n.y};return i.push({x:g.x+s.x*.35,y:g.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:p.x+n.x*.35,y:p.y+n.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...q([d,g]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...q([d,p]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,i={},a){const s=i.reticles??[],{plotMode:n="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:d=!1,ringCount:g=4,showRingBoundaries:p=!1,showQuadrantBoundaries:B=!1,showXYIndicator:F=!1,binColorScheme:T="default",valueColorScheme:b="default",reverseValueScheme:$=!1,useDefinedBinColors:f=!0,binColors:u,passBins:h=[1],highlightBin:v,highlightMetadataValue:m,interactiveTransform:x,valueRange:V,metadataValueOrder:E,testDefs:y,activeTest:A=0,activeMetadataKey:S,fallbackFormat:N="engineering",aggregationMethod:W,lotSize:J,dataAxisFlip:j,isLotStack:ut=!1,logScale:Xt,colorbarRangeMode:Yt="spec",passFailDisplay:Gt,showPartialDies:Qt=!0}=i,It=Gt??"off",ft=a?.hbinDefs,dt=a?.sbinDefs,Dt=a?.metadataFields,Ut=Dt?.find(c=>c.key===S),Jt={x:(j?.x??!1)!==(x?.flipX??!1),y:(j?.y??!1)!==(x?.flipY??!1)},Zt=ft?new Map(ft.map(c=>[c.bin,c])):null,te=dt?new Map(dt.map(c=>[c.bin,c])):null,ee=n==="softBin"?te:Zt,ne=new Set(h),ht=u&&Me(u,e)?u:xe(e,{passBins:h,binColorScheme:T,hbinDefs:ft,sbinDefs:dt,useDefinedBinColors:f}),ae=n==="softBin"?ht.soft:ht.hard,Vt={forValue:Pe(be(b,$)),forBin:c=>ae.get(c)??z};let{testNumber:H}=resolveTestNumber(A,y);if(!y?.length&&n==="value"&&!e.some(r=>r.testValues&&H in r.testValues)){const r=e.reduce((M,D)=>{if(!D.testValues)return M;const k=Object.keys(D.testValues).map(Number),R=k.length?$e(k):void 0;return R!==void 0&&(M===void 0||R<M)?R:M},void 0);r!==void 0&&(H=r)}let Z;if(Array.isArray(V))Z=V;else if(V){const{testNumber:c}=resolveTestNumber(V.test,y);Z=c===H?V.range:void 0}const I=findTestDef(y,H),Ct=n==="value"&&I!==void 0&&!vt(I),ie=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let K="off";n==="value"&&(Ct?K="test":It==="spec"&&ie?K="spec":It==="test"&&e.some(c=>U(c,H,I)!==void 0)&&(K="test"));const Ft=K==="spec"?"spec":Yt,mt=n==="stackedValues"||n==="stackedBins"||n==="stackedSoftBins";let O,_,tt=!1;if(Z){[O,_]=Z;let c=!0,r=!1;for(const M of e){const D=mt?P(M,0):P(M,H);if(D!==void 0&&(r=!0,!Number.isInteger(D))){c=!1;break}}tt=r&&c}else{const c=!mt&&n==="value"&&Ft==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let r=1/0,M=-1/0,D=!0;for(const k of e){const R=mt?P(k,0):P(k,H);R!==void 0&&(R<r&&(r=R),R>M&&(M=R),Number.isInteger(R)||(D=!1))}isFinite(r)||(D=!1),tt=D,c?(O=I.limitLow!==void 0?I.limitLow:isFinite(r)?r:0,_=I.limitHigh!==void 0?I.limitHigh:isFinite(M)?M:1):(O=isFinite(r)?r:0,_=isFinite(M)?M:1)}O===_&&(_=O+1);const Pt=Ct?!1:Xt??I?.logScale??!1,Lt=Pt&&O>0&&_>0,se=Lt;let et;if(Lt){const c=Math.log10(O),r=Math.log10(_)-c;et=M=>M<=0?0:Kt((Math.log10(M)-c)/r)}else et=c=>Kt((c-O)/(_-O));const C=Ce(t,j,x);let Rt=null;if(t.notch){const c=qt(t.notch.type),r=xt(C.physicalToScreen,Math.cos(c)*t.radius,Math.sin(c)*t.radius),M=Math.hypot(r.x,r.y);M>0&&(Rt={x:r.x/M,y:r.y/M})}let X=1/0;for(let c=0;c<e.length;c++){const r=e[c];r.width<X&&(X=r.width),r.height<X&&(X=r.height)}const Nt=1,oe=X!==1/0?Math.min(Nt,X*.12):Nt,Ot=[],pt=[],re=C.bakedToScreen.a===1&&C.bakedToScreen.b===0&&C.bakedToScreen.c===0&&C.bakedToScreen.d===1&&C.bakedToScreen.e===0&&C.bakedToScreen.f===0;let w=null;if(!re){w=new Float64Array(e.length*2);for(let c=0;c<e.length;c++){const r=G(C.bakedToScreen,e[c].physX,e[c].physY);w[c*2]=r.x,w[c*2+1]=r.y}}const le=ye(C.gridToScreen),ce=n==="hardBin"||n==="softBin"||n==="stackedBins"||n==="stackedSoftBins",ue=n==="softBin"||n==="stackedSoftBins";let nt=ce?new Map:void 0;const yt=n==="value"&&K==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,gt=n==="value"&&K==="test"?{pass:0,fail:0}:void 0,Et=n==="metadata";let at=Et?new Map:void 0,it=null;if(Et&&S){const c=collectMetadataValues(e,S),r=E?.key===S?E.values:void 0,M=r?[...r,...c.filter(D=>!r.includes(D))]:c;it=new Map(M.map((D,k)=>[D,Ut?.values?.find(R=>R.value===D)?.color??ve(k)]))}for(let c=0;c<e.length;c++){const r=e[c],M=w?w[c*2]:r.physX,D=w?w[c*2+1]:r.physY;if(pt.push({x:M,y:D}),nt&&!r.partial&&!r.edgeExcluded){const k=ue?r.sbin:r.hbin;k!=null&&nt.set(k,(nt.get(k)??0)+1)}if(yt&&!r.partial&&!r.edgeExcluded){const k=ot(P(r,H),I);k&&yt[k]++}if(gt&&!r.partial&&!r.edgeExcluded){const k=U(r,H,I);k!==void 0&&gt[k?"pass":"fail"]++}if(at&&!r.partial&&!r.edgeExcluded){const k=ct(r,S);k!==void 0&&at.set(k,(at.get(k)??0)+1)}r.partial&&!Qt||_e(Ot,r,M,D,n,le,oe,Vt,v,et,H,ee,ne,I,K,S,it,m)}const At=o?generateTextOverlay(e,w,{plotMode:n,colorFns:Vt,normalize:et,activeTest:A,valueRange:[O,_],testDefs:y,fallbackFormat:N,passFailDisplay:K,activeMetadataKey:S,metadataColorMap:it,allIntegerValues:tt}):[],Y=Ne(t,C.physicalToScreen);p&&Y.push(...Oe(t,C.physicalToScreen,g)),B&&Y.push(...Ee(t,C.physicalToScreen,t.center.x,t.center.y)),l&&Y.push(...Ae(s,C.gridToScreen)),d&&Y.push(...He(e)),F&&Y.push(...Ke(t,C.gridToScreen,At));const fe=pt.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Ot,hoverPoints:pt,texts:At,overlays:Y,plotMode:n,binColorScheme:T,valueColorScheme:b,reverseValueScheme:$,binColors:ht,metadata:t.metadata??null,dies:e,valueRange:[O,_],allIntegerValues:tt,testDefs:y,activeTest:A,logScale:se,logScaleRequested:Pt,aggrMethod:W,lotSize:J,axisFlip:Jt,dataAxisFlip:{x:j?.x??!1,y:j?.y??!1},gridToScreen:C.gridToScreen,rotation:((t.orientation+(x?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:ut,passFailDisplay:K,colorbarRangeMode:Ft,waferCenter:t.center,waferRadius:t.radius,notchDir:Rt,binCounts:nt,metadataCounts:at,metadataColorMap:it,activeMetadataKey:S,metadataFields:Dt,specCounts:yt,passFailCounts:gt,dieBounds:fe}}export{getDieKey}from"../core/dies.js";
@@ -2,6 +2,8 @@ import type { Die } from '../core/dies.js';
2
2
  import type { DieMetadata, WaferMetadata } from '../core/metadata.js';
3
3
  import type { Wafer } from '../core/wafer.js';
4
4
  import type { Reticle } from '../core/reticle.js';
5
+ import { type DerivedTestDef } from './derivedTests/apply.js';
6
+ export type { DerivedTestDef } from './derivedTests/apply.js';
5
7
  import { type View, type ViewOptions, type PlotMode } from './buildView.js';
6
8
  /**
7
9
  * Test result for a single die position, as output by the prober.
@@ -55,12 +57,19 @@ export interface DieResult {
55
57
  */
56
58
  siteNum?: number;
57
59
  /**
58
- * STDF `pir.part_id` — tester-assigned identifier for this tested unit.
59
- * At most fabs this encodes the probe sequence (the order in which the prober
60
- * stepped across the wafer), but the field is semantically neutral — its
61
- * meaning is fab-specific.
60
+ * STDF PRR `PART_ID` — the tester's part identification, text in STDF (a
61
+ * number is accepted too). Data about the part, not an identifier: a die's
62
+ * identity is its position. Its meaning is fab-specific; at many fabs it
63
+ * encodes the probe sequence.
62
64
  */
63
- partId?: number;
65
+ partId?: number | string;
66
+ /**
67
+ * The tester marked this record as replacing an earlier one (STDF PRR
68
+ * `PART_FLG` bit 0/1): `'partId'` — the earlier record with the same part ID;
69
+ * `'position'` — the earlier record at the same position. Such a record always
70
+ * wins, whatever `retestPolicy` says.
71
+ */
72
+ supersedes?: 'partId' | 'position';
64
73
  /** Per-die metadata — all fields appear automatically in hover tooltips. See `DieMetadata → §12.4`. */
65
74
  metadata?: DieMetadata;
66
75
  }
@@ -212,16 +221,28 @@ export interface TestDef {
212
221
  */
213
222
  logScale?: boolean;
214
223
  /**
215
- * Lower specification limit in the same units as the test value.
216
- * When set, values below this limit are considered out-of-spec.
217
- * Both limits are optional independently — some tests have one-sided limits.
224
+ * Lower **test limit** (STDF `LO_LIMIT`) — the pass/fail limit, in the test
225
+ * value's units. A value below it is outside the test limits. Both limits are
226
+ * optional independently — some tests have one-sided limits.
218
227
  */
219
228
  limitLow?: number;
229
+ /** Upper test limit (STDF `HI_LIMIT`). A value above it is outside the test limits. */
230
+ limitHigh?: number;
220
231
  /**
221
- * Upper specification limit in the same units as the test value.
222
- * When set, values above this limit are considered out-of-spec.
232
+ * Whether a value exactly equal to `limitLow` passes. Default `true`. STDF
233
+ * states it per test in `PARM_FLG` bit 6, ATDF in the Limit Compare field.
223
234
  */
224
- limitHigh?: number;
235
+ limitLowInclusive?: boolean;
236
+ /** Whether a value exactly equal to `limitHigh` passes. Default `true`; STDF `PARM_FLG` bit 7. */
237
+ limitHighInclusive?: boolean;
238
+ /**
239
+ * Lower **specification limit** (STDF `LO_SPEC`) — separate from the test limits,
240
+ * and what process capability (Cp/Cpk/Pp/Ppk) is judged against when both spec
241
+ * limits are given. Without them, capability uses the test limits and says so.
242
+ */
243
+ specLow?: number;
244
+ /** Upper specification limit (STDF `HI_SPEC`). See `specLow`. */
245
+ specHigh?: number;
225
246
  /**
226
247
  * Test kind: `'P'` (parametric — a continuous measured value) or `'F'`
227
248
  * (functional — a pass/fail outcome, conventionally recorded as 1 = pass,
@@ -232,6 +253,24 @@ export interface TestDef {
232
253
  * would be meaningless. Default: `'P'`.
233
254
  */
234
255
  testType?: 'P' | 'F';
256
+ /**
257
+ * Set by `buildWaferMap` on a test computed from `derivedTests`, never by the
258
+ * caller. It lives on `TestDef` rather than only on `DerivedTestDef` because
259
+ * `result.testDefs` is one homogeneous `TestDef[]`: every display surface
260
+ * reads its test names from there, and a derived value must be
261
+ * distinguishable from a measured one without a cast — an engineer reading a
262
+ * Cpk table cannot be left to assume a number came off the tester.
263
+ */
264
+ readonly derived?: true;
265
+ /**
266
+ * The expression a `derived` test was computed from, carried through verbatim
267
+ * so a display surface can answer "where did this number come from" without
268
+ * the caller having to hold the definition alongside the result. Absent on a
269
+ * measured test.
270
+ */
271
+ readonly expression?: string;
272
+ /** Named constants the `expression` resolved, for the same reason. */
273
+ readonly constants?: Record<string, number>;
235
274
  }
236
275
  /**
237
276
  * True when `def` describes a parametric (continuous-value) test — i.e. its
@@ -368,6 +407,34 @@ export interface WaferMapInputBase {
368
407
  * Both spaces range 0–32767 and may overlap — define them separately.
369
408
  */
370
409
  sbinDefs?: BinDef[];
410
+ /**
411
+ * Tests computed from other tests on the same die, rather than measured.
412
+ *
413
+ * Each entry is a `TestDef` plus an `expression`. They are evaluated per probe
414
+ * record at build time — before lot stacking and before retest resolution, so
415
+ * every value is derived from one real touchdown — and then behave as ordinary
416
+ * tests: they appear in `testDefs`, in the value plot modes, the colorbar,
417
+ * tooltips, `analyzeWaferMap`, Insights and the report.
418
+ *
419
+ * ```ts
420
+ * derivedTests: [
421
+ * { testNumber: 900001, name: 'Leakage Shift', unit: 'uA',
422
+ * expression: 'abs(t[1020] - t[1010])', limitHigh: 5 },
423
+ * { testNumber: 900002, name: 'Sweep All Pass', testType: 'F',
424
+ * expression: 'all(testPass[1010..1025])' },
425
+ * ]
426
+ * ```
427
+ *
428
+ * Expressions are parsed to a typed tree and walked — there is no `eval`, no
429
+ * `Function`, and no way to reach a host object — so a spec can be shared as
430
+ * plain JSON between teams. An entry that cannot be compiled is DROPPED with a
431
+ * `derived-test-invalid` warning naming the position; it is never partially
432
+ * applied.
433
+ *
434
+ * A boolean expression is a verdict: declare `testType: 'F'` and the result
435
+ * lands in `die.testPass`, never as a 1/0 in `testValues`.
436
+ */
437
+ derivedTests?: DerivedTestDef[];
371
438
  /**
372
439
  * Named definitions for `die.metadata` keys that should be selectable as the
373
440
  * `'metadata'` plot mode — a generic categorical view driven by whatever
@@ -519,6 +586,24 @@ export interface WaferWarning {
519
586
  * were given as text (or verdicts as something other than true/false), as a
520
587
  * CSV parser produces. They are not converted, so those dies are judged and
521
588
  * plotted wrongly — a bin of "1" is not pass bin 1. The message counts them.
589
+ * - `'input-values-outside-stdf'` — raised by `buildWaferMap`: bins, coordinates,
590
+ * test numbers, site numbers or `waferConfig.orientation` outside the STDF V4
591
+ * ranges, or test values that are not finite. Used as given for now; a future
592
+ * release treats them as missing. A `NaN` bin is already treated as no bin, and
593
+ * is counted here too.
594
+ * - `'retests-by-part-id'` — raised by `buildWaferMap` (severity `'info'`): dies
595
+ * with no position that share a part ID were treated as retests of one die.
596
+ * Blank part IDs never match, and part IDs are not used on a wafer where one
597
+ * value covers more than 20% of the unpositioned records (a default, not an ID).
598
+ * - `'derived-test-invalid'` — raised by `buildWaferMap`: a `derivedTests` entry
599
+ * could not be compiled — a parse or type error in its `expression`, a
600
+ * `testNumber` that collides with a measured test, a `testType` that
601
+ * disagrees with what the expression produces, or an accessor that is
602
+ * statically impossible (`t[n]` on a functional test, `specPass[n]` on a test
603
+ * with no limits). The message names the test and the position in the
604
+ * expression. That derived test is DROPPED — never partially applied, since a
605
+ * half-working expression plots wrong numbers rather than no numbers. Also
606
+ * used for the non-fatal unit-mismatch advisory, where the value IS computed.
522
607
  * - `'input-field-removed'` — raised by `buildWaferMap`: the input used a name
523
608
  * removed in an earlier release (`data`, `die`, `stack`, `values`,
524
609
  * `TestDef.index`, `dieConfig.origin`, `waferConfig.flat`,
@@ -529,7 +614,7 @@ export interface WaferWarning {
529
614
  * The union is intentionally open to string so future advisory codes can be
530
615
  * added without a breaking change; switch with a `default` branch.
531
616
  */
532
- code: 'partial-coverage' | 'geometry-conflict' | 'non-standard-diameter' | 'diameter-exceeds-die-extent' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | 'analysis-option-corrected' | 'bin-colors-shared' | 'pass-bins-mixed' | 'ring-count-mixed' | 'input-field-removed' | 'input-values-not-numbers' | (string & {});
617
+ code: 'partial-coverage' | 'geometry-conflict' | 'non-standard-diameter' | 'diameter-exceeds-die-extent' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | 'analysis-option-corrected' | 'bin-colors-shared' | 'pass-bins-mixed' | 'ring-count-mixed' | 'input-field-removed' | 'input-values-not-numbers' | 'input-values-outside-stdf' | 'retests-by-part-id' | 'derived-test-invalid' | (string & {});
533
618
  /** Human-readable explanation, suitable for direct display. */
534
619
  message: string;
535
620
  /**
@@ -1 +1 @@
1
- import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as he,getDieKey as V,hasPosition as A}from"../core/dies.js";import{applyOrientation as U,transformDies as me,clipDiesToWafer as ge}from"../core/transforms.js";import{generateDies as pe}from"../core/dies.js";import{affineRotation as ye,affineMirror as be,affineCompose as we,affinePoint as xe}from"../core/transforms.js";import{inferWaferFromXY as De}from"../core/inference/wafer.js";import{resolveGridPitch as Me}from"../core/inference/pitch.js";import{assignGridIndices as Ce}from"../core/inference/grid.js";import{generateReticleGrid as ve}from"../core/reticle.js";import{buildView as J}from"./buildView.js";import{maxOf as K,minOf as Q,modeOf as Oe}from"../core/utils.js";import{aggregateValues as Z,aggregateBinCounts as Se}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function ke(t){const n=(l,s)=>({ringCount:l,ringCountWarning:{code:"analysis-option-corrected",severity:"warning",message:`ringCount=${String(t)} ${s} (using ${l}).`}});if(t===void 0)return{ringCount:4,ringCountWarning:void 0};if(typeof t!="number"||!Number.isFinite(t))return n(4,"is not a finite number");const e=Math.max(1,Math.round(t));return e===t?{ringCount:t,ringCountWarning:void 0}:n(e,"is not a whole number of at least 1")}function ee(t){const n=Array.isArray(t)?{}:t,e=Array.isArray(n.lotStack?.results)?n.lotStack.results:[],l=(Array.isArray(t)?[t]:[n.results,...e]).filter(a=>Array.isArray(a)),s={hbin:0,sbin:0,testValues:0,testPass:0};let r;const h=(a,i)=>{s[a]++,r??=`${a} ${JSON.stringify(i)}`};for(const a of l){const i=new Set,f=new Set;for(let p=0;p<a.length;p+=p<50?1:100){const m=a[p];if(!(m===null||typeof m!="object")){if(m.testValues&&typeof m.testValues=="object")for(const D of Object.keys(m.testValues))i.add(D);if(m.testPass&&typeof m.testPass=="object")for(const D of Object.keys(m.testPass))f.add(D)}}const u=[...i],d=[...f];for(const p of a){if(p===null||typeof p!="object")continue;const m=p;m.hbin!=null&&typeof m.hbin!="number"&&h("hbin",m.hbin),m.sbin!=null&&typeof m.sbin!="number"&&h("sbin",m.sbin);const D=m.testValues;if(D&&typeof D=="object")for(let y=0;y<u.length;y++){const b=D[u[y]];b!=null&&typeof b!="number"&&h("testValues",b)}const C=m.testPass;if(C&&typeof C=="object")for(let y=0;y<d.length;y++){const b=C[d[y]];b!=null&&typeof b!="boolean"&&h("testPass",b)}}}const o=[];if(s.hbin&&o.push(`${s.hbin} hard bin${s.hbin===1?"":"s"}`),s.sbin&&o.push(`${s.sbin} soft bin${s.sbin===1?"":"s"}`),s.testValues&&o.push(`${s.testValues} test value${s.testValues===1?"":"s"}`),s.testPass&&o.push(`${s.testPass} pass/fail verdict${s.testPass===1?"":"s"}`),!o.length)return;const g=`buildWaferMap received ${o.join(", ")} of the wrong type (for example ${r}). Bins and test values must be numbers and verdicts true/false \u2014 a bin of "1" is not pass bin 1, so those dies count as fails and yield is wrong, and a test value given as text is not plotted or analysed correctly. Convert them with Number() (and verdicts to booleans) and rebuild.`;return console.warn(`[wafermap] ${g}`),{code:"input-values-not-numbers",message:g,severity:"error"}}function te(t){const n=(a,i)=>a!==null&&typeof a=="object"&&a[i]!==void 0,e=Array.isArray(t)?{}:t,l=[],s=(a,i,f)=>{a&&l.push(`\`${i}\` (now \`${f}\`)`)};s(n(e,"data"),"data","results"),s(n(e,"die"),"die","dieConfig"),s(n(e,"stack"),"stack","lotStack"),s(n(e.dieConfig,"origin"),"dieConfig.origin","dieConfig.coordinateOrigin"),s(n(e.waferConfig,"flat"),"waferConfig.flat","waferConfig.notch"),s(n(e.reticleConfig,"anchor"),"reticleConfig.anchor","reticleConfig.anchorDie"),s(n(e.lotStack,"aggr"),"lotStack.aggr","lotStack.method"),s(Array.isArray(e.testDefs)&&e.testDefs.some(a=>n(a,"index")&&!n(a,"testNumber")),"TestDef.index","TestDef.testNumber");const r=n(e.lotStack,"results")?e.lotStack.results:[],h=(Array.isArray(t)?[t]:[e.results,...Array.isArray(r)?r:[]]).filter(a=>Array.isArray(a));if(s(h.some(a=>a.slice(0,100).some(i=>n(i,"values"))),"values","testValues"),!l.length)return;const o=l.length>1,g=`buildWaferMap ignored ${l.join(", ")}: ${o?"these names were":"that name was"} removed in an earlier release, so what ${o?"they":"it"} described is not on this map. Rename and rebuild.`;return console.warn(`[wafermap] ${g}`),{code:"input-field-removed",message:g,severity:"error"}}function Ae(t){if(t.results!==void 0||t.lotStack!==void 0)throw new Error("buildWaferMap: pass `layout`, `results` or `lotStack` \u2014 only one.");const n=t.waferConfig?.diameter,e=t.dieConfig?.width,l=t.dieConfig?.height;if(!(n>0&&e>0&&l>0))throw new Error("buildWaferMap: `layout` needs waferConfig.diameter and dieConfig.width and height, all positive.");const s=j({diameter:n,notch:t.waferConfig?.notch,orientation:0}),r=ge(pe(s,{width:e,height:l}),s,{width:e,height:l}).filter(i=>!i.partial),{flipX:h,flipY:o}=ne(t.dieConfig,{type:"center"}),{layout:g,...a}=t;return{...a,results:r.map(i=>({x:h?-i.x:i.x,y:o?-i.y:i.y})),waferConfig:{...t.waferConfig,center:{x:0,y:0}},dieConfig:{...t.dieConfig,coordinateOrigin:{type:"center"}}}}function Re(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],ringCount:4,ringCountWarning:void 0,removedFieldWarning:te(t),nonNumericWarning:ee(t),standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],...ke(t.ringCount),removedFieldWarning:te(t),nonNumericWarning:ee(t),standardDiameters:t.standardDiameters,testDefs:t.testDefs?.length?t.testDefs:void 0,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function $e(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function We(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let l=1/0,s=-1/0,r=1/0,h=-1/0;for(const o of t)o.x<l&&(l=o.x),o.x>s&&(s=o.x),o.y<r&&(r=o.y),o.y>h&&(h=o.y);return{offsetX:Math.round((s+l)/2),offsetY:Math.round((h+r)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Ee(t,n,e,l,s,r){if(r)return{colMidX:(Math.round(r.x)-n)*l,colMidY:(Math.round(r.y)-e)*s,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let h=1/0,o=-1/0,g=1/0,a=-1/0;for(const i of t){const f=Math.round(i.x)-n,u=Math.round(i.y)-e;f<h&&(h=f),f>o&&(o=f),u<g&&(g=u),u>a&&(a=u)}return{colMidX:(h+o)/2*l,colMidY:(g+a)/2*s,anchored:!1}}function Fe(t,n,e){let l=1/0,s=-1/0,r=1/0,h=-1/0,o=0,g=0;for(const f of t){const u=Math.round(f.x)-n,d=Math.round(f.y)-e;o+=u,g+=d,u<l&&(l=u),u>s&&(s=u),d<r&&(r=d),d>h&&(h=d)}const a=t.length,i=(f,u,d)=>{const p=(d-u)/2;return p<=0?0:Math.abs(f/a-(u+d)/2)/p};return i(o,l,s)>.11||i(g,r,h)>.11}function ne(t,n){let e=t?.xAxisDirection==="left",l=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(l=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:l}}function Pe(t,n){const{method:e,targetBin:l}=t,s=t.results.map(r=>r.filter(A));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const r=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),h=new Set;for(const i of s)for(const f of i)if(f.testValues)for(const u of Object.keys(f.testValues)){const d=Number(u);r.has(d)||h.add(d)}if(h.size===0)return Z(s,e);const o=[...h],g=o.map(i=>Z(s,e,i)),a=new Map;for(let i=0;i<o.length;i++){const f=o[i];for(const u of g[i]){const d=V(u);a.has(d)||a.set(d,{template:u,testValues:{}});const p=a.get(d),m=u.testValues?.[0];m!==void 0&&(p.testValues[f]=m)}}return[...a.values()].map(({template:i,testValues:f})=>({...i,testValues:Object.keys(f).length>0?f:void 0}))}if(e==="countBin"||e==="percent"){if(l===void 0)return[];const r=Se(s,l,"hard");if(e==="countBin")return r;const h=s.length;return r.map(o=>({...o,testValues:{0:h>0?(o.testValues?.[0]??0)/h*100:0}}))}if(e==="mode"){const r=new Map;for(const o of s)for(const g of o){const a=V(g);r.has(a)||r.set(a,[]),r.get(a).push(g)}const h=[];for(const[o,g]of r){const a=o.split(","),i=Number(a[0]),f=Number(a[1]),u=g.map(p=>p.hbin).filter(p=>p!==void 0),d=Oe(u);d!==null&&h.push({x:i,y:f,hbin:d})}return h}return[]}function se(t){const n=t.filter(A),e=n.length,l=n.filter(r=>r.edgeExcluded).length,s=n.filter(r=>dieHasTestData(r)||r.hbin!==void 0||r.sbin!==void 0).length;return{filledDies:s,totalDies:e,edgeExcludedDies:l,unpositionedDies:t.length-n.length,ratio:e>0?s/e:0}}function ie(t,n,e="exclude"){const l=new Set(n),r=t.filter(d=>!d.partial).filter(d=>d.edgeExcluded).length,h=t.filter(d=>d.partial).length;let o=0,g=0,a=!1;for(const d of t){if(!he(d))continue;const p=d.hbin??d.sbin;p!==void 0&&(a=!0,l.has(p)?o++:g++)}const i=o+g,f=a&&i>0?o/i*100:null;let u=null;if(e==="denominator-only"){const d=i+r;u=a&&d>0?o/d*100:null}return{passDies:o,failDies:g,edgeExcludedDies:r,partialDies:h,totalDies:i,yieldPercent:f,yieldPercentGross:u}}function oe(t,n,e,l,s,r=0,h=0,o=0,g=0,a=0,i=!1,f=!1){if(!t)return[];const u=t.anchorDie??{x:0,y:0},d=ve(n,{width:t.width,height:t.height,diePitchX:l,diePitchY:s,anchorDie:{x:u.x-r,y:u.y-h},gridOrigin:{x:-o,y:-g}}),p=we(be(i,f,n.center.x,n.center.y),ye(a,n.center.x,n.center.y));return d.filter(m=>{const D=m.width/2,C=m.height/2,y=[[m.x-D,m.y-C],[m.x+D,m.y-C],[m.x+D,m.y+C],[m.x-D,m.y+C]].map(([w,B])=>xe(p,w,B)),b=Q(y.map(w=>w.x)),R=K(y.map(w=>w.x)),$=Q(y.map(w=>w.y)),F=K(y.map(w=>w.y));return e.some(w=>w.physX>=b&&w.physX<R&&w.physY>=$&&w.physY<F)})}function Ie(t,n,e){const s=Math.max(0,n.radius-e)**2;return t.map(r=>{const h=r.physX-n.center.x,o=r.physY-n.center.y;return h*h+o*o>s?{...r,edgeExcluded:!0}:r})}function Ne(t,n,e){const l=t.filter(A),s=t.filter(i=>!A(i)),r=new Map;for(const i of l){let f=r.get(i.x);f||(f=new Map,r.set(i.x,f)),f.set(i.y,(f.get(i.y)??0)+1)}const h=new Set(e);function o(i,f){const u=i.hbin,d=f.hbin;if(u===void 0||d===void 0)return!1;const p=h.has(u),m=h.has(d);return n==="best"?m!==p?m:d<u:m!==p?p:d>u}const g=new Map;for(const i of l){const f=V(i),u=g.get(f);n==="first"&&u||(n==="best"||n==="worst")&&u&&!o(u,i)||g.set(f,i)}return[...Array.from(g.values()).map(i=>{const u=r.get(i.x)?.get(i.y)??1;return u>1?{...i,retestCount:u}:i}),...s]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const l=t.testPass?.[n];if(l!==void 0)return l;if(e!==void 0&&!isParametricTest(e)){const s=t.testValues?.[n];if(s===0||s===1)return s===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function _(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function re(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function ae(t,n){return t.map(({code:e,message:l})=>({code:e,message:l,severity:e==="non-standard-diameter"||e==="diameter-exceeds-die-extent"?"warning":"error",confidence:n.wafer.confidence}))}const Ve="do not fit inside the supplied",Te="Inferred wafer diameter",_e="The probed dies reach only",Be=.75,Ye=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const Le=.02;function de(t,n){return n.reduce((e,l)=>Math.abs(l-t)<Math.abs(e-t)?l:e)}function Xe(t,n){if(!n.length)return!0;const e=de(t,n);return Math.abs(e-t)/e<=Le}function je(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=Re(!Array.isArray(t)&&t.layout?Ae(t):t),{debug:l,...s}=n??{},r=e.lotStackOpts?Pe(e.lotStackOpts,e.testDefs):e.results;for(let c=0;c<Math.min(5,r.length);c++){const x=r[c];if(typeof x.x=="string"||typeof x.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const c of r)if(c.x==null!=(c.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(c.x)}, y=${String(c.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const h=Ne(r,e.retestPolicy,e.passBins),o={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}},g=[];if(e.explicitDies){let c=e.explicitDies;const x=h.filter(A),S=h.filter(N=>!A(N));if(x.length>0){const N=new Map(x.map(k=>[V(k),k]));c=c.map(k=>{const H=N.get(V(k));return H?_(k,H):k})}const I=e.waferOpts?.diameter??300,E=j({diameter:I,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});E.orientation!==0&&(c=U(c,E));const q=oe(e.reticleOpts,E,c,1,1,0,0,0,0,E.orientation),le=s.showReticle??e.reticleOpts!==void 0,L=J(E,c,{...s,reticles:q,showReticle:le,plotMode:re(h,s),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ue=S.map((N,k)=>_({id:`unpositioned_${k}`,width:c[0]?.width??1,height:c[0]?.height??1},N)),X=[...c,...ue];return{wafer:E,dies:X,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:o,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...ae(g,o),...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:se(X),passBins:e.passBins,ringCount:e.ringCount,yield:ie(X,e.passBins,e.edgeDieYieldMode),reticles:q,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const a=h.filter(A),i=h.filter(c=>!A(c)),f=a.map(c=>({x:c.x,y:c.y})),u=Me(f,e.dieOpts,e.waferOpts?.diameter);o.diePitch={confidence:u.confidence,units:u.units};const{pitchX:d,pitchY:p}=u,m=u.units,D=$e(a,e.dieOpts),C=Ce(f);o.grid={confidence:C.confidence};const{offsetX:y,offsetY:b}=We(f,D,C),{flipX:R,flipY:$}=ne(e.dieOpts,D),{colMidX:F,colMidY:w,anchored:B}=Ee(f,y,b,d,p,e.waferOpts?.center);let M=e.waferOpts?.diameter;const T=f.map(c=>({x:(Math.round(c.x)-y)*d-F,y:(Math.round(c.y)-b)*p-w})),W=K(T.map(({x:c,y:x})=>Math.hypot(Math.abs(c)+d/2,Math.abs(x)+p/2)));if(M===void 0)if(f.length>0)if(m==="mm"){const c=De(T,{minRadius:W});M=c.diameter,o.wafer={confidence:c.confidence,method:c.method};const x=je(e.standardDiameters);if(!Xe(M,x)){const S=de(M,x);g.push({code:"non-standard-diameter",message:`${Te} ${M.toFixed(1)} mm is not a standard wafer size (nearest is ${S} mm). The diameter was derived from the die extent, which assumes the probed grid reaches the wafer edge \u2014 a non-standard result usually means it does not, because the outer dies were never probed. Every die is then placed against a wafer that is too small, which moves dies between rings and changes ring and edge findings. Supply waferConfig.diameter.`})}}else M=W*2,o.wafer={confidence:u.confidence*.8,method:"extent"};else M=m==="mm"?300:30,o.wafer={confidence:0,method:"default"};if(!B&&f.length>0&&Fe(f,y,b)&&(o.wafer.method="inferred-partial",g.push({code:"partial-coverage",message:"Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly."})),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&W>0&&f.length>=Ye){const c=W/(M/2);c<Be&&g.push({code:"diameter-exceeds-die-extent",message:`${_e} ${(c*100).toFixed(0)}% of the ${M} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`})}if(e.waferOpts?.diameter!==void 0){const c=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(c&&W>M/2+1e-9){const x=T.filter(({x:S,y:I})=>Math.hypot(Math.abs(S)+d/2,Math.abs(I)+p/2)>M/2+1e-9).length;g.push({code:"geometry-conflict",message:`${x} of ${T.length} probed die positions ${Ve} ${M} mm wafer at the supplied die pitch of ${d} \xD7 ${p} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(W*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`})}}const O=j({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let v=a.map(c=>{const x=Math.round(c.x)-y,S=Math.round(c.y)-b,I={id:`${x}_${S}`,x,y:S,physX:x*d-F,physY:S*p-w,width:d,height:p,insideWafer:!0,partial:!1};return _(I,c)});(y!==0||b!==0)&&(v=v.map(c=>({...c,x:c.x+y,y:c.y+b,id:`${c.x+y}_${c.y+b}`}))),v=U(v,O),(R||$)&&(v=me(v,{flipX:R,flipY:$},O.center));const z=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(v=Ie(v,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&z.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${O.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const G=oe(e.reticleOpts,O,v,d,p,y,b,F,w,O.orientation,R,$),fe=s.showReticle??e.reticleOpts!==void 0,P=J(O,v,{...s,reticles:G,showReticle:fe,plotMode:re(h,s),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,dataAxisFlip:{x:R,y:$},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ce=i.map((c,x)=>_({id:`unpositioned_${x}`,width:d,height:p},c)),Y=[...v,...ce];return{wafer:O,dies:Y,view:P,reticleConfig:e.reticleOpts,units:m,inference:o,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.nonNumericWarning?[e.nonNumericWarning]:[],...ae(g,o),...z,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:P.plotMode,metadata:P.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:se(Y),passBins:e.passBins,ringCount:e.ringCount,yield:ie(Y,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:P.aggrMethod,lotSize:P.lotSize}}
1
+ import{createWafer as z}from"../core/wafer.js";import{isYieldEligibleDie as xe,getDieKey as B,hasPosition as E}from"../core/dies.js";import{applyOrientation as ee,transformDies as De,clipDiesToWafer as Me}from"../core/transforms.js";import{generateDies as ve}from"../core/dies.js";import{affineRotation as Ce,affineMirror as Oe,affineCompose as Se,affinePoint as $e}from"../core/transforms.js";import{inferWaferFromXY as ke}from"../core/inference/wafer.js";import{resolveGridPitch as Ae}from"../core/inference/pitch.js";import{assignGridIndices as Ne}from"../core/inference/grid.js";import{generateReticleGrid as Re}from"../core/reticle.js";import{applyDerivedTests as te}from"./derivedTests/apply.js";import{buildView as ne}from"./buildView.js";import{maxOf as G,minOf as se,modeOf as We}from"../core/utils.js";import{aggregateValues as ie,aggregateBinCounts as Ee}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function Ie(t){const n=(f,i)=>({ringCount:f,ringCountWarning:{code:"analysis-option-corrected",severity:"warning",message:`ringCount=${String(t)} ${i} (using ${f}).`}});if(t===void 0)return{ringCount:4,ringCountWarning:void 0};if(typeof t!="number"||!Number.isFinite(t))return n(4,"is not a finite number");const e=Math.max(1,Math.round(t));return e===t?{ringCount:t,ringCountWarning:void 0}:n(e,"is not a whole number of at least 1")}function re(t){const n=Array.isArray(t)?{}:t,e=Array.isArray(n.lotStack?.results)?n.lotStack.results:[],f=(Array.isArray(t)?[t]:[n.results,...e]).filter(h=>Array.isArray(h)),i={hbin:0,sbin:0,testValues:0,testPass:0};let o;const u=(h,S)=>{i[h]++,o??=`${h} ${JSON.stringify(S)}`},s={bins:0,positions:0,testNumbers:0,values:0,sites:0,orientation:0};let m=0;const c=h=>Number.isInteger(h)&&h>=0&&h<=oe,a=h=>{typeof h=="number"&&(Number.isNaN(h)?m++:c(h)||s.bins++)},g=h=>{typeof h=="number"&&!(Number.isInteger(h)&&Math.abs(h)<=H)&&s.positions++};for(const h of f){const S=new Set,w=new Set;for(let v=0;v<h.length;v+=v<50?1:100){const r=h[v];if(!(r===null||typeof r!="object")){if(r.testValues&&typeof r.testValues=="object")for(const $ of Object.keys(r.testValues))S.add($);if(r.testPass&&typeof r.testPass=="object")for(const $ of Object.keys(r.testPass))w.add($)}}const p=[...S],D=[...w];for(const v of new Set([...p,...D])){const r=Number(v);Number.isInteger(r)&&r>=0&&r<=ae||s.testNumbers++}for(const v of h){if(v===null||typeof v!="object")continue;const r=v;r.hbin!=null&&typeof r.hbin!="number"&&u("hbin",r.hbin),r.sbin!=null&&typeof r.sbin!="number"&&u("sbin",r.sbin),a(r.hbin),a(r.sbin),g(r.x),g(r.y),typeof r.siteNum=="number"&&!(Number.isInteger(r.siteNum)&&r.siteNum>=0&&r.siteNum<=255)&&s.sites++;const $=r.testValues;if($&&typeof $=="object")for(let N=0;N<p.length;N++){const O=$[p[N]];O!=null&&(typeof O!="number"?u("testValues",O):Number.isFinite(O)||s.values++)}const I=r.testPass;if(I&&typeof I=="object")for(let N=0;N<D.length;N++){const O=I[D[N]];O!=null&&typeof O!="boolean"&&u("testPass",O)}}}const y=n.waferConfig?.orientation;typeof y=="number"&&![0,90,180,270].includes((y%360+360)%360)&&s.orientation++;const l=[],b=(h,S,w=`${S}s`)=>`${h} ${h===1?S:w}`,x=[];if(i.hbin&&x.push(b(i.hbin,"hard bin")),i.sbin&&x.push(b(i.sbin,"soft bin")),i.testValues&&x.push(b(i.testValues,"test value")),i.testPass&&x.push(b(i.testPass,"pass/fail verdict")),x.length){const h=`buildWaferMap received ${x.join(", ")} of the wrong type (for example ${o}). Bins and test values must be numbers and verdicts true/false \u2014 a bin of "1" is not pass bin 1, so those dies count as fails and yield is wrong, and a test value given as text is not plotted or analysed correctly. Convert them with Number() (and verdicts to booleans) and rebuild.`;console.warn(`[wafermap] ${h}`),l.push({code:"input-values-not-numbers",message:h,severity:"error"})}const M=[];if(s.bins&&M.push(`${b(s.bins,"bin")} (legal: whole numbers 0\u2013${oe})`),s.positions&&M.push(`${b(s.positions,"coordinate")} (legal: whole numbers \u2212${H} to ${H})`),s.testNumbers&&M.push(`${b(s.testNumbers,"test number")} (legal: whole numbers 0\u2013${ae})`),s.values&&M.push(`${b(s.values,"test value")} that ${s.values===1?"is":"are"} not finite`),s.sites&&M.push(`${b(s.sites,"site number")} (legal: 0\u2013255)`),s.orientation&&M.push("a waferConfig.orientation other than 0, 90, 180 or 270"),M.length||m){const h=[];M.length&&h.push(`buildWaferMap received values outside the STDF V4 ranges: ${M.join("; ")}. They are used as given for now; a future release treats them as missing.`),m&&h.push(`${b(m,"bin was","bins were")} NaN and ${m===1?"is":"are"} treated as no bin.`);const S=h.join(" ");console.warn(`[wafermap] ${S}`),l.push({code:"input-values-outside-stdf",message:S,severity:"warning"})}return l}const oe=32767,H=32767,ae=4294967295;function de(t){const n=(c,a)=>c!==null&&typeof c=="object"&&c[a]!==void 0,e=Array.isArray(t)?{}:t,f=[],i=(c,a,g)=>{c&&f.push(`\`${a}\` (now \`${g}\`)`)};i(n(e,"data"),"data","results"),i(n(e,"die"),"die","dieConfig"),i(n(e,"stack"),"stack","lotStack"),i(n(e.dieConfig,"origin"),"dieConfig.origin","dieConfig.coordinateOrigin"),i(n(e.waferConfig,"flat"),"waferConfig.flat","waferConfig.notch"),i(n(e.reticleConfig,"anchor"),"reticleConfig.anchor","reticleConfig.anchorDie"),i(n(e.lotStack,"aggr"),"lotStack.aggr","lotStack.method"),i(Array.isArray(e.testDefs)&&e.testDefs.some(c=>n(c,"index")&&!n(c,"testNumber")),"TestDef.index","TestDef.testNumber");const o=n(e.lotStack,"results")?e.lotStack.results:[],u=(Array.isArray(t)?[t]:[e.results,...Array.isArray(o)?o:[]]).filter(c=>Array.isArray(c));if(i(u.some(c=>c.slice(0,100).some(a=>n(a,"values"))),"values","testValues"),!f.length)return;const s=f.length>1,m=`buildWaferMap ignored ${f.join(", ")}: ${s?"these names were":"that name was"} removed in an earlier release, so what ${s?"they":"it"} described is not on this map. Rename and rebuild.`;return console.warn(`[wafermap] ${m}`),{code:"input-field-removed",message:m,severity:"error"}}function Te(t){if(t.results!==void 0||t.lotStack!==void 0)throw new Error("buildWaferMap: pass `layout`, `results` or `lotStack` \u2014 only one.");const n=t.waferConfig?.diameter,e=t.dieConfig?.width,f=t.dieConfig?.height;if(!(n>0&&e>0&&f>0))throw new Error("buildWaferMap: `layout` needs waferConfig.diameter and dieConfig.width and height, all positive.");const i=z({diameter:n,notch:t.waferConfig?.notch,orientation:0}),o=Me(ve(i,{width:e,height:f}),i,{width:e,height:f}).filter(a=>!a.partial),{flipX:u,flipY:s}=fe(t.dieConfig,{type:"center"}),{layout:m,...c}=t;return{...c,results:o.map(a=>({x:u?-a.x:a.x,y:s?-a.y:a.y})),waferConfig:{...t.waferConfig,center:{x:0,y:0}},dieConfig:{...t.dieConfig,coordinateOrigin:{type:"center"}}}}function Fe(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],ringCount:4,ringCountWarning:void 0,removedFieldWarning:de(t),inputValueWarnings:re(t),standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,derivedTests:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],...Ie(t.ringCount),removedFieldWarning:de(t),inputValueWarnings:re(t),standardDiameters:t.standardDiameters,testDefs:t.testDefs?.length?t.testDefs:void 0,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,derivedTests:t.derivedTests?.length?t.derivedTests:void 0,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function _e(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Pe(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let f=1/0,i=-1/0,o=1/0,u=-1/0;for(const s of t)s.x<f&&(f=s.x),s.x>i&&(i=s.x),s.y<o&&(o=s.y),s.y>u&&(u=s.y);return{offsetX:Math.round((i+f)/2),offsetY:Math.round((u+o)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Ve(t,n,e,f,i,o){if(o)return{colMidX:(Math.round(o.x)-n)*f,colMidY:(Math.round(o.y)-e)*i,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let u=1/0,s=-1/0,m=1/0,c=-1/0;for(const a of t){const g=Math.round(a.x)-n,y=Math.round(a.y)-e;g<u&&(u=g),g>s&&(s=g),y<m&&(m=y),y>c&&(c=y)}return{colMidX:(u+s)/2*f,colMidY:(m+c)/2*i,anchored:!1}}function Be(t,n,e){let f=1/0,i=-1/0,o=1/0,u=-1/0,s=0,m=0;for(const g of t){const y=Math.round(g.x)-n,l=Math.round(g.y)-e;s+=y,m+=l,y<f&&(f=y),y>i&&(i=y),l<o&&(o=l),l>u&&(u=l)}const c=t.length,a=(g,y,l)=>{const b=(l-y)/2;return b<=0?0:Math.abs(g/c-(y+l)/2)/b};return a(s,f,i)>.11||a(m,o,u)>.11}function fe(t,n){let e=t?.xAxisDirection==="left",f=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(f=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:f}}function Ye(t,n){const{method:e,targetBin:f}=t,i=t.results.map(o=>o.filter(E));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const o=new Set((n??[]).filter(a=>!isParametricTest(a)).map(a=>a.testNumber)),u=new Set;for(const a of i)for(const g of a)if(g.testValues)for(const y of Object.keys(g.testValues)){const l=Number(y);o.has(l)||u.add(l)}if(u.size===0)return ie(i,e);const s=[...u],m=s.map(a=>ie(i,e,a)),c=new Map;for(let a=0;a<s.length;a++){const g=s[a];for(const y of m[a]){const l=B(y);c.has(l)||c.set(l,{template:y,testValues:{}});const b=c.get(l),x=y.testValues?.[0];x!==void 0&&(b.testValues[g]=x)}}return[...c.values()].map(({template:a,testValues:g})=>({...a,testValues:Object.keys(g).length>0?g:void 0}))}if(e==="countBin"||e==="percent"){if(f===void 0)return[];const o=Ee(i,f,"hard");if(e==="countBin")return o;const u=i.length;return o.map(s=>({...s,testValues:{0:u>0?(s.testValues?.[0]??0)/u*100:0}}))}if(e==="mode"){const o=new Map;for(const s of i)for(const m of s){const c=B(m);o.has(c)||o.set(c,[]),o.get(c).push(m)}const u=[];for(const[s,m]of o){const c=s.split(","),a=Number(c[0]),g=Number(c[1]),y=m.map(b=>b.hbin).filter(b=>b!==void 0),l=We(y);l!==null&&u.push({x:a,y:g,hbin:l})}return u}return[]}function ce(t){const n=t.filter(E),e=n.length,f=n.filter(o=>o.edgeExcluded).length,i=n.filter(o=>dieHasTestData(o)||o.hbin!==void 0||o.sbin!==void 0).length;return{filledDies:i,totalDies:e,edgeExcludedDies:f,unpositionedDies:t.length-n.length,ratio:e>0?i/e:0}}function le(t,n,e="exclude"){const f=new Set(n),o=t.filter(l=>!l.partial).filter(l=>l.edgeExcluded).length,u=t.filter(l=>l.partial).length;let s=0,m=0,c=!1;for(const l of t){if(!xe(l))continue;const b=l.hbin??l.sbin;b!==void 0&&(c=!0,f.has(b)?s++:m++)}const a=s+m,g=c&&a>0?s/a*100:null;let y=null;if(e==="denominator-only"){const l=a+o;y=c&&l>0?s/l*100:null}return{passDies:s,failDies:m,edgeExcludedDies:o,partialDies:u,totalDies:a,yieldPercent:g,yieldPercentGross:y}}function ue(t,n,e,f,i,o=0,u=0,s=0,m=0,c=0,a=!1,g=!1){if(!t)return[];const y=t.anchorDie??{x:0,y:0},l=Re(n,{width:t.width,height:t.height,diePitchX:f,diePitchY:i,anchorDie:{x:y.x-o,y:y.y-u},gridOrigin:{x:-s,y:-m}}),b=Se(Oe(a,g,n.center.x,n.center.y),Ce(c,n.center.x,n.center.y));return l.filter(x=>{const M=x.width/2,h=x.height/2,S=[[x.x-M,x.y-h],[x.x+M,x.y-h],[x.x+M,x.y+h],[x.x-M,x.y+h]].map(([r,$])=>$e(b,r,$)),w=se(S.map(r=>r.x)),p=G(S.map(r=>r.x)),D=se(S.map(r=>r.y)),v=G(S.map(r=>r.y));return e.some(r=>r.physX>=w&&r.physX<p&&r.physY>=D&&r.physY<v)})}function Le(t,n,e){const i=Math.max(0,n.radius-e)**2;return t.map(o=>{const u=o.physX-n.center.x,s=o.physY-n.center.y;return u*u+s*s>i?{...o,edgeExcluded:!0}:o})}const Xe=.2;function je(t,n,e){const f=new Set(e);function i(w,p){if(n==="last")return!0;if(n==="first")return!1;const D=L(w.hbin),v=L(p.hbin);if(D===void 0||v===void 0)return!1;const r=f.has(D),$=f.has(v);return n==="best"?$!==r?$:v<D:$!==r?r:v>D}const o=(w,p)=>p.supersedes!==void 0||i(w,p),u=w=>{const p=w.partId===void 0?"":String(w.partId).trim();return p===""?void 0:p},s=t.filter(w=>!E(w)),m=new Map;for(const w of s){const p=u(w);p!==void 0&&m.set(p,(m.get(p)??0)+1)}const a=(s.length===0?0:Math.max(0,...m.values())/s.length)<=Xe,g=new Map,y=[],l=new Map;let b=0,x=0;for(const w of t){const p=u(w);let D;if(E(w)?D=`p:${B(w)}`:a&&p!==void 0?D=`i:${p}`:D=`u:${x++}`,w.supersedes==="partId"&&p!==void 0){const r=l.get(p);r!==void 0&&r!==D&&g.has(r)&&g.delete(r)}const v=g.get(D);v===void 0?(g.set(D,{winner:w,count:1}),y.push(D)):(v.count++,D.startsWith("i:")&&b++,o(v.winner,w)&&(v.winner=w)),p!==void 0&&l.set(p,D)}const M=[],h=[...y.filter(w=>w.startsWith("p:")),...y.filter(w=>!w.startsWith("p:"))];for(const w of h){const p=g.get(w);p!==void 0&&M.push(p.count>1?{...p.winner,retestCount:p.count}:p.winner)}const S=b>0?{code:"retests-by-part-id",severity:"info",message:`${b} record${b===1?"":"s"} with no position shared a part ID with an earlier record and ${b===1?"was":"were"} treated as a retest of it.`}:void 0;return{results:M,warning:S}}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const f=t.testPass?.[n];if(f!==void 0)return f;if(e!==void 0&&!isParametricTest(e)){const i=t.testValues?.[n];if(i===0||i===1)return i===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function L(t){return t===void 0||Number.isNaN(t)?void 0:t}function X(t,n){const e={},f=L(n.hbin),i=L(n.sbin);return f!==void 0&&(e.hbin=f),i!==void 0&&(e.sbin=i),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 he(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function pe(t,n){return t.map(({code:e,message:f})=>({code:e,message:f,severity:e==="non-standard-diameter"||e==="diameter-exceeds-die-extent"?"warning":"error",confidence:n.wafer.confidence}))}const Ke="do not fit inside the supplied",Ue="Inferred wafer diameter",ze="The probed dies reach only",Ge=.75,He=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const qe=.02;function me(t,n){return n.reduce((e,f)=>Math.abs(f-t)<Math.abs(e-t)?f:e)}function Je(t,n){if(!n.length)return!0;const e=me(t,n);return Math.abs(e-t)/e<=qe}function Qe(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=Fe(!Array.isArray(t)&&t.layout?Te(t):t),{debug:f,...i}=n??{},o=[];if(e.derivedTests){const d=new Set(e.passBins);if(e.lotStackOpts){const C=e.lotStackOpts.results.map(k=>te(k,e.testDefs,e.derivedTests,d));e.lotStackOpts={...e.lotStackOpts,results:C.map(k=>k.results)},e.testDefs=C[0]?.testDefs??e.testDefs,o.push(...C[0]?.warnings??[])}else{const C=te(e.results,e.testDefs,e.derivedTests,d);e.results=C.results,e.testDefs=C.testDefs,o.push(...C.warnings)}}const u=e.lotStackOpts?Ye(e.lotStackOpts,e.testDefs):e.results;for(let d=0;d<Math.min(5,u.length);d++){const C=u[d];if(typeof C.x=="string"||typeof C.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const d of u)if(d.x==null!=(d.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(d.x)}, y=${String(d.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const{results:s,warning:m}=je(u,e.retestPolicy,e.passBins),c={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}},a=[];if(e.explicitDies){let d=e.explicitDies;const C=s.filter(E),k=s.filter(V=>!E(V));if(C.length>0){const V=new Map(C.map(W=>[B(W),W]));d=d.map(W=>{const Z=V.get(B(W));return Z?X(W,Z):W})}const P=e.waferOpts?.diameter??300,F=z({diameter:P,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});F.orientation!==0&&(d=ee(d,F));const Q=ue(e.reticleOpts,F,d,1,1,0,0,0,0,F.orientation),be=i.showReticle??e.reticleOpts!==void 0,K=ne(F,d,{...i,reticles:Q,showReticle:be,plotMode:he(s,i),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),we=k.map((V,W)=>X({id:`unpositioned_${W}`,width:d[0]?.width??1,height:d[0]?.height??1},V)),U=[...d,...we];return{wafer:F,dies:U,view:K,reticleConfig:e.reticleOpts,units:"mm",inference:c,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.inputValueWarnings,...m?[m]:[],...pe(a,c),...o,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:K.plotMode,metadata:K.metadata,isLotStack:!1,dataCoverage:ce(U),passBins:e.passBins,ringCount:e.ringCount,yield:le(U,e.passBins,e.edgeDieYieldMode),reticles:Q,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const g=s.filter(E),y=s.filter(d=>!E(d)),l=g.map(d=>({x:d.x,y:d.y})),b=Ae(l,e.dieOpts,e.waferOpts?.diameter);c.diePitch={confidence:b.confidence,units:b.units};const{pitchX:x,pitchY:M}=b,h=b.units,S=_e(g,e.dieOpts),w=Ne(l);c.grid={confidence:w.confidence};const{offsetX:p,offsetY:D}=Pe(l,S,w),{flipX:v,flipY:r}=fe(e.dieOpts,S),{colMidX:$,colMidY:I,anchored:N}=Ve(l,p,D,x,M,e.waferOpts?.center);let O=e.waferOpts?.diameter;const Y=l.map(d=>({x:(Math.round(d.x)-p)*x-$,y:(Math.round(d.y)-D)*M-I})),T=G(Y.map(({x:d,y:C})=>Math.hypot(Math.abs(d)+x/2,Math.abs(C)+M/2)));if(O===void 0)if(l.length>0)if(h==="mm"){const d=ke(Y,{minRadius:T});O=d.diameter,c.wafer={confidence:d.confidence,method:d.method};const C=Qe(e.standardDiameters);if(!Je(O,C)){const k=me(O,C);a.push({code:"non-standard-diameter",message:`${Ue} ${O.toFixed(1)} mm is not a standard wafer size (nearest is ${k} 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 O=T*2,c.wafer={confidence:b.confidence*.8,method:"extent"};else O=h==="mm"?300:30,c.wafer={confidence:0,method:"default"};if(!N&&l.length>0&&Be(l,p,D)&&(c.wafer.method="inferred-partial",a.push({code:"partial-coverage",message:"Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly."})),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&T>0&&l.length>=He){const d=T/(O/2);d<Ge&&a.push({code:"diameter-exceeds-die-extent",message:`${ze} ${(d*100).toFixed(0)}% of the ${O} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`})}if(e.waferOpts?.diameter!==void 0){const d=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(d&&T>O/2+1e-9){const C=Y.filter(({x:k,y:P})=>Math.hypot(Math.abs(k)+x/2,Math.abs(P)+M/2)>O/2+1e-9).length;a.push({code:"geometry-conflict",message:`${C} of ${Y.length} probed die positions ${Ke} ${O} mm wafer at the supplied die pitch of ${x} \xD7 ${M} 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 ${(T*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`})}}const R=z({diameter:O,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let A=g.map(d=>{const C=Math.round(d.x)-p,k=Math.round(d.y)-D,P={id:`${C}_${k}`,x:C,y:k,physX:C*x-$,physY:k*M-I,width:x,height:M,insideWafer:!0,partial:!1};return X(P,d)});(p!==0||D!==0)&&(A=A.map(d=>({...d,x:d.x+p,y:d.y+D,id:`${d.x+p}_${d.y+D}`}))),A=ee(A,R),(v||r)&&(A=De(A,{flipX:v,flipY:r},R.center));const q=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(A=Le(A,R,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>R.radius&&q.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${R.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const J=ue(e.reticleOpts,R,A,x,M,p,D,$,I,R.orientation,v,r),ge=i.showReticle??e.reticleOpts!==void 0,_=ne(R,A,{...i,reticles:J,showReticle:ge,plotMode:he(s,i),testDefs:e.testDefs,passBins:e.passBins,ringCount:e.ringCount,dataAxisFlip:{x:v,y:r},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ye=y.map((d,C)=>X({id:`unpositioned_${C}`,width:x,height:M},d)),j=[...A,...ye];return{wafer:R,dies:j,view:_,reticleConfig:e.reticleOpts,units:h,inference:c,warnings:[...e.removedFieldWarning?[e.removedFieldWarning]:[],...e.inputValueWarnings,...m?[m]:[],...pe(a,c),...q,...o,...e.ringCountWarning?[e.ringCountWarning]:[]],plotMode:_.plotMode,metadata:_.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:ce(j),passBins:e.passBins,ringCount:e.ringCount,yield:le(j,e.passBins,e.edgeDieYieldMode),reticles:J,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:_.aggrMethod,lotSize:_.lotSize}}
@@ -1,9 +1,17 @@
1
1
  /**
2
- * The release that deletes every export deprecated through this helper. Named in
3
- * each notice and each `@deprecated` tag, and held to by tests/deprecations.test.mjs,
4
- * which fails once the changelog or package.json reaches it with them still here.
2
+ * The release that deletes an export deprecated through this helper, unless its
3
+ * deprecation names a later one. Named in each notice and each `@deprecated` tag,
4
+ * and held to by tests/deprecations.test.mjs, which fails once the changelog or
5
+ * package.json reaches an export's removal release with it still here.
5
6
  */
6
7
  export declare const DEPRECATED_REMOVAL_VERSION = "0.31.0";
8
+ /**
9
+ * @internal The release that removes each deprecated name. A name deprecated
10
+ * after the default removal release was scheduled gets a later one — a removal
11
+ * must follow a release in which the name shipped deprecated, never coincide
12
+ * with it.
13
+ */
14
+ export declare const DEPRECATED_REMOVALS: Map<string, string>;
7
15
  /**
8
16
  * @internal Every name deprecated through this module, and whether it is a function
9
17
  * (which logs a notice) or a value (which cannot). tests/deprecations.test.mjs pins
@@ -22,10 +30,10 @@ export declare function noticeOnce(key: string, message: string): void;
22
30
  * The wrapper has exactly `fn`'s type — generics and overloads included — so a
23
31
  * deprecated export's signature does not change until it is removed.
24
32
  */
25
- export declare function deprecated<F extends (...args: never[]) => unknown>(fn: F, name: string, advice: string): F;
33
+ export declare function deprecated<F extends (...args: never[]) => unknown>(fn: F, name: string, advice: string, removal?: string): F;
26
34
  /**
27
35
  * @internal Register a deprecated constant. A value cannot log on use, so its
28
36
  * `@deprecated` tag and the changelog are the only announcement.
29
37
  */
30
- export declare function deprecatedValue<T>(value: T, name: string): T;
38
+ export declare function deprecatedValue<T>(value: T, name: string, removal?: string): T;
31
39
  //# sourceMappingURL=deprecate.d.ts.map
@@ -1 +1 @@
1
- export const DEPRECATED_REMOVAL_VERSION="0.31.0",DEPRECATED_EXPORTS=new Map;const n=new Set;export function noticeOnce(t,e){n.has(t)||(n.add(t),console.warn(`[wafermap] ${e}`))}export function deprecated(t,e,r){DEPRECATED_EXPORTS.set(e,"function");const o=t;return((...c)=>(noticeOnce(e,`${e} is deprecated and will be removed in ${DEPRECATED_REMOVAL_VERSION}. ${r}`),o(...c)))}export function deprecatedValue(t,e){return DEPRECATED_EXPORTS.set(e,"value"),t}
1
+ export const DEPRECATED_REMOVAL_VERSION="0.31.0",DEPRECATED_REMOVALS=new Map,DEPRECATED_EXPORTS=new Map;const r=new Set;export function noticeOnce(t,e){r.has(t)||(r.add(t),console.warn(`[wafermap] ${e}`))}export function deprecated(t,e,n,o=DEPRECATED_REMOVAL_VERSION){DEPRECATED_EXPORTS.set(e,"function"),DEPRECATED_REMOVALS.set(e,o);const c=t;return((...E)=>(noticeOnce(e,`${e} is deprecated and will be removed in ${o}. ${n}`),c(...E)))}export function deprecatedValue(t,e,n=DEPRECATED_REMOVAL_VERSION){return DEPRECATED_EXPORTS.set(e,"value"),DEPRECATED_REMOVALS.set(e,n),t}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Build-time application of derived tests.
3
+ *
4
+ * Runs over raw `DieResult` records **before** `collapseLotStack` and before
5
+ * retest resolution, and that ordering is load-bearing:
6
+ *
7
+ * - **Before stacking.** `abs(t[1020] - t[1010])` computed from the two raw
8
+ * values on one probe record is a real measurement of one die. The same
9
+ * expression computed from two independently aggregated stacks is a difference
10
+ * of two averages over possibly different die populations — a number with no
11
+ * physical meaning that would nonetheless plot happily.
12
+ * - **Before retest collapse.** Every accessor then reads the same probe record,
13
+ * so a derived value can never mix a value from one touchdown with a verdict
14
+ * from another. `retestPolicy` afterwards selects among records that already
15
+ * carry their own derived value.
16
+ *
17
+ * A derived test is an ordinary test from here on: it lands in `testValues` (or
18
+ * `testPass`, if its expression is boolean) and its def joins `testDefs`, so
19
+ * every downstream surface — value plot modes, the colorbar, tooltips,
20
+ * `analyzeWaferMap`, Insights, the report, gallery reconciliation — picks it up
21
+ * with no further change.
22
+ */
23
+ import type { DieResult, TestDef, WaferWarning } from '../buildWaferMap.js';
24
+ /**
25
+ * A test whose values are computed from other tests on the same die, rather than
26
+ * measured by the tester.
27
+ *
28
+ * It is a `TestDef` with an expression, not a parallel concept: `unit`,
29
+ * `limitLow`/`limitHigh` and `logScale` already mean exactly the right thing for
30
+ * a derived quantity, and `result.testDefs` stays one homogeneous array.
31
+ *
32
+ * ```ts
33
+ * { testNumber: 900001, name: 'Leakage Shift', unit: 'uA',
34
+ * expression: 'abs(t[1020] - t[1010])', limitHigh: 5 }
35
+ *
36
+ * { testNumber: 900002, name: 'Sweep All Pass', testType: 'F',
37
+ * expression: 'all(testPass[1010..1025])' }
38
+ * ```
39
+ */
40
+ export interface DerivedTestDef extends TestDef {
41
+ /**
42
+ * Expression over other tests on the same die.
43
+ *
44
+ * Reads: `t[n]` (measured value, parametric tests only), `testPass[n]` (the
45
+ * tester's recorded verdict), `specPass[n]` (spec-limit judgement, needs
46
+ * limits), `diePass()` (the die's bin verdict). A range — `t[1010..1015]` —
47
+ * produces a set that must be reduced: `mean`, `sum`, `min`, `max`, `all`,
48
+ * `any`, `none`, `countTrue`, `countFalse`, `countKnown`.
49
+ *
50
+ * Operators `+ - * / % ^`, comparisons, `and`/`or`/`not`, and
51
+ * `if(cond, a, b)`. Functions `abs sqrt ln log10 exp floor ceil round sign
52
+ * pow min max`.
53
+ *
54
+ * Evaluated with no JavaScript engine of any kind: it is parsed to a typed
55
+ * tree at build time and walked per die. There is no `eval`, no `Function`,
56
+ * no member access and no way to name a host object.
57
+ */
58
+ expression: string;
59
+ /** Named constants usable in `expression`. The only resolvable identifiers
60
+ * besides function names. */
61
+ constants?: Record<string, number>;
62
+ /**
63
+ * Marks this def as derived. Set by `buildWaferMap`, never by the caller.
64
+ *
65
+ * It is a display flag: every surface that names the test reads it, through
66
+ * `renderer/testLabel.ts`, to show that the value was not measured. It does
67
+ * NOT exclude derived tests from anything — a derived test is an ordinary test
68
+ * from the build onwards, and appears in `functionalYield`, the per-test
69
+ * statistics, capability, findings and Insights exactly like a measured one.
70
+ */
71
+ readonly derived?: true;
72
+ }
73
+ export interface ApplyResult {
74
+ results: DieResult[];
75
+ testDefs: TestDef[] | undefined;
76
+ warnings: WaferWarning[];
77
+ }
78
+ /**
79
+ * Apply `derivedTests` to `results`, returning new records that carry the
80
+ * derived values and a `testDefs` array extended with the derived defs.
81
+ *
82
+ * Input records are not mutated — the caller's array is its own.
83
+ */
84
+ export declare function applyDerivedTests(results: DieResult[], testDefs: TestDef[] | undefined, derivedTests: DerivedTestDef[] | undefined, passBins: ReadonlySet<number>): ApplyResult;
85
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1 @@
1
+ import{diePassStatus as P}from"../../core/dies.js";import{getTestPassStatus as D,isParametricTest as S}from"../buildWaferMap.js";import{classifySpec as V}from"../spec.js";import{parseExpression as k}from"./parser.js";import{evaluate as O}from"./evaluate.js";function p(m){return{code:"derived-test-invalid",message:m,severity:"warning"}}function M(m,l){const w=[],o=n=>{switch(n.k){case"acc":{if(n.kind!=="t")return;const i=l.get(n.test)?.unit;return i?new Set([i]):void 0}case"unary":return n.op==="-"?o(n.arg):void 0;case"binary":{const i=o(n.left),a=o(n.right);if(n.op==="+"||n.op==="-"){if(i&&a){const d=new Set([...i,...a]);return d.size>1&&w.push(`${[...d].join(" and ")} are combined by "${n.op}"`),d}return i??a}return}case"call":{if(n.name==="min"||n.name==="max"||n.name==="mean"||n.name==="sum"){const i=n.args[0];if(i?.k==="vec"&&i.kind==="t"){const a=new Set(i.tests.map(d=>l.get(d)?.unit).filter(d=>!!d));return a.size>1&&w.push(`${[...a].join(" and ")} are combined by "${n.name}"`),a}}return}default:return}};return o(m),w}function T(m,l,w){const o=[],n=new Map((l??[]).map(e=>[e.testNumber,e])),i=e=>`${JSON.stringify(e.name??"")} (test ${e.testNumber})`,a=[],d=new Set;for(const e of m){const t=i(e);if(!Number.isInteger(e.testNumber)){o.push(p(`Derived test ${t} was dropped: \`testNumber\` must be an integer.`));continue}if(w.has(e.testNumber)||n.has(e.testNumber)){o.push(p(`Derived test ${t} was dropped: test number ${e.testNumber} already exists in the data. Measured values are never replaced by a derived test \u2014 give it an unused number.`));continue}if(d.has(e.testNumber)){o.push(p(`Derived test ${t} was dropped: test number ${e.testNumber} is used by an earlier derived test.`));continue}if(typeof e.expression!="string"||e.expression.trim()===""){o.push(p(`Derived test ${t} was dropped: \`expression\` is empty.`));continue}d.add(e.testNumber),a.push(e)}const y=new Map(n);for(const e of a){const{expression:t,constants:u,...c}=e;y.set(e.testNumber,{...c,testType:e.testType??"P"})}const s=new Map,r=new Set;for(const e of a){const t=i(e),u={testDefs:y,constants:e.constants??{}},c=k(e.expression,u);if(!c.ok){o.push(p(`Derived test ${t} was dropped: ${c.message}`)),r.add(e.testNumber);continue}const f=c.type==="boolean",g=!S(e);if(f!==g){o.push(p(f?`Derived test ${t} was dropped: its expression produces a true/false verdict, so it must declare \`testType: 'F'\`.`:`Derived test ${t} was dropped: it declares \`testType: 'F'\` but its expression produces a number. Functional tests carry a verdict, not a measurement.`)),r.add(e.testNumber);continue}if(f&&(e.limitLow!==void 0||e.limitHigh!==void 0)&&o.push(p(`Derived test ${t}: test limits are ignored on a true/false derived test.`)),e.testNumber===void 0)continue;const N=e.testNumber,x=[...new Set(c.reads.map(h=>h.test))].filter(h=>h!==N&&d.has(h));if(c.reads.some(h=>h.test===N)){o.push(p(`Derived test ${t} was dropped: its expression reads itself.`)),r.add(N);continue}for(const h of M(c.ast,y))o.push(p(`Derived test ${t} combines different units \u2014 ${h}. The result is computed as written; set \`unit\` on the derived test to say what it is.`));s.set(N,{raw:e,compiled:{def:{...e,testType:f?"F":"P",derived:!0},ast:c.ast,boolean:f},deps:x})}const b=[],v=new Map,$=(e,t)=>{if(v.get(e)==="done")return!r.has(e);if(v.get(e)==="visiting"){const f=[...t.slice(t.indexOf(e)),e].join(" \u2192 ");for(const g of t.slice(t.indexOf(e)))r.has(g)||(r.add(g),o.push(p(`Derived test ${i(s.get(g).raw)} was dropped: its expression depends on itself through ${f}.`)));return!1}const u=s.get(e);if(u===void 0)return!1;v.set(e,"visiting");let c=!0;for(const f of u.deps)if(!$(f,[...t,e])){if(!r.has(e)){r.add(e);const g=s.get(f)?.raw.name??`test ${f}`;o.push(p(`Derived test ${i(u.raw)} was dropped: it reads ${JSON.stringify(g)} (test ${f}), which was itself dropped.`))}c=!1;break}return v.set(e,"done"),!c||r.has(e)?!1:(b.push(u.compiled),!0)};for(const e of a)$(e.testNumber,[]);return{compiled:b,warnings:o}}export function applyDerivedTests(m,l,w,o){if(w===void 0||w.length===0)return{results:m,testDefs:l,warnings:[]};const n=new Set;for(const s of m){for(const r of Object.keys(s.testValues??{}))n.add(Number(r));for(const r of Object.keys(s.testPass??{}))n.add(Number(r))}const{compiled:i,warnings:a}=T(w,l,n);if(i.length===0)return{results:m,testDefs:l,warnings:a};const d=new Map((l??[]).map(s=>[s.testNumber,s]));for(const s of i)d.set(s.def.testNumber,s.def);return{results:m.map(s=>{let r,b;const v=t=>r?.[t]??s.testValues?.[t],e={value:v,testPass:t=>b?.[t]??D({testValues:r??s.testValues,testPass:b??s.testPass},t,d.get(t)),specPass:t=>{const u=V(v(t),d.get(t));return u===null?void 0:u==="pass"},diePass:()=>P(s,o)};for(const t of i){const u=O(t.ast,e);u!==void 0&&(t.boolean?(b??={...s.testPass},b[t.def.testNumber]=u):(r??={...s.testValues},r[t.def.testNumber]=u))}return r===void 0&&b===void 0?s:{...s,...r!==void 0?{testValues:r}:{},...b!==void 0?{testPass:b}:{}}}),testDefs:[...l??[],...i.map(s=>s.def)],warnings:a}}