Parametric sweeps
A test program often measures one quantity at a series of conditions — supply voltage, gate voltage, bake time, load current — and records each condition as its own test number. Read one test at a time, that is a row of unrelated distributions. Declared as a sweep, each block becomes a curve, and the chart measures the pair of curves against each other: where they cross, and how far apart they are, horizontally, at a given level (the width).

Every sweep you define gets a card in the Insights Sweeps tab, which this page opens on. Each line is the median across the dies in scope, with a p10–p90 band; the Show and Group by controls above rescope all five at once. The five below are common characterisation sweeps, each showing a different use of the crossing and the width.

Every sweep is also a drilldown: back on the grid, select some dies and right-click (or right-click a wafer card) to draw any of these curves — or a histogram, or process capability — from just those dies.

Where a sweep goes

Each snippet below is one entry of insights.sweeps — a chart definition passed to the renderer, not a test and not part of buildWaferMap's input:

renderWaferGallery(container, items, {
  insights: { enabled: true, defaultView: 'sweeps', sweeps: [ /* the five below */ ] },
});

In tsmap the same entries go in a sweeps file of their own (Setup ▾ → Sweeps…, or --sweeps), separate from the test definitions.

1 · Temperature inversion

{ id: 'fmax-vdd',
  title: 'Fmax vs VDD — 25 °C vs 125 °C',
  series: [
    { label: '25 °C',  tests: FMAX_COLD, xValues: VDD },
    { label: '125 °C', tests: FMAX_HOT,  xValues: VDD },
  ],
  separationAt: [1.2e9, 2.0e9],
  xLabel: 'VDD (V)', yLabel: 'Fmax' }

At advanced nodes the hot corner is faster at low voltage: threshold voltage falls with temperature faster than mobility loss slows the transistor. The crossing is the temperature-inversion voltage — below it, 125 °C is no longer the worst case, which changes which corner a low-voltage sign-off has to be run at. The two widths are the extra VDD the slower corner needs to reach 1.2 GHz and 2.0 GHz; note that the slower corner swaps between them.

2 · DIBL, on a log scale

// One derived test per step: log10 of the measured drain current.
{ testNumber: 3200 + i, name: `log Id lin ${vg}`,
  expression: `log10(t[${3100 + i}])` }

{ id: 'dibl',
  title: 'NMOS transfer curve — Vds 0.05 V vs 0.8 V',
  series: [
    { label: 'Vds = 0.05 V', tests: LOG_ID_LIN, xValues: VG },
    { label: 'Vds = 0.8 V',  tests: LOG_ID_SAT, xValues: VG },
  ],
  crossing: false,
  separationAt: [-7],
  xLabel: 'Vg (V)', yLabel: 'log10 Id (A)' }

A transfer curve spans decades of current, so it is only readable with current on a log scale. That is the y axis, and a sweep reads any test, including a derived one — so each step is swept as log10(t[n]). (A log x axis is xScale; see sweep 5.) The width at log10 Id = −7 (100 nA) is the gate-voltage shift between the two drain biases at constant current — the constant-current threshold shift. Divided by the 0.75 V change in Vds it is DIBL. The curves never cross, so crossing: false keeps the card from reporting that.

The map opens on Subthreshold slope, a derived test of two of those derived tests: 0.1 / (t[3202] - t[3200]), the gate voltage per decade of current. The † in front of its name says it was not measured.

3 · Data retention

const BAKE_H = [0, 24, 48, 96, 168, 336, 500, 750, 1000];

{ id: 'retention',
  title: 'Retention — cell Vt through a 150 °C bake',
  series: [
    { label: 'Programmed', tests: ['3300..3308'], xValues: BAKE_H },
    { label: 'Erased',     tests: ['3310..3318'], xValues: BAKE_H },
  ],
  xLabel: 'Bake time (h)', yLabel: 'Cell Vt' }

The two cell states drift towards each other as charge leaks during an accelerated bake; the crossing is where the median read window closes completely. The read points are not evenly spaced — they rarely are in a qualification — which is what xValues is for: with it the crossing is reported in hours, and without it the axis would be the ordinal position of each read, with the crossing "between the 8th and 9th". Nothing is interpolated along test numbers, which are identifiers, not a scale.

The tests are named as ranges, in the same 3300..3308 syntax a derived-test expression uses. A range picks up only the tests that exist inside it, so a program numbered in steps of two needs nothing extra; if one of them is missing from the data, the range comes up one short of xValues and the card says so, rather than sliding every later bake time onto the wrong read.

4 · Output drive strength

// In amps, the bare unit — xUnit prints them as mA.
const LOAD_A = [0, 2, 4, 6, 8, 10, 12, 14, 16].map(mA => mA / 1000);

{ id: 'io-drive',
  title: 'Output driver — VOH vs VOL under load',
  series: [
    { label: 'VOH (sourcing)', tests: ['3400..3408'], xValues: LOAD_A },
    { label: 'VOL (sinking)',  tests: ['3410..3418'], xValues: LOAD_A },
  ],
  xLabel: 'Load current', xUnit: 'A', yLabel: 'Output voltage' }

As load current rises, the high output sags and the low output climbs. Where they cross, the pin can no longer present two distinguishable logic levels: that current is the absolute ceiling on what the driver can deliver, well beyond where it would already have left the VOH/VOL spec. The p10–p90 bands show how much of the population gets there early. xUnit makes the axis and the crossing read 8 mA rather than a bare number beside "(mA)".

5 · Resistance distribution, values from test names

// Tests named "Normalized_LRS= LRS_STATS_12K / Total_LRS (…)": the
// threshold is only in the name, so read it from there.
{ id: 'lrs-cdf',
  title: 'RRAM LRS distribution — before vs after bake',
  series: [
    { label: 'Before bake', tests: ['3500..3509'], xFromName: 'LRS_STATS_{x}' },
    { label: 'After bake',  tests: ['3510..3519'], xFromName: 'LRS_STATS_{x}' },
  ],
  xScale: 'log',            // thresholds grow by multiples: 1K … 1M
  xUnit: 'Ω', xLabel: 'LRS threshold',
  yLabel: 'Fraction of cells below',
  separationAt: [0.5],      // the median resistance shift
  crossing: false }

Each test is one point of a cumulative distribution: the fraction of RRAM cells whose low-resistance state reads below a threshold. The program writes the threshold only into the test text, so xFromName reads it from there — {x} takes the number with its SI prefix, so 12K is 12,000 Ω. It is a placeholder pattern, not a regular expression, so a shared sweeps file cannot freeze the app.

The thresholds go up by multiples, so xScale: 'log' gives each the same room. On a log axis the width at 0.5 is a ratio — how many times higher the median cell resistance is after the bake — with both ends shown. Two distributions of the same cells are not expected to meet, so crossing: false.