Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,27 @@ All notable changes to this project will be documented in this file. See [commit
### Bug fixes

- transform a division or unit conversion only when the result is exact at the
configured `precision`. `calc(100% / 3)` stays `calc(100% / 3)` instead of
becoming `calc(33.33333%)`, and `calc(1cm + 1px)`
is no longer converted to an approximate `1.02646cm`.
configured `precision`. For example, keep `calc(100% / 3)` as `calc(100% / 3)` instead of
transforming to `calc(33.33333%)`, and no longer convert `calc(1cm + 1px)`
to an approximate `1.02646cm`.
Use `precision: false` to transform every division.
- keep `precision` significant digits for values below 1, so
`calc(1px / 150000)` is no longer rounded to `.00001px`
- `min()`, `max()` and `clamp()` preserve the original unit
- keep the order of terms and factors around `var()`, `env()`, `attr()` and
other substitution functions. These are replaced by raw tokens before the
value is computed, so with `--a: 1px + 2px`, `var(--a) * 2` means
`1px + 2px * 2`. Factors are no longer reordered or cancelled:
`var(--a) * 2` is not rewritten to `2 * var(--a)`, `var(--a) / var(--a)` is
not cancelled, and parentheses are kept (`1px - (2 * var(--a))`).
Unrecognised functions such as `anchor-size()` are treated the same way, so
- preserve the original unit in `min()`, `max()` and `clamp()`
- keep the order of factors around `var()`, `env()`, `attr()` and
other substitution functions.
Factors are no longer reordered or cancelled:
do not rewrite `var(--a) * 2` to `2 * var(--a)`, do not cancel `var(--a) / var(--a)`
and keep parentheses (`1px - (2 * var(--a))`).
Treat unrecognised functions such as `anchor-size()` the same way, so
`2 * anchor-size(width) * .5` is no longer folded
- keep the parentheses of a nested `calc()` that contains unresolved values, so
`calc(var(--a) - calc(var(--b) - var(--c)))` is no longer rewritten to
`calc(var(--a) - var(--b) + var(--c))`
- do not flip every sign of a parenthesized sum that starts with a negative
term: `calc((var(--b) - 7 - 2))` is now `calc(-9 + var(--b))` instead of
`calc(-1 * (9 - var(--b)))`, which changed the value when `--b` expands to
several tokens
term: `calc((var(--b) - 7 - 2))` now becomes `calc(-9 + var(--b))` instead of
`calc(-1 * (9 - var(--b)))`, because this last transformation changes the value
when `--b` expands to several tokens

## 11.2.1 (2026-09-20)

Expand Down
155 changes: 155 additions & 0 deletions scripts/benchmark/bootstrap-matrix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { seededRandom } from './random.js';
import { finiteValues } from './statistics.js';

export function matrixWidth(rows) {
const width = rows[0]?.length;
if (!Number.isInteger(width) || width <= 0)
throw new RangeError('cannot bootstrap a matrix without columns');
return width;
}

export function validateRows(rows, width) {
for (const row of rows) {
if (!Array.isArray(row) || row.length !== width)
throw new TypeError('bootstrap rows must have equal widths');
finiteValues(row);
}
}

export function columnMeans(rows, width) {
const means = Array(width).fill(0);
for (const row of rows)
for (let column = 0; column < width; column++) means[column] += row[column];
return means.map((sum) => sum / rows.length);
}

export function resampleRows(
rows,
width,
resamples,
seed,
observed,
standardErrors
) {
const distributions = Array.from({ length: width }, () => Array(resamples));
const studentizedDeviations = Array(resamples);
const random = seededRandom(seed);
for (let sample = 0; sample < resamples; sample++) {
const sums = Array(width).fill(0);
for (let i = 0; i < rows.length; i++) {
const row = rows[Math.floor(random() * rows.length)];
for (let column = 0; column < width; column++)
sums[column] += row[column];
}
let maxDeviation = 0;
for (let column = 0; column < width; column++) {
const mean = sums[column] / rows.length;
distributions[column][sample] = mean;
const deviation = pairedDeviation(
mean,
observed[column],
standardErrors[column]
);
maxDeviation = Math.max(maxDeviation, deviation);
}
studentizedDeviations[sample] = maxDeviation;
}
return { distributions, studentizedDeviations };
}

export function pairedDeviation(mean, observed, standardError) {
if (standardError !== 0) return Math.abs((mean - observed) / standardError);
return mean === observed ? 0 : Infinity;
}

export function splitStrata(rows, strata) {
const labels = [...new Set(strata)];
if (labels.length !== 2)
throw new RangeError('stratified max-T requires exactly two strata');
const ordered = labels.sort();
const strataRows = ordered.map((label) =>
rows
.map((row, index) => ({ row, label: strata[index] }))
.filter((item) => item.label === label)
.map((item) => item.row)
.sort(compareRows)
);
if (strataRows.some((group) => group.length === 0))
throw new RangeError('cannot resample an empty stratum');
return { ordered, strataRows };
}

export function resampleStratum(group, indices, means, width, random) {
means.fill(0);
for (let i = 0; i < group.length; i++) {
const index = Math.floor(random() * group.length);
indices[i] = index;
const row = group[index];
for (let column = 0; column < width; column++) means[column] += row[column];
}
for (let column = 0; column < width; column++) means[column] /= group.length;
}

export function sampledEffect(sampledMeans, column) {
let effect = 0;
for (const means of sampledMeans) effect += means[column];
return effect / sampledMeans.length;
}

export function sampledVariance(
strataRows,
sampledIndices,
sampledMeans,
column
) {
let variance = 0;
for (let stratum = 0; stratum < strataRows.length; stratum++) {
const group = strataRows[stratum];
const indices = sampledIndices[stratum];
const mean = sampledMeans[stratum][column];
let within = 0;
for (let i = 0; i < indices.length; i++)
within += (group[indices[i]][column] - mean) ** 2;
variance += within / Math.max(1, group.length - 1) / group.length;
}
return variance;
}

export function studentizedStatistic(
deviation,
sampledSE,
observedSE,
counters
) {
if (sampledSE !== 0) return deviation / sampledSE;
counters.degenerateEndpoint = true;
if (deviation === 0) return 0;
if (observedSE === 0)
throw new RangeError(
'nonzero bootstrap deviation has no positive standard error'
);
counters.degenerateFallbacks++;
return deviation / observedSE;
}

export function standardErrorsForSample(stratumMeans, sampledRows, width) {
return Array.from({ length: width }, (_, column) => {
let variance = 0;
for (let stratum = 0; stratum < sampledRows.length; stratum++) {
const rows = sampledRows[stratum];
const mean = stratumMeans[stratum][column];
const within =
rows.reduce((sum, row) => sum + (row[column] - mean) ** 2, 0) /
Math.max(1, rows.length - 1);
variance += within / rows.length;
}
return Math.sqrt(variance / stratumMeans.length ** 2);
});
}

function compareRows(left, right) {
for (let index = 0; index < left.length; index++) {
if (left[index] !== right[index]) return left[index] - right[index];
}
return 0;
}
Loading
Loading