src/webgpu/util/math.ts (1,749 lines of code) (raw):
import { ROArrayArray, ROArrayArrayArray } from '../../common/util/types.js';
import { assert } from '../../common/util/util.js';
import {
Float16Array,
getFloat16,
hfround,
setFloat16,
} from '../../external/petamoriken/float16/float16.js';
import { kBit, kValue } from './constants.js';
import {
reinterpretF64AsU64,
reinterpretU64AsF64,
reinterpretU32AsF32,
reinterpretU16AsF16,
} from './reinterpret.js';
/**
* A multiple of 8 guaranteed to be way too large to allocate (just under 8 pebibytes).
* This is a "safe" integer (ULP <= 1.0) very close to MAX_SAFE_INTEGER.
*
* Note: allocations of this size are likely to exceed limitations other than just the system's
* physical memory, so test cases are also needed to try to trigger "true" OOM.
*/
export const kMaxSafeMultipleOf8 = Number.MAX_SAFE_INTEGER - 7;
/** Round `n` up to the next multiple of `alignment` (inclusive). */
// MAINTENANCE_TODO: Rename to `roundUp`
export function align(n: number, alignment: number): number {
assert(Number.isInteger(n) && n >= 0, 'n must be a non-negative integer');
assert(Number.isInteger(alignment) && alignment > 0, 'alignment must be a positive integer');
return Math.ceil(n / alignment) * alignment;
}
/** Round `n` down to the next multiple of `alignment` (inclusive). */
export function roundDown(n: number, alignment: number): number {
assert(Number.isInteger(n) && n >= 0, 'n must be a non-negative integer');
assert(Number.isInteger(alignment) && alignment > 0, 'alignment must be a positive integer');
return Math.floor(n / alignment) * alignment;
}
/** Clamp a number to the provided range. */
export function clamp(n: number, { min, max }: { min: number; max: number }): number {
assert(max >= min);
return Math.min(Math.max(n, min), max);
}
/** @returns 0 if |val| is a subnormal f64 number, otherwise returns |val| */
export function flushSubnormalNumberF64(val: number): number {
return isSubnormalNumberF64(val) ? 0 : val;
}
/** @returns if number is within subnormal range of f64 */
export function isSubnormalNumberF64(n: number): boolean {
return n > kValue.f64.negative.max && n < kValue.f64.positive.min;
}
/** @returns 0 if |val| is a subnormal f32 number, otherwise returns |val| */
export function flushSubnormalNumberF32(val: number): number {
return isSubnormalNumberF32(val) ? 0 : val;
}
/** @returns if number is within subnormal range of f32 */
export function isSubnormalNumberF32(n: number): boolean {
return n > kValue.f32.negative.max && n < kValue.f32.positive.min;
}
/** @returns if number is in the finite range of f32 */
export function isFiniteF32(n: number) {
return n >= kValue.f32.negative.min && n <= kValue.f32.positive.max;
}
/** @returns 0 if |val| is a subnormal f16 number, otherwise returns |val| */
export function flushSubnormalNumberF16(val: number): number {
return isSubnormalNumberF16(val) ? 0 : val;
}
/** @returns if number is within subnormal range of f16 */
export function isSubnormalNumberF16(n: number): boolean {
return n > kValue.f16.negative.max && n < kValue.f16.positive.min;
}
/** @returns if number is in the finite range of f16 */
export function isFiniteF16(n: number) {
return n >= kValue.f16.negative.min && n <= kValue.f16.positive.max;
}
/** Should FTZ occur during calculations or not */
export type FlushMode = 'flush' | 'no-flush';
/** Should nextAfter calculate towards positive infinity or negative infinity */
export type NextDirection = 'positive' | 'negative';
/**
* Once-allocated ArrayBuffer/views to avoid overhead of allocation when
* converting between numeric formats
*
* Usage of a once-allocated pattern like this makes nextAfterF64 non-reentrant,
* so cannot call itself directly or indirectly.
*/
const nextAfterF64Data = new ArrayBuffer(8);
const nextAfterF64Int = new BigUint64Array(nextAfterF64Data);
const nextAfterF64Float = new Float64Array(nextAfterF64Data);
/**
* @returns the next f64 value after |val|, towards +inf or -inf as specified by |dir|.
* If |mode| is 'flush', all subnormal values will be flushed to 0,
* before processing and for -/+0 the nextAfterF64 will be the closest normal in
* the correct direction.
* If |mode| is 'no-flush', the next subnormal will be calculated when appropriate,
* and for -/+0 the nextAfterF64 will be the closest subnormal in the correct
* direction.
*
* val needs to be in [min f64, max f64]
*/
export function nextAfterF64(val: number, dir: NextDirection, mode: FlushMode): number {
if (Number.isNaN(val)) {
return val;
}
if (val === Number.POSITIVE_INFINITY) {
return kValue.f64.positive.infinity;
}
if (val === Number.NEGATIVE_INFINITY) {
return kValue.f64.negative.infinity;
}
assert(
val <= kValue.f64.positive.max && val >= kValue.f64.negative.min,
`${val} is not in the range of f64`
);
val = mode === 'flush' ? flushSubnormalNumberF64(val) : val;
// -/+0 === 0 returns true
if (val === 0) {
if (dir === 'positive') {
return mode === 'flush' ? kValue.f64.positive.min : kValue.f64.positive.subnormal.min;
} else {
return mode === 'flush' ? kValue.f64.negative.max : kValue.f64.negative.subnormal.max;
}
}
nextAfterF64Float[0] = val;
const is_positive = (nextAfterF64Int[0] & 0x8000_0000_0000_0000n) === 0n;
if (is_positive === (dir === 'positive')) {
nextAfterF64Int[0] += 1n;
} else {
nextAfterF64Int[0] -= 1n;
}
// Checking for overflow
if ((nextAfterF64Int[0] & 0x7ff0_0000_0000_0000n) === 0x7ff0_0000_0000_0000n) {
if (dir === 'positive') {
return kValue.f64.positive.infinity;
} else {
return kValue.f64.negative.infinity;
}
}
return mode === 'flush' ? flushSubnormalNumberF64(nextAfterF64Float[0]) : nextAfterF64Float[0];
}
/**
* Once-allocated ArrayBuffer/views to avoid overhead of allocation when
* converting between numeric formats
*
* Usage of a once-allocated pattern like this makes nextAfterF32 non-reentrant,
* so cannot call itself directly or indirectly.
*/
const nextAfterF32Data = new ArrayBuffer(4);
const nextAfterF32Int = new Uint32Array(nextAfterF32Data);
const nextAfterF32Float = new Float32Array(nextAfterF32Data);
/**
* @returns the next f32 value after |val|, towards +inf or -inf as specified by |dir|.
* If |mode| is 'flush', all subnormal values will be flushed to 0,
* before processing and for -/+0 the nextAfterF32 will be the closest normal in
* the correct direction.
* If |mode| is 'no-flush', the next subnormal will be calculated when appropriate,
* and for -/+0 the nextAfterF32 will be the closest subnormal in the correct
* direction.
*
* val needs to be in [min f32, max f32]
*/
export function nextAfterF32(val: number, dir: NextDirection, mode: FlushMode): number {
if (Number.isNaN(val)) {
return val;
}
if (val === Number.POSITIVE_INFINITY) {
return kValue.f32.positive.infinity;
}
if (val === Number.NEGATIVE_INFINITY) {
return kValue.f32.negative.infinity;
}
assert(
val <= kValue.f32.positive.max && val >= kValue.f32.negative.min,
`${val} is not in the range of f32`
);
val = mode === 'flush' ? flushSubnormalNumberF32(val) : val;
// -/+0 === 0 returns true
if (val === 0) {
if (dir === 'positive') {
return mode === 'flush' ? kValue.f32.positive.min : kValue.f32.positive.subnormal.min;
} else {
return mode === 'flush' ? kValue.f32.negative.max : kValue.f32.negative.subnormal.max;
}
}
nextAfterF32Float[0] = val; // This quantizes from number (f64) to f32
if (
(dir === 'positive' && nextAfterF32Float[0] <= val) ||
(dir === 'negative' && nextAfterF32Float[0] >= val)
) {
// val is either f32 precise or quantizing rounded in the opposite direction
// from what is needed, so need to calculate the value in the correct
// direction.
const is_positive = (nextAfterF32Int[0] & 0x80000000) === 0;
if (is_positive === (dir === 'positive')) {
nextAfterF32Int[0] += 1;
} else {
nextAfterF32Int[0] -= 1;
}
}
// Checking for overflow
if ((nextAfterF32Int[0] & 0x7f800000) === 0x7f800000) {
if (dir === 'positive') {
return kValue.f32.positive.infinity;
} else {
return kValue.f32.negative.infinity;
}
}
return mode === 'flush' ? flushSubnormalNumberF32(nextAfterF32Float[0]) : nextAfterF32Float[0];
}
/**
* Once-allocated ArrayBuffer/views to avoid overhead of allocation when
* converting between numeric formats
*
* Usage of a once-allocated pattern like this makes nextAfterF16 non-reentrant,
* so cannot call itself directly or indirectly.
*/
const nextAfterF16Data = new ArrayBuffer(2);
const nextAfterF16Hex = new Uint16Array(nextAfterF16Data);
const nextAfterF16Float = new Float16Array(nextAfterF16Data);
/**
* @returns the next f16 value after |val|, towards +inf or -inf as specified by |dir|.
* If |mode| is 'flush', all subnormal values will be flushed to 0,
* before processing and for -/+0 the nextAfterF16 will be the closest normal in
* the correct direction.
* If |mode| is 'no-flush', the next subnormal will be calculated when appropriate,
* and for -/+0 the nextAfterF16 will be the closest subnormal in the correct
* direction.
*
* val needs to be in [min f16, max f16]
*/
export function nextAfterF16(val: number, dir: NextDirection, mode: FlushMode): number {
if (Number.isNaN(val)) {
return val;
}
if (val === Number.POSITIVE_INFINITY) {
return kValue.f16.positive.infinity;
}
if (val === Number.NEGATIVE_INFINITY) {
return kValue.f16.negative.infinity;
}
assert(
val <= kValue.f16.positive.max && val >= kValue.f16.negative.min,
`${val} is not in the range of f16`
);
val = mode === 'flush' ? flushSubnormalNumberF16(val) : val;
// -/+0 === 0 returns true
if (val === 0) {
if (dir === 'positive') {
return mode === 'flush' ? kValue.f16.positive.min : kValue.f16.positive.subnormal.min;
} else {
return mode === 'flush' ? kValue.f16.negative.max : kValue.f16.negative.subnormal.max;
}
}
nextAfterF16Float[0] = val; // This quantizes from number (f64) to f16
if (
(dir === 'positive' && nextAfterF16Float[0] <= val) ||
(dir === 'negative' && nextAfterF16Float[0] >= val)
) {
// val is either f16 precise or quantizing rounded in the opposite direction
// from what is needed, so need to calculate the value in the correct
// direction.
const is_positive = (nextAfterF16Hex[0] & 0x8000) === 0;
if (is_positive === (dir === 'positive')) {
nextAfterF16Hex[0] += 1;
} else {
nextAfterF16Hex[0] -= 1;
}
}
// Checking for overflow
if ((nextAfterF16Hex[0] & 0x7c00) === 0x7c00) {
if (dir === 'positive') {
return kValue.f16.positive.infinity;
} else {
return kValue.f16.negative.infinity;
}
}
return mode === 'flush' ? flushSubnormalNumberF16(nextAfterF16Float[0]) : nextAfterF16Float[0];
}
/**
* @returns ulp(x), the unit of least precision for a specific number as a 64-bit float
*
* ulp(x) is the distance between the two floating point numbers nearest x.
* This value is also called unit of last place, ULP, and 1 ULP.
* See the WGSL spec and http://www.ens-lyon.fr/LIP/Pub/Rapports/RR/RR2005/RR2005-09.pdf
* for a more detailed/nuanced discussion of the definition of ulp(x).
*
* @param target number to calculate ULP for
* @param mode should FTZ occurring during calculation or not
*/
export function oneULPF64(target: number, mode: FlushMode = 'flush'): number {
if (Number.isNaN(target)) {
return Number.NaN;
}
target = mode === 'flush' ? flushSubnormalNumberF64(target) : target;
// For values out of bounds for f64 ulp(x) is defined as the
// distance between the two nearest f64 representable numbers to the
// appropriate edge, which also happens to be the maximum possible ULP.
if (
target === Number.POSITIVE_INFINITY ||
target >= kValue.f64.positive.max ||
target === Number.NEGATIVE_INFINITY ||
target <= kValue.f64.negative.min
) {
return kValue.f64.max_ulp;
}
// ulp(x) is min(after - before), where
// before <= x <= after
// before =/= after
// before and after are f64 representable
const before = nextAfterF64(target, 'negative', mode);
const after = nextAfterF64(target, 'positive', mode);
// Since number is internally a f64, |target| is always f64 representable, so
// either before or after will be x
return Math.min(target - before, after - target);
}
/**
* @returns ulp(x), the unit of least precision for a specific number as a 32-bit float
*
* ulp(x) is the distance between the two floating point numbers nearest x.
* This value is also called unit of last place, ULP, and 1 ULP.
* See the WGSL spec and http://www.ens-lyon.fr/LIP/Pub/Rapports/RR/RR2005/RR2005-09.pdf
* for a more detailed/nuanced discussion of the definition of ulp(x).
*
* @param target number to calculate ULP for
* @param mode should FTZ occurring during calculation or not
*/
export function oneULPF32(target: number, mode: FlushMode = 'flush'): number {
if (Number.isNaN(target)) {
return Number.NaN;
}
target = mode === 'flush' ? flushSubnormalNumberF32(target) : target;
// For values out of bounds for f32 ulp(x) is defined as the
// distance between the two nearest f32 representable numbers to the
// appropriate edge, which also happens to be the maximum possible ULP.
if (
target === Number.POSITIVE_INFINITY ||
target >= kValue.f32.positive.max ||
target === Number.NEGATIVE_INFINITY ||
target <= kValue.f32.negative.min
) {
return kValue.f32.max_ulp;
}
// ulp(x) is min(after - before), where
// before <= x <= after
// before =/= after
// before and after are f32 representable
const before = nextAfterF32(target, 'negative', mode);
const after = nextAfterF32(target, 'positive', mode);
const converted: number = quantizeToF32(target);
if (converted === target) {
// |target| is f32 representable, so either before or after will be x
return Math.min(target - before, after - target);
} else {
// |target| is not f32 representable so taking distance of neighbouring f32s.
return after - before;
}
}
/**
* @returns an integer value between 0..0xffffffff using a simple non-cryptographic hash function
* @param values integers to generate hash from.
*/
export function hashU32(...values: number[]) {
let n = 0x3504_f333;
for (const v of values) {
n = v + (n << 7) + (n >>> 1);
n = (n * 0x29493) & 0xffff_ffff;
}
n ^= n >>> 8;
n += n << 15;
n = n & 0xffff_ffff;
if (n < 0) {
n = ~n * 2 + 1;
}
return n;
}
/**
* @returns ulp(x), the unit of least precision for a specific number as a 32-bit float
*
* ulp(x) is the distance between the two floating point numbers nearest x.
* This value is also called unit of last place, ULP, and 1 ULP.
* See the WGSL spec and http://www.ens-lyon.fr/LIP/Pub/Rapports/RR/RR2005/RR2005-09.pdf
* for a more detailed/nuanced discussion of the definition of ulp(x).
*
* @param target number to calculate ULP for
* @param mode should FTZ occurring during calculation or not
*/
export function oneULPF16(target: number, mode: FlushMode = 'flush'): number {
if (Number.isNaN(target)) {
return Number.NaN;
}
target = mode === 'flush' ? flushSubnormalNumberF16(target) : target;
// For values out of bounds for f16 ulp(x) is defined as the
// distance between the two nearest f16 representable numbers to the
// appropriate edge, which also happens to be the maximum possible ULP.
if (
target === Number.POSITIVE_INFINITY ||
target >= kValue.f16.positive.max ||
target === Number.NEGATIVE_INFINITY ||
target <= kValue.f16.negative.min
) {
return kValue.f16.max_ulp;
}
// ulp(x) is min(after - before), where
// before <= x <= after
// before =/= after
// before and after are f16 representable
const before = nextAfterF16(target, 'negative', mode);
const after = nextAfterF16(target, 'positive', mode);
const converted: number = quantizeToF16(target);
if (converted === target) {
// |target| is f16 representable, so either before or after will be x
return Math.min(target - before, after - target);
} else {
// |target| is not f16 representable so taking distance of neighbouring f16s.
return after - before;
}
}
/**
* Calculate the valid roundings when quantizing to 64-bit floats
*
* TS/JS's number type is internally a f64, so the supplied value will be
* quanitized by definition. The only corner cases occur if a non-finite value
* is provided, since the valid roundings include the appropriate min or max
* value.
*
* @param n number to be quantized
* @returns all of the acceptable roundings for quantizing to 64-bits in
* ascending order.
*/
export function correctlyRoundedF64(n: number): readonly number[] {
assert(!Number.isNaN(n), `correctlyRoundedF32 not defined for NaN`);
// Above f64 range
if (n === Number.POSITIVE_INFINITY) {
return [kValue.f64.positive.max, Number.POSITIVE_INFINITY];
}
// Below f64 range
if (n === Number.NEGATIVE_INFINITY) {
return [Number.NEGATIVE_INFINITY, kValue.f64.negative.min];
}
return [n];
}
/**
* Calculate the valid roundings when quantizing to 32-bit floats
*
* TS/JS's number type is internally a f64, so quantization needs to occur when
* converting to f32 for WGSL. WGSL does not specify a specific rounding mode,
* so if a number is not precisely representable in 32-bits, but in the
* range, there are two possible valid quantizations. If it is precisely
* representable, there is only one valid quantization. This function calculates
* the valid roundings and returns them in an array.
*
* This function does not consider flushing mode, so subnormals are maintained.
* The caller is responsible to flushing before and after as appropriate.
*
* Out of bounds values need to consider how they interact with the overflow
* rules.
* * If a value is OOB but not too far out, an implementation may choose to round
* to nearest finite value or the correct infinity. This boundary is at
* 2^(f32.emax + 1) and -(2^(f32.emax + 1)) respectively.
* Values that are at or beyond these limits must be rounded towards the
* appropriate infinity.
*
* @param n number to be quantized
* @returns all of the acceptable roundings for quantizing to 32-bits in
* ascending order.
*/
export function correctlyRoundedF32(n: number): readonly number[] {
if (Number.isNaN(n)) {
return [n];
}
// Greater than or equal to the upper overflow boundry
if (n >= 2 ** (kValue.f32.emax + 1)) {
return [Number.POSITIVE_INFINITY];
}
// OOB, but less than the upper overflow boundary
if (n > kValue.f32.positive.max) {
return [kValue.f32.positive.max, Number.POSITIVE_INFINITY];
}
// f32 finite
if (n <= kValue.f32.positive.max && n >= kValue.f32.negative.min) {
const n_32 = quantizeToF32(n);
if (n === n_32) {
// n is precisely expressible as a f32, so should not be rounded
return [n];
}
if (n_32 > n) {
// n_32 rounded towards +inf, so is after n
const other = nextAfterF32(n_32, 'negative', 'no-flush');
return [other, n_32];
} else {
// n_32 rounded towards -inf, so is before n
const other = nextAfterF32(n_32, 'positive', 'no-flush');
return [n_32, other];
}
}
// OOB, but greater the lower overflow boundary
if (n > -(2 ** (kValue.f32.emax + 1))) {
return [Number.NEGATIVE_INFINITY, kValue.f32.negative.min];
}
// Less than or equal to the lower overflow boundary
return [Number.NEGATIVE_INFINITY];
}
/**
* Calculate the valid roundings when quantizing to 16-bit floats
*
* TS/JS's number type is internally a f64, so quantization needs to occur when
* converting to f16 for WGSL. WGSL does not specify a specific rounding mode,
* so if a number is not precisely representable in 16-bits, but in the
* range, there are two possible valid quantizations. If it is precisely
* representable, there is only one valid quantization. This function calculates
* the valid roundings and returns them in an array.
*
* This function does not consider flushing mode, so subnormals are maintained.
* The caller is responsible to flushing before and after as appropriate.
*
* Out of bounds values need to consider how they interact with the overflow
* rules.
* * If a value is OOB but not too far out, an implementation may choose to round
* to nearest finite value or the correct infinity. This boundary is at
* 2^(f16.emax + 1) and -(2^(f16.emax + 1)) respectively.
* Values that are at or beyond these limits must be rounded towards the
* appropriate infinity.
*
* @param n number to be quantized
* @returns all of the acceptable roundings for quantizing to 16-bits in
* ascending order.
*/
export function correctlyRoundedF16(n: number): readonly number[] {
if (Number.isNaN(n)) {
return [n];
}
// Greater than or equal to the upper overflow boundry
if (n >= 2 ** (kValue.f16.emax + 1)) {
return [Number.POSITIVE_INFINITY];
}
// OOB, but less than the upper overflow boundary
if (n > kValue.f16.positive.max) {
return [kValue.f16.positive.max, Number.POSITIVE_INFINITY];
}
// f16 finite
if (n <= kValue.f16.positive.max && n >= kValue.f16.negative.min) {
const n_16 = quantizeToF16(n);
if (n === n_16) {
// n is precisely expressible as a f16, so should not be rounded
return [n];
}
if (n_16 > n) {
// n_16 rounded towards +inf, so is after n
const other = nextAfterF16(n_16, 'negative', 'no-flush');
return [other, n_16];
} else {
// n_16 rounded towards -inf, so is before n
const other = nextAfterF16(n_16, 'positive', 'no-flush');
return [n_16, other];
}
}
// OOB, but greater the lower overflow boundary
if (n > -(2 ** (kValue.f16.emax + 1))) {
return [Number.NEGATIVE_INFINITY, kValue.f16.negative.min];
}
// Less than or equal to the lower overflow boundary
return [Number.NEGATIVE_INFINITY];
}
/**
* Calculates WGSL frexp
*
* Splits val into a fraction and an exponent so that
* val = fraction * 2 ^ exponent.
* The fraction is 0.0 or its magnitude is in the range [0.5, 1.0).
*
* @param val the float to split
* @param trait the float type, f32 or f16 or f64
* @returns the results of splitting val
*/
export function frexp(val: number, trait: 'f32' | 'f16' | 'f64'): { fract: number; exp: number } {
const buffer = new ArrayBuffer(8);
const dataView = new DataView(buffer);
// expBitCount and fractBitCount is the bitwidth of exponent and fractional part of the given FP type.
// expBias is the bias constant of exponent of the given FP type.
// Biased exponent (unsigned integer, i.e. the exponent part of float) = unbiased exponent (signed integer) + expBias.
let expBitCount: number, fractBitCount: number, expBias: number;
// To handle the exponent bits of given FP types (f16, f32, and f64), considering the highest 16
// bits is enough.
// expMaskForHigh16Bits indicates the exponent bitfield in the highest 16 bits of the given FP
// type, and targetExpBitsForHigh16Bits is the exponent bits that corresponding to unbiased
// exponent -1, i.e. the exponent bits when the FP values is in range [0.5, 1.0).
let expMaskForHigh16Bits: number, targetExpBitsForHigh16Bits: number;
// Helper function that store the given FP value into buffer as the given FP types
let setFloatToBuffer: (v: number) => void;
// Helper function that read back FP value from buffer as the given FP types
let getFloatFromBuffer: () => number;
let isFinite: (v: number) => boolean;
let isSubnormal: (v: number) => boolean;
if (trait === 'f32') {
// f32 bit pattern: s_eeeeeeee_fffffff_ffffffffffffffff
expBitCount = 8;
fractBitCount = 23;
expBias = 127;
// The exponent bitmask for high 16 bits of f32.
expMaskForHigh16Bits = 0x7f80;
// The target exponent bits is equal to those for f32 0.5 = 0x3f000000.
targetExpBitsForHigh16Bits = 0x3f00;
isFinite = isFiniteF32;
isSubnormal = isSubnormalNumberF32;
// Enforce big-endian so that offset 0 is highest byte.
setFloatToBuffer = (v: number) => dataView.setFloat32(0, v, false);
getFloatFromBuffer = () => dataView.getFloat32(0, false);
} else if (trait === 'f16') {
// f16 bit pattern: s_eeeee_ffffffffff
expBitCount = 5;
fractBitCount = 10;
expBias = 15;
// The exponent bitmask for 16 bits of f16.
expMaskForHigh16Bits = 0x7c00;
// The target exponent bits is equal to those for f16 0.5 = 0x3800.
targetExpBitsForHigh16Bits = 0x3800;
isFinite = isFiniteF16;
isSubnormal = isSubnormalNumberF16;
// Enforce big-endian so that offset 0 is highest byte.
setFloatToBuffer = (v: number) => setFloat16(dataView, 0, v, false);
getFloatFromBuffer = () => getFloat16(dataView, 0, false);
} else {
assert(trait === 'f64');
// f64 bit pattern: s_eeeeeeeeeee_ffff_ffffffffffffffffffffffffffffffffffffffffffffffff
expBitCount = 11;
fractBitCount = 52;
expBias = 1023;
// The exponent bitmask for 16 bits of f64.
expMaskForHigh16Bits = 0x7ff0;
// The target exponent bits is equal to those for f64 0.5 = 0x3fe0_0000_0000_0000.
targetExpBitsForHigh16Bits = 0x3fe0;
isFinite = Number.isFinite;
isSubnormal = isSubnormalNumberF64;
// Enforce big-endian so that offset 0 is highest byte.
setFloatToBuffer = (v: number) => dataView.setFloat64(0, v, false);
getFloatFromBuffer = () => dataView.getFloat64(0, false);
}
// Helper function that extract the unbiased exponent of the float in buffer.
const extractUnbiasedExpFromNormalFloatInBuffer = () => {
// Assert the float in buffer is finite normal float.
assert(isFinite(getFloatFromBuffer()) && !isSubnormal(getFloatFromBuffer()));
// Get the highest 16 bits of float as uint16, which can contain the whole exponent part for both f16, f32, and f64.
const high16BitsAsUint16 = dataView.getUint16(0, false);
// Return the unbiased exp by masking, shifting and unbiasing.
return ((high16BitsAsUint16 & expMaskForHigh16Bits) >> (16 - 1 - expBitCount)) - expBias;
};
// Helper function that modify the exponent of float in buffer to make it in range [0.5, 1.0).
// By setting the unbiased exponent to -1, the fp value will be in range 2**-1 * [1.0, 2.0), i.e. [0.5, 1.0).
const modifyExpOfNormalFloatInBuffer = () => {
// Assert the float in buffer is finite normal float.
assert(isFinite(getFloatFromBuffer()) && !isSubnormal(getFloatFromBuffer()));
// Get the highest 16 bits of float as uint16, which contains the whole exponent part for both f16, f32, and f64.
const high16BitsAsUint16 = dataView.getUint16(0, false);
// Modify the exponent bits.
const modifiedHigh16Bits =
(high16BitsAsUint16 & ~expMaskForHigh16Bits) | targetExpBitsForHigh16Bits;
// Set back to buffer
dataView.setUint16(0, modifiedHigh16Bits, false);
};
// +/- 0.0
if (val === 0) {
return { fract: val, exp: 0 };
}
// NaN and Inf
if (!isFinite(val)) {
return { fract: val, exp: 0 };
}
setFloatToBuffer(val);
// Don't use val below. Use helper functions working with buffer instead.
let exp = 0;
// Normailze the value if it is subnormal. Increase the exponent by multiplying a subnormal value
// with 2**fractBitCount will result in a finite normal FP value of the given FP type.
if (isSubnormal(getFloatFromBuffer())) {
setFloatToBuffer(getFloatFromBuffer() * 2 ** fractBitCount);
exp = -fractBitCount;
}
// A normal FP value v is represented as v = ((-1)**s)*(2**(unbiased exponent))*f, where f is in
// range [1.0, 2.0). By moving a factor 2 from f to exponent, we have
// v = ((-1)**s)*(2**(unbiased exponent + 1))*(f / 2), where (f / 2) is in range [0.5, 1.0), so
// the exp = (unbiased exponent + 1) and fract = ((-1)**s)*(f / 2) is what we expect to get from
// frexp function. Note that fract and v only differs in exponent bitfield as long as v is normal.
// Calc the result exp by getting the unbiased float exponent and plus 1.
exp += extractUnbiasedExpFromNormalFloatInBuffer() + 1;
// Modify the exponent of float in buffer to make it be in range [0.5, 1.0) to get fract.
modifyExpOfNormalFloatInBuffer();
return { fract: getFloatFromBuffer(), exp };
}
/**
* Calculates the linear interpolation between two values of a given fractional.
*
* If |t| is 0, |a| is returned, if |t| is 1, |b| is returned, otherwise
* interpolation/extrapolation equivalent to a + t(b - a) is performed.
*
* Numerical stable version is adapted from http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0811r2.html
*/
export function lerp(a: number, b: number, t: number): number {
if (!Number.isFinite(a) || !Number.isFinite(b)) {
return Number.NaN;
}
if ((a <= 0.0 && b >= 0.0) || (a >= 0.0 && b <= 0.0)) {
return t * b + (1 - t) * a;
}
if (t === 1.0) {
return b;
}
const x = a + t * (b - a);
return t > 1.0 === b > a ? Math.max(b, x) : Math.min(b, x);
}
/**
* Version of lerp that operates on bigint values
*
* lerp was not made into a generic or to take in (number|bigint), because that
* introduces a bunch of complexity overhead related to type differentiation
*/
export function lerpBigInt(a: bigint, b: bigint, idx: number, steps: number): bigint {
assert(Math.trunc(idx) === idx);
assert(Math.trunc(steps) === steps);
// This constrains t to [0.0, 1.0]
assert(idx >= 0);
assert(steps > 0);
assert(idx < steps);
if (steps === 1) {
return a;
}
if (idx === 0) {
return a;
}
if (idx === steps - 1) {
return b;
}
const min = (x: bigint, y: bigint): bigint => {
return x < y ? x : y;
};
const max = (x: bigint, y: bigint): bigint => {
return x > y ? x : y;
};
// For number the variable t is used, there t = idx / (steps - 1),
// but that is a fraction on [0, 1], so becomes either 0 or 1 when converted
// to bigint, so need to expand things out.
const big_idx = BigInt(idx);
const big_steps = BigInt(steps);
if ((a <= 0n && b >= 0n) || (a >= 0n && b <= 0n)) {
return (b * big_idx) / (big_steps - 1n) + (a - (a * big_idx) / (big_steps - 1n));
}
const x = a + (b * big_idx) / (big_steps - 1n) - (a * big_idx) / (big_steps - 1n);
return !(b > a) ? max(b, x) : min(b, x);
}
/** @returns a linear increasing range of numbers. */
export function linearRange(a: number, b: number, num_steps: number): readonly number[] {
if (num_steps <= 0) {
return [];
}
// Avoid division by 0
if (num_steps === 1) {
return [a];
}
return Array.from(Array(num_steps).keys()).map(i => lerp(a, b, i / (num_steps - 1)));
}
/**
* Version of linearRange that operates on bigint values
*
* linearRange was not made into a generic or to take in (number|bigint),
* because that introduces a bunch of complexity overhead related to type
* differentiation
*/
export function linearRangeBigInt(a: bigint, b: bigint, num_steps: number): Array<bigint> {
if (num_steps <= 0) {
return [];
}
// Avoid division by 0
if (num_steps === 1) {
return [a];
}
return Array.from(Array(num_steps).keys()).map(i => lerpBigInt(a, b, i, num_steps));
}
/**
* @returns a non-linear increasing range of numbers, with a bias towards the beginning.
*
* Generates a linear range on [0,1] with |num_steps|, then squares all the values to make the curve be quadratic,
* thus biasing towards 0, but remaining on the [0, 1] range.
* This biased range is then scaled to the desired range using lerp.
* Different curves could be generated by changing c, where greater values of c will bias more towards 0.
*/
export function biasedRange(a: number, b: number, num_steps: number): readonly number[] {
const c = 2;
if (num_steps <= 0) {
return [];
}
// Avoid division by 0
if (num_steps === 1) {
return [a];
}
return Array.from(Array(num_steps).keys()).map(i => lerp(a, b, Math.pow(i / (num_steps - 1), c)));
}
/**
* Version of biasedRange that operates on bigint values
*
* biasedRange was not made into a generic or to take in (number|bigint),
* because that introduces a bunch of complexity overhead related to type
* differentiation.
*
* Scaling is used internally so that the number of possible indices is
* significantly larger than num_steps. This is done to avoid duplicate entries
* in the resulting range due to quantizing to integers during the calculation.
*
* If a and b are close together, such that the number of integers between them
* is close to num_steps, then duplicates will occur regardless of scaling.
*/
export function biasedRangeBigInt(a: bigint, b: bigint, num_steps: number): readonly bigint[] {
if (num_steps <= 0) {
return [];
}
// Avoid division by 0
if (num_steps === 1) {
return [a];
}
const c = 2;
const scaling = 1000;
const scaled_num_steps = num_steps * scaling;
return Array.from(Array(num_steps).keys()).map(i => {
const biased_i = Math.pow(i / (num_steps - 1), c); // Floating Point on [0, 1]
const scaled_i = Math.trunc((scaled_num_steps - 1) * biased_i); // Integer on [0, scaled_num_steps - 1]
return lerpBigInt(a, b, scaled_i, scaled_num_steps);
});
}
/**
* @returns an ascending sorted array of numbers spread over the entire range of 32-bit floats
*
* Numbers are divided into 4 regions: negative normals, negative subnormals, positive subnormals & positive normals.
* Zero is included.
*
* Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
* means that number of precise f32 values between each returned value in a region should be about the same. This allows
* for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f32 range.
*
* This function is intended to provide dense coverage of the f32 range, for a minimal list of values to use to cover
* f32 behaviour, use sparseScalarF32Range instead.
*
* @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries
* must be 0 or greater.
*/
export function scalarF32Range(
counts: {
neg_norm?: number;
neg_sub?: number;
pos_sub: number;
pos_norm: number;
} = { pos_sub: 10, pos_norm: 50 }
): Array<number> {
counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;
let special_pos: number[] = [];
// The first interior point for 'pos_norm' is at 3. Because we have two special values we start allowing these
// special values as soon as they will fit as interior values.
if (counts.pos_norm >= 4) {
special_pos = [
// Largest float as signed integer
0x4effffff,
// Largest float as unsigned integer
0x4f7fffff,
];
}
// Generating bit fields first and then converting to f32, so that the spread across the possible f32 values is more
// even. Generating against the bounds of f32 values directly results in the values being extremely biased towards the
// extremes, since they are so much larger.
const bit_fields = [
...linearRange(kBit.f32.negative.min, kBit.f32.negative.max, counts.neg_norm),
...linearRange(
kBit.f32.negative.subnormal.min,
kBit.f32.negative.subnormal.max,
counts.neg_sub
),
// -0.0
0x80000000,
// +0.0
0,
...linearRange(
kBit.f32.positive.subnormal.min,
kBit.f32.positive.subnormal.max,
counts.pos_sub
),
...[
...linearRange(
kBit.f32.positive.min,
kBit.f32.positive.max,
counts.pos_norm - special_pos.length
),
...special_pos,
].sort((n1, n2) => n1 - n2),
].map(Math.trunc);
return bit_fields.map(reinterpretU32AsF32);
}
/**
* @returns an ascending sorted array of numbers.
*
* The numbers returned are based on the `full32Range` as described above. The difference comes depending
* on the `source` parameter. If the `source` is `const` then the numbers will be restricted to be
* in the range `[low, high]`. This allows filtering out a set of `f32` values which are invalid for
* const-evaluation but are needed to test the non-const implementation.
*
* @param source the input source for the test. If the `source` is `const` then the return will be filtered
* @param low the lowest f32 value to permit when filtered
* @param high the highest f32 value to permit when filtered
*/
export function filteredScalarF32Range(source: String, low: number, high: number): Array<number> {
return scalarF32Range().filter(x => source !== 'const' || (x >= low && x <= high));
}
/**
* @returns an ascending sorted array of numbers spread over the entire range of 16-bit floats
*
* Numbers are divided into 4 regions: negative normals, negative subnormals, positive subnormals & positive normals.
* Zero is included.
*
* Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
* means that number of precise f16 values between each returned value in a region should be about the same. This allows
* for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f16 range.
*
* This function is intended to provide dense coverage of the f16 range, for a minimal list of values to use to cover
* f16 behaviour, use sparseScalarF16Range instead.
*
* @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries
* must be 0 or greater.
*/
export function scalarF16Range(
counts: {
neg_norm?: number;
neg_sub?: number;
pos_sub: number;
pos_norm: number;
} = { pos_sub: 10, pos_norm: 50 }
): Array<number> {
counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;
// Generating bit fields first and then converting to f16, so that the spread across the possible f16 values is more
// even. Generating against the bounds of f16 values directly results in the values being extremely biased towards the
// extremes, since they are so much larger.
const bit_fields = [
...linearRange(kBit.f16.negative.min, kBit.f16.negative.max, counts.neg_norm),
...linearRange(
kBit.f16.negative.subnormal.min,
kBit.f16.negative.subnormal.max,
counts.neg_sub
),
// -0.0
0x8000,
// +0.0
0,
...linearRange(
kBit.f16.positive.subnormal.min,
kBit.f16.positive.subnormal.max,
counts.pos_sub
),
...linearRange(kBit.f16.positive.min, kBit.f16.positive.max, counts.pos_norm),
].map(Math.trunc);
return bit_fields.map(reinterpretU16AsF16);
}
/**
* @returns an ascending sorted array of numbers spread over the entire range of 64-bit floats
*
* Numbers are divided into 4 regions: negative normals, negative subnormals, positive subnormals & positive normals.
* Zero is included.
*
* Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
* means that number of precise f64 values between each returned value in a region should be about the same. This allows
* for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the f64 range.
*
* This function is intended to provide dense coverage of the f64 range, for a minimal list of values to use to cover
* f64 behaviour, use sparseScalarF64Range instead.
*
* @param counts structure param with 4 entries indicating the number of entries to be generated each region, entries
* must be 0 or greater.
*/
export function scalarF64Range(
counts: {
neg_norm?: number;
neg_sub?: number;
pos_sub: number;
pos_norm: number;
} = { pos_sub: 10, pos_norm: 50 }
): Array<number> {
counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;
// Generating bit fields first and then converting to f64, so that the spread across the possible f64 values is more
// even. Generating against the bounds of f64 values directly results in the values being extremely biased towards the
// extremes, since they are so much larger.
const bit_fields = [
...linearRangeBigInt(kBit.f64.negative.min, kBit.f64.negative.max, counts.neg_norm),
...linearRangeBigInt(
kBit.f64.negative.subnormal.min,
kBit.f64.negative.subnormal.max,
counts.neg_sub
),
// -0.0
0x8000_0000_0000_0000n,
// +0.0
0n,
...linearRangeBigInt(
kBit.f64.positive.subnormal.min,
kBit.f64.positive.subnormal.max,
counts.pos_sub
),
...linearRangeBigInt(kBit.f64.positive.min, kBit.f64.positive.max, counts.pos_norm),
];
return bit_fields.map(reinterpretU64AsF64);
}
/**
* @returns an ascending sorted array of f64 values spread over specific range of f64 normal floats
*
* Numbers are divided into 4 regions: negative 64-bit normals, negative 64-bit subnormals, positive 64-bit subnormals &
* positive 64-bit normals.
* Zero is included.
*
* Numbers are generated via taking a linear spread of the bit field representations of the values in each region. This
* means that number of precise f64 values between each returned value in a region should be about the same. This allows
* for a wide range of magnitudes to be generated, instead of being extremely biased towards the edges of the range.
*
* @param begin a negative f64 normal float value
* @param end a positive f64 normal float value
* @param counts structure param with 4 entries indicating the number of entries
* to be generated each region, entries must be 0 or greater.
*/
export function limitedScalarF64Range(
begin: number,
end: number,
counts: { neg_norm?: number; neg_sub?: number; pos_sub: number; pos_norm: number } = {
pos_sub: 10,
pos_norm: 50,
}
): Array<number> {
assert(
begin <= kValue.f64.negative.max,
`Beginning of range ${begin} must be negative f64 normal`
);
assert(end >= kValue.f64.positive.min, `Ending of range ${end} must be positive f64 normal`);
counts.neg_norm = counts.neg_norm === undefined ? counts.pos_norm : counts.neg_norm;
counts.neg_sub = counts.neg_sub === undefined ? counts.pos_sub : counts.neg_sub;
const u64_begin = reinterpretF64AsU64(begin);
const u64_end = reinterpretF64AsU64(end);
// Generating bit fields first and then converting to f64, so that the spread across the possible f64 values is more
// even. Generating against the bounds of f64 values directly results in the values being extremely biased towards the
// extremes, since they are so much larger.
const bit_fields = [
...linearRangeBigInt(u64_begin, kBit.f64.negative.max, counts.neg_norm),
...linearRangeBigInt(
kBit.f64.negative.subnormal.min,
kBit.f64.negative.subnormal.max,
counts.neg_sub
),
// -0.0
0x8000_0000_0000_0000n,
// +0.0
0n,
...linearRangeBigInt(
kBit.f64.positive.subnormal.min,
kBit.f64.positive.subnormal.max,
counts.pos_sub
),
...linearRangeBigInt(kBit.f64.positive.min, u64_end, counts.pos_norm),
];
return bit_fields.map(reinterpretU64AsF64);
}
/** Short list of i32 values of interest to test against */
const kInterestingI32Values: readonly number[] = [
kValue.i32.negative.max,
Math.trunc(kValue.i32.negative.max / 2),
-256,
-10,
-1,
0,
1,
10,
256,
Math.trunc(kValue.i32.positive.max / 2),
kValue.i32.positive.max,
];
/** @returns minimal i32 values that cover the entire range of i32 behaviours
*
* This is used instead of fullI32Range when the number of test cases being
* generated is a super linear function of the length of i32 values which is
* leading to time outs.
*/
export function sparseI32Range(): readonly number[] {
return kInterestingI32Values;
}
const kVectorI32Values = {
2: kInterestingI32Values.flatMap(f => [
[f, 1],
[-1, f],
]),
3: kInterestingI32Values.flatMap(f => [
[f, 1, -2],
[-1, f, 2],
[1, -2, f],
]),
4: kInterestingI32Values.flatMap(f => [
[f, -1, 2, 3],
[1, f, -2, 3],
[1, 2, f, -3],
[-1, 2, -3, f],
]),
};
/**
* Returns set of vectors, indexed by dimension containing interesting i32
* values.
*
* The tests do not do the simple option for coverage of computing the cartesian
* product of all of the interesting i32 values N times for vecN tests,
* because that creates a huge number of tests for vec3 and vec4, leading to
* time outs.
*
* Instead they insert the interesting i32 values into each location of the
* vector to get a spread of testing over the entire range. This reduces the
* number of cases being run substantially, but maintains coverage.
*/
export function vectorI32Range(dim: number): ROArrayArray<number> {
assert(dim === 2 || dim === 3 || dim === 4, 'vectorI32Range only accepts dimensions 2, 3, and 4');
return kVectorI32Values[dim];
}
const kSparseVectorI32Values = {
2: sparseI32Range().map((i, idx) => [idx % 2 === 0 ? i : idx, idx % 2 === 1 ? i : -idx]),
3: sparseI32Range().map((i, idx) => [
idx % 3 === 0 ? i : idx,
idx % 3 === 1 ? i : -idx,
idx % 3 === 2 ? i : idx,
]),
4: sparseI32Range().map((i, idx) => [
idx % 4 === 0 ? i : idx,
idx % 4 === 1 ? i : -idx,
idx % 4 === 2 ? i : idx,
idx % 4 === 3 ? i : -idx,
]),
};
/**
* Minimal set of vectors, indexed by dimension, that contain interesting
* abstract integer values.
*
* This is an even more stripped down version of `vectorI32Range` for when
* pairs of vectors are being tested.
* All interesting integers from sparseI32Range are guaranteed to be
* tested, but not in every position.
*/
export function sparseVectorI32Range(dim: number): ROArrayArray<number> {
assert(
dim === 2 || dim === 3 || dim === 4,
'sparseVectorI32Range only accepts dimensions 2, 3, and 4'
);
return kSparseVectorI32Values[dim];
}
/**
* @returns an ascending sorted array of numbers spread over the entire range of 32-bit signed ints
*
* Numbers are divided into 2 regions: negatives, and positives, with their spreads biased towards 0
* Zero is included in range.
*
* @param counts structure param with 2 entries indicating the number of entries to be generated each region, values must be 0 or greater.
*/
export function fullI32Range(
counts: {
negative?: number;
positive: number;
} = { positive: 50 }
): Array<number> {
counts.negative = counts.negative === undefined ? counts.positive : counts.negative;
return [
...biasedRange(kValue.i32.negative.min, -1, counts.negative),
0,
...biasedRange(1, kValue.i32.positive.max, counts.positive),
].map(Math.trunc);
}
/** Short list of u32 values of interest to test against */
const kInterestingU32Values: readonly number[] = [
0,
1,
10,
256,
Math.trunc(kValue.u32.max / 2),
kValue.u32.max,
];
/** @returns minimal u32 values that cover the entire range of u32 behaviours
*
* This is used instead of fullU32Range when the number of test cases being
* generated is a super linear function of the length of u32 values which is
* leading to time outs.
*/
export function sparseU32Range(): readonly number[] {
return kInterestingU32Values;
}
const kVectorU32Values = {
2: kInterestingU32Values.flatMap(f => [
[f, 1],
[1, f],
]),
3: kInterestingU32Values.flatMap(f => [
[f, 1, 2],
[1, f, 2],
[1, 2, f],
]),
4: kInterestingU32Values.flatMap(f => [
[f, 1, 2, 3],
[1, f, 2, 3],
[1, 2, f, 3],
[1, 2, 3, f],
]),
};
/**
* Returns set of vectors, indexed by dimension containing interesting u32
* values.
*
* The tests do not do the simple option for coverage of computing the cartesian
* product of all of the interesting u32 values N times for vecN tests,
* because that creates a huge number of tests for vec3 and vec4, leading to
* time outs.
*
* Instead they insert the interesting u32 values into each location of the
* vector to get a spread of testing over the entire range. This reduces the
* number of cases being run substantially, but maintains coverage.
*/
export function vectorU32Range(dim: number): ROArrayArray<number> {
assert(dim === 2 || dim === 3 || dim === 4, 'vectorU32Range only accepts dimensions 2, 3, and 4');
return kVectorU32Values[dim];
}
const kSparseVectorU32Values = {
2: sparseU32Range().map((i, idx) => [idx % 2 === 0 ? i : idx, idx % 2 === 1 ? i : -idx]),
3: sparseU32Range().map((i, idx) => [
idx % 3 === 0 ? i : idx,
idx % 3 === 1 ? i : -idx,
idx % 3 === 2 ? i : idx,
]),
4: sparseU32Range().map((i, idx) => [
idx % 4 === 0 ? i : idx,
idx % 4 === 1 ? i : -idx,
idx % 4 === 2 ? i : idx,
idx % 4 === 3 ? i : -idx,
]),
};
/**
* Minimal set of vectors, indexed by dimension, that contain interesting
* abstract integer values.
*
* This is an even more stripped down version of `vectorU32Range` for when
* pairs of vectors are being tested.
* All interesting integers from sparseU32Range are guaranteed to be
* tested, but not in every position.
*/
export function sparseVectorU32Range(dim: number): ROArrayArray<number> {
assert(
dim === 2 || dim === 3 || dim === 4,
'sparseVectorU32Range only accepts dimensions 2, 3, and 4'
);
return kSparseVectorU32Values[dim];
}
/**
* @returns an ascending sorted array of numbers spread over the entire range of 32-bit unsigned ints
*
* Numbers are biased towards 0, and 0 is included in the range.
*
* @param count number of entries to include in the range, in addition to 0, must be greater than 0, defaults to 50
*/
export function fullU32Range(count: number = 50): Array<number> {
return [0, ...biasedRange(1, kValue.u32.max, count)].map(Math.trunc);
}
/** Short list of i64 values of interest to test against */
const kInterestingI64Values: readonly bigint[] = [
kValue.i64.negative.max,
kValue.i64.negative.max / 2n,
-256n,
-10n,
-1n,
0n,
1n,
10n,
256n,
kValue.i64.positive.max / 2n,
kValue.i64.positive.max,
];
/** @returns minimal i64 values that cover the entire range of i64 behaviours
*
* This is used instead of fullI64Range when the number of test cases being
* generated is a super linear function of the length of i64 values which is
* leading to time outs.
*/
export function sparseI64Range(): readonly bigint[] {
return kInterestingI64Values;
}
const kVectorI64Values = {
2: kInterestingI64Values.flatMap(f => [
[f, 1n],
[-1n, f],
]),
3: kInterestingI64Values.flatMap(f => [
[f, 1n, -2n],
[-1n, f, 2n],
[1n, -2n, f],
]),
4: kInterestingI64Values.flatMap(f => [
[f, -1n, 2n, 3n],
[1n, f, -2n, 3n],
[1n, 2n, f, -3n],
[-1n, 2n, -3n, f],
]),
};
/**
* Returns set of vectors, indexed by dimension containing interesting i64
* values.
*
* The tests do not do the simple option for coverage of computing the cartesian
* product of all of the interesting i64 values N times for vecN tests,
* because that creates a huge number of tests for vec3 and vec4, leading to
* time outs.
*
* Instead they insert the interesting i64 values into each location of the
* vector to get a spread of testing over the entire range. This reduces the
* number of cases being run substantially, but maintains coverage.
*/
export function vectorI64Range(dim: number): ROArrayArray<bigint> {
assert(dim === 2 || dim === 3 || dim === 4, 'vectorI64Range only accepts dimensions 2, 3, and 4');
return kVectorI64Values[dim];
}
const kSparseVectorI64Values = {
2: sparseI64Range().map((i, idx) => [
idx % 2 === 0 ? i : BigInt(idx),
idx % 2 === 1 ? i : -BigInt(idx),
]),
3: sparseI64Range().map((i, idx) => [
idx % 3 === 0 ? i : BigInt(idx),
idx % 3 === 1 ? i : -BigInt(idx),
idx % 3 === 2 ? i : BigInt(idx),
]),
4: sparseI64Range().map((i, idx) => [
idx % 4 === 0 ? i : BigInt(idx),
idx % 4 === 1 ? i : -BigInt(idx),
idx % 4 === 2 ? i : BigInt(idx),
idx % 4 === 3 ? i : -BigInt(idx),
]),
};
/**
* Minimal set of vectors, indexed by dimension, that contain interesting
* abstract integer values.
*
* This is an even more stripped down version of `vectorI64Range` for when
* pairs of vectors are being tested.
* All interesting integers from sparseI64Range are guaranteed to be
* tested, but not in every position.
*/
export function sparseVectorI64Range(dim: number): ROArrayArray<bigint> {
assert(
dim === 2 || dim === 3 || dim === 4,
'sparseVectorI64Range only accepts dimensions 2, 3, and 4'
);
return kSparseVectorI64Values[dim];
}
/**
* @returns an ascending sorted array of numbers spread over the entire range of 64-bit signed ints
*
* Numbers are divided into 2 regions: negatives, and positives, with their spreads biased towards 0
* Zero is included in range.
*
* @param counts structure param with 2 entries indicating the number of entries to be generated each region, values must be 0 or greater.
*/
export function fullI64Range(
counts: {
negative?: number;
positive: number;
} = { positive: 50 }
): Array<bigint> {
counts.negative = counts.negative === undefined ? counts.positive : counts.negative;
return [
...biasedRangeBigInt(kValue.i64.negative.min, -1n, counts.negative),
0n,
...biasedRangeBigInt(1n, kValue.i64.positive.max, counts.positive),
];
}
/** Short list of f32 values of interest to test against */
const kInterestingF32Values: readonly number[] = [
kValue.f32.negative.min,
-10.0,
-1.0,
-0.125,
kValue.f32.negative.max,
kValue.f32.negative.subnormal.min,
kValue.f32.negative.subnormal.max,
-0.0,
0.0,
kValue.f32.positive.subnormal.min,
kValue.f32.positive.subnormal.max,
kValue.f32.positive.min,
0.125,
1.0,
10.0,
kValue.f32.positive.max,
];
/** @returns minimal f32 values that cover the entire range of f32 behaviours
*
* Has specially selected values that cover edge cases, normals, and subnormals.
* This is used instead of fullF32Range when the number of test cases being
* generated is a super linear function of the length of f32 values which is
* leading to time outs.
*
* These values have been chosen to attempt to test the widest range of f32
* behaviours in the lowest number of entries, so may potentially miss function
* specific values of interest. If there are known values of interest they
* should be appended to this list in the test generation code.
*/
export function sparseScalarF32Range(): readonly number[] {
return kInterestingF32Values;
}
const kVectorF32Values = {
2: kInterestingF32Values.flatMap(f => [
[f, 1.0],
[-1.0, f],
]),
3: kInterestingF32Values.flatMap(f => [
[f, 1.0, -2.0],
[-1.0, f, 2.0],
[1.0, -2.0, f],
]),
4: kInterestingF32Values.flatMap(f => [
[f, -1.0, 2.0, 3.0],
[1.0, f, -2.0, 3.0],
[1.0, 2.0, f, -3.0],
[-1.0, 2.0, -3.0, f],
]),
};
/**
* Returns set of vectors, indexed by dimension containing interesting float
* values.
*
* The tests do not do the simple option for coverage of computing the cartesian
* product of all of the interesting float values N times for vecN tests,
* because that creates a huge number of tests for vec3 and vec4, leading to
* time outs.
*
* Instead they insert the interesting f32 values into each location of the
* vector to get a spread of testing over the entire range. This reduces the
* number of cases being run substantially, but maintains coverage.
*/
export function vectorF32Range(dim: number): ROArrayArray<number> {
assert(dim === 2 || dim === 3 || dim === 4, 'vectorF32Range only accepts dimensions 2, 3, and 4');
return kVectorF32Values[dim];
}
const kSparseVectorF32Values = {
2: sparseScalarF32Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]),
3: sparseScalarF32Range().map((f, idx) => [
idx % 3 === 0 ? f : idx,
idx % 3 === 1 ? f : -idx,
idx % 3 === 2 ? f : idx,
]),
4: sparseScalarF32Range().map((f, idx) => [
idx % 4 === 0 ? f : idx,
idx % 4 === 1 ? f : -idx,
idx % 4 === 2 ? f : idx,
idx % 4 === 3 ? f : -idx,
]),
};
/**
* Minimal set of vectors, indexed by dimension, that contain interesting float
* values.
*
* This is an even more stripped down version of `vectorF32Range` for when
* pairs of vectors are being tested.
* All of the interesting floats from sparseScalarF32 are guaranteed to be
* tested, but not in every position.
*/
export function sparseVectorF32Range(dim: number): ROArrayArray<number> {
assert(
dim === 2 || dim === 3 || dim === 4,
'sparseVectorF32Range only accepts dimensions 2, 3, and 4'
);
return kSparseVectorF32Values[dim];
}
const kSparseMatrixF32Values = {
2: {
2: kInterestingF32Values.map((f, idx) => [
[idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx],
[idx % 4 === 2 ? f : -idx, idx % 4 === 3 ? f : idx],
]),
3: kInterestingF32Values.map((f, idx) => [
[idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx, idx % 6 === 2 ? f : idx],
[idx % 6 === 3 ? f : -idx, idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
]),
4: kInterestingF32Values.map((f, idx) => [
[
idx % 8 === 0 ? f : idx,
idx % 8 === 1 ? f : -idx,
idx % 8 === 2 ? f : idx,
idx % 8 === 3 ? f : -idx,
],
[
idx % 8 === 4 ? f : -idx,
idx % 8 === 5 ? f : idx,
idx % 8 === 6 ? f : -idx,
idx % 8 === 7 ? f : idx,
],
]),
},
3: {
2: kInterestingF32Values.map((f, idx) => [
[idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx],
[idx % 6 === 2 ? f : -idx, idx % 6 === 3 ? f : idx],
[idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
]),
3: kInterestingF32Values.map((f, idx) => [
[idx % 9 === 0 ? f : idx, idx % 9 === 1 ? f : -idx, idx % 9 === 2 ? f : idx],
[idx % 9 === 3 ? f : -idx, idx % 9 === 4 ? f : idx, idx % 9 === 5 ? f : -idx],
[idx % 9 === 6 ? f : idx, idx % 9 === 7 ? f : -idx, idx % 9 === 8 ? f : idx],
]),
4: kInterestingF32Values.map((f, idx) => [
[
idx % 12 === 0 ? f : idx,
idx % 12 === 1 ? f : -idx,
idx % 12 === 2 ? f : idx,
idx % 12 === 3 ? f : -idx,
],
[
idx % 12 === 4 ? f : -idx,
idx % 12 === 5 ? f : idx,
idx % 12 === 6 ? f : -idx,
idx % 12 === 7 ? f : idx,
],
[
idx % 12 === 8 ? f : idx,
idx % 12 === 9 ? f : -idx,
idx % 12 === 10 ? f : idx,
idx % 12 === 11 ? f : -idx,
],
]),
},
4: {
2: kInterestingF32Values.map((f, idx) => [
[idx % 8 === 0 ? f : idx, idx % 8 === 1 ? f : -idx],
[idx % 8 === 2 ? f : -idx, idx % 8 === 3 ? f : idx],
[idx % 8 === 4 ? f : idx, idx % 8 === 5 ? f : -idx],
[idx % 8 === 6 ? f : -idx, idx % 8 === 7 ? f : idx],
]),
3: kInterestingF32Values.map((f, idx) => [
[idx % 12 === 0 ? f : idx, idx % 12 === 1 ? f : -idx, idx % 12 === 2 ? f : idx],
[idx % 12 === 3 ? f : -idx, idx % 12 === 4 ? f : idx, idx % 12 === 5 ? f : -idx],
[idx % 12 === 6 ? f : idx, idx % 12 === 7 ? f : -idx, idx % 12 === 8 ? f : idx],
[idx % 12 === 9 ? f : -idx, idx % 12 === 10 ? f : idx, idx % 12 === 11 ? f : -idx],
]),
4: kInterestingF32Values.map((f, idx) => [
[
idx % 16 === 0 ? f : idx,
idx % 16 === 1 ? f : -idx,
idx % 16 === 2 ? f : idx,
idx % 16 === 3 ? f : -idx,
],
[
idx % 16 === 4 ? f : -idx,
idx % 16 === 5 ? f : idx,
idx % 16 === 6 ? f : -idx,
idx % 16 === 7 ? f : idx,
],
[
idx % 16 === 8 ? f : idx,
idx % 16 === 9 ? f : -idx,
idx % 16 === 10 ? f : idx,
idx % 16 === 11 ? f : -idx,
],
[
idx % 16 === 12 ? f : -idx,
idx % 16 === 13 ? f : idx,
idx % 16 === 14 ? f : -idx,
idx % 16 === 15 ? f : idx,
],
]),
},
};
/**
* Returns a minimal set of matrices, indexed by dimension containing
* interesting float values.
*
* This is the matrix analogue of `sparseVectorF32Range`, so it is producing a
* minimal coverage set of matrices that test all of the interesting f32 values.
* There is not a more expansive set of matrices, since matrices are even more
* expensive than vectors for increasing runtime with coverage.
*
* All of the interesting floats from sparseScalarF32 are guaranteed to be
* tested, but not in every position.
*/
export function sparseMatrixF32Range(c: number, r: number): ROArrayArrayArray<number> {
assert(
c === 2 || c === 3 || c === 4,
'sparseMatrixF32Range only accepts column counts of 2, 3, and 4'
);
assert(
r === 2 || r === 3 || r === 4,
'sparseMatrixF32Range only accepts row counts of 2, 3, and 4'
);
return kSparseMatrixF32Values[c][r];
}
/** Short list of f16 values of interest to test against */
const kInterestingF16Values: readonly number[] = [
kValue.f16.negative.min,
-10.0,
-1.0,
-0.125,
kValue.f16.negative.max,
kValue.f16.negative.subnormal.min,
kValue.f16.negative.subnormal.max,
-0.0,
0.0,
kValue.f16.positive.subnormal.min,
kValue.f16.positive.subnormal.max,
kValue.f16.positive.min,
0.125,
1.0,
10.0,
kValue.f16.positive.max,
];
/** @returns minimal f16 values that cover the entire range of f16 behaviours
*
* Has specially selected values that cover edge cases, normals, and subnormals.
* This is used instead of fullF16Range when the number of test cases being
* generated is a super linear function of the length of f16 values which is
* leading to time outs.
*
* These values have been chosen to attempt to test the widest range of f16
* behaviours in the lowest number of entries, so may potentially miss function
* specific values of interest. If there are known values of interest they
* should be appended to this list in the test generation code.
*/
export function sparseScalarF16Range(): readonly number[] {
return kInterestingF16Values;
}
const kVectorF16Values = {
2: kInterestingF16Values.flatMap(f => [
[f, 1.0],
[-1.0, f],
]),
3: kInterestingF16Values.flatMap(f => [
[f, 1.0, -2.0],
[-1.0, f, 2.0],
[1.0, -2.0, f],
]),
4: kInterestingF16Values.flatMap(f => [
[f, -1.0, 2.0, 3.0],
[1.0, f, -2.0, 3.0],
[1.0, 2.0, f, -3.0],
[-1.0, 2.0, -3.0, f],
]),
};
/**
* Returns set of vectors, indexed by dimension containing interesting f16
* values.
*
* The tests do not do the simple option for coverage of computing the cartesian
* product of all of the interesting float values N times for vecN tests,
* because that creates a huge number of tests for vec3 and vec4, leading to
* time outs.
*
* Instead they insert the interesting f16 values into each location of the
* vector to get a spread of testing over the entire range. This reduces the
* number of cases being run substantially, but maintains coverage.
*/
export function vectorF16Range(dim: number): ROArrayArray<number> {
assert(dim === 2 || dim === 3 || dim === 4, 'vectorF16Range only accepts dimensions 2, 3, and 4');
return kVectorF16Values[dim];
}
const kSparseVectorF16Values = {
2: sparseScalarF16Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]),
3: sparseScalarF16Range().map((f, idx) => [
idx % 3 === 0 ? f : idx,
idx % 3 === 1 ? f : -idx,
idx % 3 === 2 ? f : idx,
]),
4: sparseScalarF16Range().map((f, idx) => [
idx % 4 === 0 ? f : idx,
idx % 4 === 1 ? f : -idx,
idx % 4 === 2 ? f : idx,
idx % 4 === 3 ? f : -idx,
]),
};
/**
* Minimal set of vectors, indexed by dimension, that contain interesting f16
* values.
*
* This is an even more stripped down version of `vectorF16Range` for when
* pairs of vectors are being tested.
* All of the interesting floats from sparseScalarF16 are guaranteed to be
* tested, but not in every position.
*/
export function sparseVectorF16Range(dim: number): ROArrayArray<number> {
assert(
dim === 2 || dim === 3 || dim === 4,
'sparseVectorF16Range only accepts dimensions 2, 3, and 4'
);
return kSparseVectorF16Values[dim];
}
const kSparseMatrixF16Values = {
2: {
2: kInterestingF16Values.map((f, idx) => [
[idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx],
[idx % 4 === 2 ? f : -idx, idx % 4 === 3 ? f : idx],
]),
3: kInterestingF16Values.map((f, idx) => [
[idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx, idx % 6 === 2 ? f : idx],
[idx % 6 === 3 ? f : -idx, idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
]),
4: kInterestingF16Values.map((f, idx) => [
[
idx % 8 === 0 ? f : idx,
idx % 8 === 1 ? f : -idx,
idx % 8 === 2 ? f : idx,
idx % 8 === 3 ? f : -idx,
],
[
idx % 8 === 4 ? f : -idx,
idx % 8 === 5 ? f : idx,
idx % 8 === 6 ? f : -idx,
idx % 8 === 7 ? f : idx,
],
]),
},
3: {
2: kInterestingF16Values.map((f, idx) => [
[idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx],
[idx % 6 === 2 ? f : -idx, idx % 6 === 3 ? f : idx],
[idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
]),
3: kInterestingF16Values.map((f, idx) => [
[idx % 9 === 0 ? f : idx, idx % 9 === 1 ? f : -idx, idx % 9 === 2 ? f : idx],
[idx % 9 === 3 ? f : -idx, idx % 9 === 4 ? f : idx, idx % 9 === 5 ? f : -idx],
[idx % 9 === 6 ? f : idx, idx % 9 === 7 ? f : -idx, idx % 9 === 8 ? f : idx],
]),
4: kInterestingF16Values.map((f, idx) => [
[
idx % 12 === 0 ? f : idx,
idx % 12 === 1 ? f : -idx,
idx % 12 === 2 ? f : idx,
idx % 12 === 3 ? f : -idx,
],
[
idx % 12 === 4 ? f : -idx,
idx % 12 === 5 ? f : idx,
idx % 12 === 6 ? f : -idx,
idx % 12 === 7 ? f : idx,
],
[
idx % 12 === 8 ? f : idx,
idx % 12 === 9 ? f : -idx,
idx % 12 === 10 ? f : idx,
idx % 12 === 11 ? f : -idx,
],
]),
},
4: {
2: kInterestingF16Values.map((f, idx) => [
[idx % 8 === 0 ? f : idx, idx % 8 === 1 ? f : -idx],
[idx % 8 === 2 ? f : -idx, idx % 8 === 3 ? f : idx],
[idx % 8 === 4 ? f : idx, idx % 8 === 5 ? f : -idx],
[idx % 8 === 6 ? f : -idx, idx % 8 === 7 ? f : idx],
]),
3: kInterestingF16Values.map((f, idx) => [
[idx % 12 === 0 ? f : idx, idx % 12 === 1 ? f : -idx, idx % 12 === 2 ? f : idx],
[idx % 12 === 3 ? f : -idx, idx % 12 === 4 ? f : idx, idx % 12 === 5 ? f : -idx],
[idx % 12 === 6 ? f : idx, idx % 12 === 7 ? f : -idx, idx % 12 === 8 ? f : idx],
[idx % 12 === 9 ? f : -idx, idx % 12 === 10 ? f : idx, idx % 12 === 11 ? f : -idx],
]),
4: kInterestingF16Values.map((f, idx) => [
[
idx % 16 === 0 ? f : idx,
idx % 16 === 1 ? f : -idx,
idx % 16 === 2 ? f : idx,
idx % 16 === 3 ? f : -idx,
],
[
idx % 16 === 4 ? f : -idx,
idx % 16 === 5 ? f : idx,
idx % 16 === 6 ? f : -idx,
idx % 16 === 7 ? f : idx,
],
[
idx % 16 === 8 ? f : idx,
idx % 16 === 9 ? f : -idx,
idx % 16 === 10 ? f : idx,
idx % 16 === 11 ? f : -idx,
],
[
idx % 16 === 12 ? f : -idx,
idx % 16 === 13 ? f : idx,
idx % 16 === 14 ? f : -idx,
idx % 16 === 15 ? f : idx,
],
]),
},
};
/**
* Returns a minimal set of matrices, indexed by dimension containing interesting
* f16 values.
*
* This is the matrix analogue of `sparseVectorF16Range`, so it is producing a
* minimal coverage set of matrices that test all of the interesting f16 values.
* There is not a more expansive set of matrices, since matrices are even more
* expensive than vectors for increasing runtime with coverage.
*
* All of the interesting floats from sparseScalarF16 are guaranteed to be tested, but
* not in every position.
*/
export function sparseMatrixF16Range(c: number, r: number): ROArrayArray<number>[] {
assert(
c === 2 || c === 3 || c === 4,
'sparseMatrixF16Range only accepts column counts of 2, 3, and 4'
);
assert(
r === 2 || r === 3 || r === 4,
'sparseMatrixF16Range only accepts row counts of 2, 3, and 4'
);
return kSparseMatrixF16Values[c][r];
}
/** Short list of f64 values of interest to test against */
const kInterestingF64Values: readonly number[] = [
kValue.f64.negative.min,
-10.0,
-1.0,
-0.125,
kValue.f64.negative.max,
kValue.f64.negative.subnormal.min,
kValue.f64.negative.subnormal.max,
-0.0,
0.0,
kValue.f64.positive.subnormal.min,
kValue.f64.positive.subnormal.max,
kValue.f64.positive.min,
0.125,
1.0,
10.0,
kValue.f64.positive.max,
];
/** @returns minimal F64 values that cover the entire range of F64 behaviours
*
* Has specially selected values that cover edge cases, normals, and subnormals.
* This is used instead of fullF64Range when the number of test cases being
* generated is a super linear function of the length of F64 values which is
* leading to time outs.
*
* These values have been chosen to attempt to test the widest range of F64
* behaviours in the lowest number of entries, so may potentially miss function
* specific values of interest. If there are known values of interest they
* should be appended to this list in the test generation code.
*/
export function sparseScalarF64Range(): readonly number[] {
return kInterestingF64Values;
}
const kVectorF64Values = {
2: kInterestingF64Values.flatMap(f => [
[f, 1.0],
[-1.0, f],
]),
3: kInterestingF64Values.flatMap(f => [
[f, 1.0, -2.0],
[-1.0, f, 2.0],
[1.0, -2.0, f],
]),
4: kInterestingF64Values.flatMap(f => [
[f, -1.0, 2.0, 3.0],
[1.0, f, -2.0, 3.0],
[1.0, 2.0, f, -3.0],
[-1.0, 2.0, -3.0, f],
]),
};
/**
* Returns set of vectors, indexed by dimension containing interesting float
* values.
*
* The tests do not do the simple option for coverage of computing the cartesian
* product of all of the interesting float values N times for vecN tests,
* because that creates a huge number of tests for vec3 and vec4, leading to
* time outs.
*
* Instead they insert the interesting F64 values into each location of the
* vector to get a spread of testing over the entire range. This reduces the
* number of cases being run substantially, but maintains coverage.
*/
export function vectorF64Range(dim: number): ROArrayArray<number> {
assert(dim === 2 || dim === 3 || dim === 4, 'vectorF64Range only accepts dimensions 2, 3, and 4');
return kVectorF64Values[dim];
}
const kSparseVectorF64Values = {
2: sparseScalarF64Range().map((f, idx) => [idx % 2 === 0 ? f : idx, idx % 2 === 1 ? f : -idx]),
3: sparseScalarF64Range().map((f, idx) => [
idx % 3 === 0 ? f : idx,
idx % 3 === 1 ? f : -idx,
idx % 3 === 2 ? f : idx,
]),
4: sparseScalarF64Range().map((f, idx) => [
idx % 4 === 0 ? f : idx,
idx % 4 === 1 ? f : -idx,
idx % 4 === 2 ? f : idx,
idx % 4 === 3 ? f : -idx,
]),
};
/**
* Minimal set of vectors, indexed by dimension, that contain interesting f64
* values.
*
* This is an even more stripped down version of `vectorF64Range` for when
* pairs of vectors are being tested.
* All the interesting floats from sparseScalarF64 are guaranteed to be tested, but
* not in every position.
*/
export function sparseVectorF64Range(dim: number): ROArrayArray<number> {
assert(
dim === 2 || dim === 3 || dim === 4,
'sparseVectorF64Range only accepts dimensions 2, 3, and 4'
);
return kSparseVectorF64Values[dim];
}
const kSparseMatrixF64Values = {
2: {
2: kInterestingF64Values.map((f, idx) => [
[idx % 4 === 0 ? f : idx, idx % 4 === 1 ? f : -idx],
[idx % 4 === 2 ? f : -idx, idx % 4 === 3 ? f : idx],
]),
3: kInterestingF64Values.map((f, idx) => [
[idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx, idx % 6 === 2 ? f : idx],
[idx % 6 === 3 ? f : -idx, idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
]),
4: kInterestingF64Values.map((f, idx) => [
[
idx % 8 === 0 ? f : idx,
idx % 8 === 1 ? f : -idx,
idx % 8 === 2 ? f : idx,
idx % 8 === 3 ? f : -idx,
],
[
idx % 8 === 4 ? f : -idx,
idx % 8 === 5 ? f : idx,
idx % 8 === 6 ? f : -idx,
idx % 8 === 7 ? f : idx,
],
]),
},
3: {
2: kInterestingF64Values.map((f, idx) => [
[idx % 6 === 0 ? f : idx, idx % 6 === 1 ? f : -idx],
[idx % 6 === 2 ? f : -idx, idx % 6 === 3 ? f : idx],
[idx % 6 === 4 ? f : idx, idx % 6 === 5 ? f : -idx],
]),
3: kInterestingF64Values.map((f, idx) => [
[idx % 9 === 0 ? f : idx, idx % 9 === 1 ? f : -idx, idx % 9 === 2 ? f : idx],
[idx % 9 === 3 ? f : -idx, idx % 9 === 4 ? f : idx, idx % 9 === 5 ? f : -idx],
[idx % 9 === 6 ? f : idx, idx % 9 === 7 ? f : -idx, idx % 9 === 8 ? f : idx],
]),
4: kInterestingF64Values.map((f, idx) => [
[
idx % 12 === 0 ? f : idx,
idx % 12 === 1 ? f : -idx,
idx % 12 === 2 ? f : idx,
idx % 12 === 3 ? f : -idx,
],
[
idx % 12 === 4 ? f : -idx,
idx % 12 === 5 ? f : idx,
idx % 12 === 6 ? f : -idx,
idx % 12 === 7 ? f : idx,
],
[
idx % 12 === 8 ? f : idx,
idx % 12 === 9 ? f : -idx,
idx % 12 === 10 ? f : idx,
idx % 12 === 11 ? f : -idx,
],
]),
},
4: {
2: kInterestingF64Values.map((f, idx) => [
[idx % 8 === 0 ? f : idx, idx % 8 === 1 ? f : -idx],
[idx % 8 === 2 ? f : -idx, idx % 8 === 3 ? f : idx],
[idx % 8 === 4 ? f : idx, idx % 8 === 5 ? f : -idx],
[idx % 8 === 6 ? f : -idx, idx % 8 === 7 ? f : idx],
]),
3: kInterestingF64Values.map((f, idx) => [
[idx % 12 === 0 ? f : idx, idx % 12 === 1 ? f : -idx, idx % 12 === 2 ? f : idx],
[idx % 12 === 3 ? f : -idx, idx % 12 === 4 ? f : idx, idx % 12 === 5 ? f : -idx],
[idx % 12 === 6 ? f : idx, idx % 12 === 7 ? f : -idx, idx % 12 === 8 ? f : idx],
[idx % 12 === 9 ? f : -idx, idx % 12 === 10 ? f : idx, idx % 12 === 11 ? f : -idx],
]),
4: kInterestingF64Values.map((f, idx) => [
[
idx % 16 === 0 ? f : idx,
idx % 16 === 1 ? f : -idx,
idx % 16 === 2 ? f : idx,
idx % 16 === 3 ? f : -idx,
],
[
idx % 16 === 4 ? f : -idx,
idx % 16 === 5 ? f : idx,
idx % 16 === 6 ? f : -idx,
idx % 16 === 7 ? f : idx,
],
[
idx % 16 === 8 ? f : idx,
idx % 16 === 9 ? f : -idx,
idx % 16 === 10 ? f : idx,
idx % 16 === 11 ? f : -idx,
],
[
idx % 16 === 12 ? f : -idx,
idx % 16 === 13 ? f : idx,
idx % 16 === 14 ? f : -idx,
idx % 16 === 15 ? f : idx,
],
]),
},
};
/**
* Returns a minimal set of matrices, indexed by dimension containing interesting
* float values.
*
* This is the matrix analogue of `sparseVectorF64Range`, so it is producing a
* minimal coverage set of matrices that test all the interesting f64 values.
* There is not a more expansive set of matrices, since matrices are even more
* expensive than vectors for increasing runtime with coverage.
*
* All the interesting floats from sparseScalarF64 are guaranteed to be tested, but
* not in every position.
*/
export function sparseMatrixF64Range(cols: number, rows: number): ROArrayArrayArray<number> {
assert(
cols === 2 || cols === 3 || cols === 4,
'sparseMatrixF64Range only accepts column counts of 2, 3, and 4'
);
assert(
rows === 2 || rows === 3 || rows === 4,
'sparseMatrixF64Range only accepts row counts of 2, 3, and 4'
);
return kSparseMatrixF64Values[cols][rows];
}
/**
* @returns the result matrix in Array<Array<number>> type.
*
* Matrix multiplication. A is m x n and B is n x p. Returns
* m x p result.
*/
// A is m x n. B is n x p. product is m x p.
export function multiplyMatrices(
A: Array<Array<number>>,
B: Array<Array<number>>
): Array<Array<number>> {
assert(A.length > 0 && B.length > 0 && B[0].length > 0 && A[0].length === B.length);
const product = new Array<Array<number>>(A.length);
for (let i = 0; i < product.length; ++i) {
product[i] = new Array<number>(B[0].length).fill(0);
}
for (let m = 0; m < A.length; ++m) {
for (let p = 0; p < B[0].length; ++p) {
for (let n = 0; n < B.length; ++n) {
product[m][p] += A[m][n] * B[n][p];
}
}
}
return product;
}
/** Sign-extend the `bits`-bit number `n` to a 32-bit signed integer. */
export function signExtend(n: number, bits: number): number {
const shift = 32 - bits;
return (n << shift) >> shift;
}
export interface QuantizeFunc<T> {
(num: T): T;
}
/** @returns the closest 32-bit floating point value to the input */
export function quantizeToF32(num: number): number {
return Math.fround(num);
}
/** @returns the closest 16-bit floating point value to the input */
export function quantizeToF16(num: number): number {
return hfround(num);
}
/**
* @returns the closest 32-bit signed integer value to the input, rounding
* towards 0, if not already an integer
*/
export function quantizeToI32(num: number): number {
if (num >= kValue.i32.positive.max) {
return kValue.i32.positive.max;
}
if (num <= kValue.i32.negative.min) {
return kValue.i32.negative.min;
}
return Math.trunc(num);
}
/**
* @returns the closest 32-bit unsigned integer value to the input, rounding
* towards 0, if not already an integer
*/
export function quantizeToU32(num: number): number {
if (num >= kValue.u32.max) {
return kValue.u32.max;
}
if (num <= 0) {
return 0;
}
return Math.trunc(num);
}
/**
* @returns the closest 64-bit signed integer value to the input.
*/
export function quantizeToI64(num: bigint): bigint {
if (num >= kValue.i64.positive.max) {
return kValue.i64.positive.max;
}
if (num <= kValue.i64.negative.min) {
return kValue.i64.negative.min;
}
return num;
}
/** @returns whether the number is an integer and a power of two */
export function isPowerOfTwo(n: number): boolean {
if (!Number.isInteger(n)) {
return false;
}
assert((n | 0) === n, 'isPowerOfTwo only supports 32-bit numbers');
return n !== 0 && (n & (n - 1)) === 0;
}
/** @returns the Greatest Common Divisor (GCD) of the inputs */
export function gcd(a: number, b: number): number {
assert(Number.isInteger(a) && a > 0);
assert(Number.isInteger(b) && b > 0);
while (b !== 0) {
const bTemp = b;
b = a % b;
a = bTemp;
}
return a;
}
/** @returns the Least Common Multiplier (LCM) of the inputs */
export function lcm(a: number, b: number): number {
return (a * b) / gcd(a, b);
}
/** @returns the cross of an array with the intermediate result of cartesianProduct
*
* @param elements array of values to cross with the intermediate result of
* cartesianProduct
* @param intermediate arrays of values representing the partial result of
* cartesianProduct
*/
function cartesianProductImpl<T>(
elements: readonly T[],
intermediate: ROArrayArray<T>
): ROArrayArray<T> {
const result: T[][] = [];
elements.forEach((e: T) => {
if (intermediate.length > 0) {
intermediate.forEach((i: readonly T[]) => {
result.push([...i, e]);
});
} else {
result.push([e]);
}
});
return result;
}
/** @returns the cartesian product (NxMx...) of a set of arrays
*
* This is implemented by calculating the cross of a single input against an
* intermediate result for each input to build up the final array of arrays.
*
* There are examples of doing this more succinctly using map & reduce online,
* but they are a bit more opaque to read.
*
* @param inputs arrays of numbers to calculate cartesian product over
*/
export function cartesianProduct<T>(...inputs: ROArrayArray<T>): ROArrayArray<T> {
let result: ROArrayArray<T> = [];
inputs.forEach((i: readonly T[]) => {
result = cartesianProductImpl<T>(i, result);
});
return result;
}
/** @returns all of the permutations of an array
*
* Recursively calculates all of the permutations, does not cull duplicate
* entries.
*
* Only feasible for inputs of lengths 5 or so, since the number of permutations
* is (input.length)!, so will cause the stack to explode for longer inputs.
*
* This code could be made iterative using something like
* Steinhaus–Johnson–Trotter and additionally turned into a generator to reduce
* the stack size, but there is still a fundamental combinatorial explosion
* here that will affect runtime.
*
* @param input the array to get permutations of
*/
export function calculatePermutations<T>(input: readonly T[]): ROArrayArray<T> {
if (input.length === 0) {
return [];
}
if (input.length === 1) {
return [input];
}
if (input.length === 2) {
return [input, [input[1], input[0]]];
}
const result: T[][] = [];
input.forEach((head, idx) => {
const tail = input.slice(0, idx).concat(input.slice(idx + 1));
const permutations = calculatePermutations(tail);
permutations.forEach(p => {
result.push([head, ...p]);
});
});
return result;
}
/**
* Convert an Array of Arrays to linear array
*
* Caller is responsible to retaining the dimensions of the array for later
* unflattening
*
* @param m Matrix to convert
*/
export function flatten2DArray<T>(m: ROArrayArray<T>): T[] {
const c = m.length;
const r = m[0].length;
assert(
m.every(c => c.length === r),
`Unexpectedly received jagged array to flatten`
);
const result: T[] = Array<T>(c * r);
for (let i = 0; i < c; i++) {
for (let j = 0; j < r; j++) {
result[j + i * r] = m[i][j];
}
}
return result;
}
/**
* Convert linear array to an Array of Arrays
* @param n an array to convert
* @param c number of elements in the array containing arrays
* @param r number of elements in the arrays that are contained
*/
export function unflatten2DArray<T>(n: readonly T[], c: number, r: number): ROArrayArray<T> {
assert(
c > 0 && Number.isInteger(c) && r > 0 && Number.isInteger(r),
`columns (${c}) and rows (${r}) need to be positive integers`
);
assert(n.length === c * r, `m.length(${n.length}) should equal c * r (${c * r})`);
const result: T[][] = [...Array(c)].map(_ => [...Array(r)]);
for (let i = 0; i < c; i++) {
for (let j = 0; j < r; j++) {
result[i][j] = n[j + i * r];
}
}
return result;
}
/**
* Performs a .map over a matrix and return the result
* The shape of the input and output matrices will be the same
*
* @param m input matrix of type T
* @param op operation that converts an element of type T to one of type S
* @returns a matrix with elements of type S that are calculated by applying op element by element
*/
export function map2DArray<T, S>(m: ROArrayArray<T>, op: (input: T) => S): ROArrayArray<S> {
const c = m.length;
const r = m[0].length;
assert(
m.every(c => c.length === r),
`Unexpectedly received jagged array to map`
);
const result: S[][] = [...Array(c)].map(_ => [...Array(r)]);
for (let i = 0; i < c; i++) {
for (let j = 0; j < r; j++) {
result[i][j] = op(m[i][j]);
}
}
return result;
}
/**
* Performs a .every over a matrix and return the result
*
* @param m input matrix of type T
* @param op operation that performs a test on an element
* @returns a boolean indicating if the test passed for every element
*/
export function every2DArray<T>(m: ROArrayArray<T>, op: (input: T) => boolean): boolean {
const r = m[0].length;
assert(
m.every(c => c.length === r),
`Unexpectedly received jagged array to map`
);
return m.every(col => col.every(el => op(el)));
}
/**
* Subtracts 2 vectors
*/
export function subtractVectors(v1: readonly number[], v2: readonly number[]) {
return v1.map((v, i) => v - v2[i]);
}
/**
* Computes the dot product of 2 vectors
*/
export function dotProduct(v1: readonly number[], v2: readonly number[]) {
return v1.reduce((a, v, i) => a + v * v2[i], 0);
}
/** @returns the absolute value of a bigint */
export function absBigInt(v: bigint): bigint {
return v < 0n ? -v : v;
}
/** @returns the maximum from a list of bigints */
export function maxBigInt(...vals: bigint[]): bigint {
return vals.reduce((prev, cur) => (cur > prev ? cur : prev));
}
/** @returns the minimum from a list of bigints */
export function minBigInt(...vals: bigint[]): bigint {
return vals.reduce((prev, cur) => (cur < prev ? cur : prev));
}