blob: 1f2c3a94205ff821effdc4a007f759c02ef54bab [file] [log] [blame]
import { ROArrayArray, ROArrayArrayArray } from '../../common/util/types.js';
import { assert, unreachable } from '../../common/util/util.js';
import { Float16Array } from '../../external/petamoriken/float16/float16.js';
import { Case } from '../shader/execution/expression/case.js';
import { IntervalFilter } from '../shader/execution/expression/interval_filter.js';
import BinaryStream from './binary_stream.js';
import { anyOf } from './compare.js';
import { kValue } from './constants.js';
import {
abstractFloat,
f16,
f32,
isFloatType,
ScalarValue,
ScalarType,
toMatrix,
toVector,
u32,
} from './conversion.js';
import {
calculatePermutations,
cartesianProduct,
correctlyRoundedF16,
correctlyRoundedF32,
correctlyRoundedF64,
every2DArray,
flatten2DArray,
FlushMode,
flushSubnormalNumberF16,
flushSubnormalNumberF32,
flushSubnormalNumberF64,
isFiniteF16,
isFiniteF32,
isSubnormalNumberF16,
isSubnormalNumberF32,
isSubnormalNumberF64,
map2DArray,
oneULPF16,
oneULPF32,
nextAfterF64,
quantizeToF16,
quantizeToF32,
scalarF16Range,
scalarF32Range,
scalarF64Range,
sparseMatrixF16Range,
sparseMatrixF32Range,
sparseMatrixF64Range,
sparseScalarF16Range,
sparseScalarF32Range,
sparseScalarF64Range,
sparseVectorF16Range,
sparseVectorF32Range,
sparseVectorF64Range,
unflatten2DArray,
vectorF16Range,
vectorF32Range,
vectorF64Range,
} from './math.js';
/** Indicate the kind of WGSL floating point numbers being operated on */
export type FPKind = 'f32' | 'f16' | 'abstract';
enum SerializedFPIntervalKind {
Abstract,
F32,
F16,
}
/** serializeFPKind() serializes a FPKind to a BinaryStream */
export function serializeFPKind(s: BinaryStream, value: FPKind) {
switch (value) {
case 'abstract':
s.writeU8(SerializedFPIntervalKind.Abstract);
break;
case 'f16':
s.writeU8(SerializedFPIntervalKind.F16);
break;
case 'f32':
s.writeU8(SerializedFPIntervalKind.F32);
break;
}
}
/** deserializeFPKind() deserializes a FPKind from a BinaryStream */
export function deserializeFPKind(s: BinaryStream): FPKind {
const kind = s.readU8();
switch (kind) {
case SerializedFPIntervalKind.Abstract:
return 'abstract';
case SerializedFPIntervalKind.F16:
return 'f16';
case SerializedFPIntervalKind.F32:
return 'f32';
default:
unreachable(`invalid deserialized FPKind: ${kind}`);
}
}
// Containers
/**
* Representation of endpoints for an interval as an array with either one or
* two elements. Single element indicates that the interval is a single point.
* For two elements, the first is the lower edges of the interval and the
* second is the upper edge, i.e. e[0] <= e[1], where e is an IntervalEndpoints
*/
export type IntervalEndpoints = readonly [number] | readonly [number, number];
/** Represents a closed interval of floating point numbers */
export class FPInterval {
public readonly kind: FPKind;
public readonly begin: number;
public readonly end: number;
/**
* Constructor
*
* `FPTraits.toInterval` is the preferred way to create FPIntervals
*
* @param kind the floating point number type this is an interval for
* @param endpoints beginning and end of the interval
*/
public constructor(kind: FPKind, ...endpoints: IntervalEndpoints) {
this.kind = kind;
const begin = endpoints[0];
const end = endpoints.length === 2 ? endpoints[1] : endpoints[0];
assert(!Number.isNaN(begin) && !Number.isNaN(end), `endpoints need to be non-NaN`);
assert(
begin <= end,
`endpoints[0] (${begin}) must be less than or equal to endpoints[1] (${end})`
);
this.begin = begin;
this.end = end;
}
/** @returns the floating point traits for this interval */
public traits(): FPTraits {
return FP[this.kind];
}
/** @returns begin and end if non-point interval, otherwise just begin */
public endpoints(): IntervalEndpoints {
return this.isPoint() ? [this.begin] : [this.begin, this.end];
}
/** @returns if a point or interval is completely contained by this interval */
public contains(n: number | FPInterval): boolean {
if (Number.isNaN(n)) {
// Being the 'any' interval indicates that accuracy is not defined for this
// test, so the test is just checking that this input doesn't cause the
// implementation to misbehave, so NaN is accepted.
return this.begin === Number.NEGATIVE_INFINITY && this.end === Number.POSITIVE_INFINITY;
}
if (n instanceof FPInterval) {
return this.begin <= n.begin && this.end >= n.end;
}
return this.begin <= n && this.end >= n;
}
/** @returns if any values in the interval may be flushed to zero, this
* includes any subnormals and zero itself.
*/
public containsZeroOrSubnormals(): boolean {
return !(
this.end < this.traits().constants().negative.subnormal.min ||
this.begin > this.traits().constants().positive.subnormal.max
);
}
/** @returns if this interval contains a single point */
public isPoint(): boolean {
return this.begin === this.end;
}
/** @returns if this interval only contains finite values */
public isFinite(): boolean {
return this.traits().isFinite(this.begin) && this.traits().isFinite(this.end);
}
/** @returns a string representation for logging purposes */
public toString(): string {
return `{ '${this.kind}', [${this.endpoints().map(this.traits().scalarBuilder)}] }`;
}
}
/** serializeFPInterval() serializes a FPInterval to a BinaryStream */
export function serializeFPInterval(s: BinaryStream, i: FPInterval) {
serializeFPKind(s, i.kind);
const traits = FP[i.kind];
s.writeCond(i !== traits.constants().unboundedInterval, {
if_true: () => {
// Bounded
switch (i.kind) {
case 'abstract':
s.writeF64(i.begin);
s.writeF64(i.end);
break;
case 'f32':
s.writeF32(i.begin);
s.writeF32(i.end);
break;
case 'f16':
s.writeF16(i.begin);
s.writeF16(i.end);
break;
default:
unreachable(`Unable to serialize FPInterval ${i}`);
break;
}
},
if_false: () => {
// Unbounded
},
});
}
/** deserializeFPInterval() deserializes a FPInterval from a BinaryStream */
export function deserializeFPInterval(s: BinaryStream): FPInterval {
const kind = deserializeFPKind(s);
const traits = FP[kind];
return s.readCond({
if_true: () => {
// Bounded
switch (kind) {
case 'abstract':
return new FPInterval(traits.kind, s.readF64(), s.readF64());
case 'f32':
return new FPInterval(traits.kind, s.readF32(), s.readF32());
case 'f16':
return new FPInterval(traits.kind, s.readF16(), s.readF16());
}
unreachable(`Unable to deserialize FPInterval with kind ${kind}`);
},
if_false: () => {
// Unbounded
return traits.constants().unboundedInterval;
},
});
}
/**
* Representation of a vec2/3/4 of floating point intervals as an array of
* FPIntervals.
*/
export type FPVector =
| [FPInterval, FPInterval]
| [FPInterval, FPInterval, FPInterval]
| [FPInterval, FPInterval, FPInterval, FPInterval];
/** Shorthand for an Array of Arrays that contains a column-major matrix */
type Array2D<T> = ROArrayArray<T>;
/**
* Representation of a matCxR of floating point intervals as an array of arrays
* of FPIntervals. This maps onto the WGSL concept of matrix. Internally
*/
export type FPMatrix =
| readonly [readonly [FPInterval, FPInterval], readonly [FPInterval, FPInterval]]
| readonly [
readonly [FPInterval, FPInterval],
readonly [FPInterval, FPInterval],
readonly [FPInterval, FPInterval],
]
| readonly [
readonly [FPInterval, FPInterval],
readonly [FPInterval, FPInterval],
readonly [FPInterval, FPInterval],
readonly [FPInterval, FPInterval],
]
| readonly [
readonly [FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval],
]
| readonly [
readonly [FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval],
]
| readonly [
readonly [FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval],
]
| readonly [
readonly [FPInterval, FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval, FPInterval],
]
| readonly [
readonly [FPInterval, FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval, FPInterval],
]
| readonly [
readonly [FPInterval, FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval, FPInterval],
readonly [FPInterval, FPInterval, FPInterval, FPInterval],
];
// Utilities
/** @returns input with an appended 0, if inputs contains non-zero subnormals */
// When f16 traits is defined, this can be replaced with something like
// `FP.f16..addFlushIfNeeded`
function addFlushedIfNeededF16(values: readonly number[]): readonly number[] {
return values.some(v => v !== 0 && isSubnormalNumberF16(v)) ? values.concat(0) : values;
}
// Operations
/**
* A function that converts a point to an acceptance interval.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface ScalarToInterval {
(x: number): FPInterval;
}
/** Operation used to implement a ScalarToInterval */
interface ScalarToIntervalOp {
/** @returns acceptance interval for a function at point x */
impl: ScalarToInterval;
/**
* Calculates where in the domain defined by x the min/max extrema of impl
* occur and returns a span of those points to be used as the domain instead.
*
* Used by this.runScalarToIntervalOp before invoking impl.
* If not defined, the endpoints of the existing domain are assumed to be the
* extrema.
*
* This is only implemented for operations that meet all the following
* criteria:
* a) non-monotonic
* b) used in inherited accuracy calculations
* c) need to take in an interval for b)
* i.e. fooInterval takes in x: number | FPInterval, not x: number
*/
extrema?: (x: FPInterval) => FPInterval;
/**
* Restricts the inputs to operation to the given domain.
*
* Only defined for operations that have tighter domain requirements than 'must
* be finite'.
*/
domain?: () => FPInterval;
}
/**
* A function that converts a pair of points to an acceptance interval.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface ScalarPairToInterval {
(x: number, y: number): FPInterval;
}
/** Domain for a ScalarPairToInterval implementation */
interface ScalarPairToIntervalDomain {
// Arrays to support discrete valid domain intervals
x: readonly FPInterval[];
y: readonly FPInterval[];
}
/** Operation used to implement a ScalarPairToInterval */
interface ScalarPairToIntervalOp {
/** @returns acceptance interval for a function at point (x, y) */
impl: ScalarPairToInterval;
/**
* Calculates where in domain defined by x & y the min/max extrema of impl
* occur and returns spans of those points to be used as the domain instead.
*
* Used by runScalarPairToIntervalOp before invoking impl.
* If not defined, the endpoints of the existing domain are assumed to be the
* extrema.
*
* This is only implemented for functions that meet all the following
* criteria:
* a) non-monotonic
* b) used in inherited accuracy calculations
* c) need to take in an interval for b)
*/
extrema?: (x: FPInterval, y: FPInterval) => [FPInterval, FPInterval];
/**
* Restricts the inputs to operation to the given domain.
*
* Only defined for operations that have tighter domain requirements than 'must
* be finite'.
*/
domain?: () => ScalarPairToIntervalDomain;
}
/**
* A function that converts a triplet of points to an acceptance interval.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface ScalarTripleToInterval {
(x: number, y: number, z: number): FPInterval;
}
/** Operation used to implement a ScalarTripleToInterval */
interface ScalarTripleToIntervalOp {
// Re-using the *Op interface pattern for symmetry with the other operations.
/** @returns acceptance interval for a function at point (x, y, z) */
impl: ScalarTripleToInterval;
}
// Currently ScalarToVector is not integrated with the rest of the floating point
// framework, because the only builtins that use it are actually
// u32 -> [f32, f32, f32, f32] functions, so the whole rounding and interval
// process doesn't get applied to the inputs.
// They do use the framework internally by invoking divisionInterval on segments
// of the input.
/**
* A function that converts a point to a vector of acceptance intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface ScalarToVector {
(n: number): FPVector;
}
/**
* A function that converts a vector to an acceptance interval.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface VectorToInterval {
(x: readonly number[]): FPInterval;
}
/** Operation used to implement a VectorToInterval */
interface VectorToIntervalOp {
// Re-using the *Op interface pattern for symmetry with the other operations.
/** @returns acceptance interval for a function on vector x */
impl: VectorToInterval;
}
/**
* A function that converts a pair of vectors to an acceptance interval.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface VectorPairToInterval {
(x: readonly number[], y: readonly number[]): FPInterval;
}
/** Operation used to implement a VectorPairToInterval */
interface VectorPairToIntervalOp {
// Re-using the *Op interface pattern for symmetry with the other operations.
/** @returns acceptance interval for a function on vectors (x, y) */
impl: VectorPairToInterval;
}
/**
* A function that converts a vector to a vector of acceptance intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface VectorToVector {
(x: readonly number[]): FPVector;
}
/** Operation used to implement a VectorToVector */
interface VectorToVectorOp {
// Re-using the *Op interface pattern for symmetry with the other operations.
/** @returns a vector of acceptance intervals for a function on vector x */
impl: VectorToVector;
}
/**
* A function that converts a pair of vectors to a vector of acceptance
* intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface VectorPairToVector {
(x: readonly number[], y: readonly number[]): FPVector;
}
/** Operation used to implement a VectorPairToVector */
interface VectorPairToVectorOp {
// Re-using the *Op interface pattern for symmetry with the other operations.
/** @returns a vector of acceptance intervals for a function on vectors (x, y) */
impl: VectorPairToVector;
}
/**
* A function that converts a vector and a scalar to a vector of acceptance
* intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface VectorScalarToVector {
(x: readonly number[], y: number): FPVector;
}
/**
* A function that converts a scalar and a vector to a vector of acceptance
* intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface ScalarVectorToVector {
(x: number, y: readonly number[]): FPVector;
}
/**
* A function that converts a matrix to an acceptance interval.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface MatrixToScalar {
(m: Array2D<number>): FPInterval;
}
/** Operation used to implement a MatrixToMatrix */
interface MatrixToMatrixOp {
// Re-using the *Op interface pattern for symmetry with the other operations.
/** @returns a matrix of acceptance intervals for a function on matrix x */
impl: MatrixToMatrix;
}
/**
* A function that converts a matrix to a matrix of acceptance intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface MatrixToMatrix {
(m: Array2D<number>): FPMatrix;
}
/**
* A function that converts a pair of matrices to a matrix of acceptance
* intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface MatrixPairToMatrix {
(x: Array2D<number>, y: Array2D<number>): FPMatrix;
}
/**
* A function that converts a matrix and a scalar to a matrix of acceptance
* intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface MatrixScalarToMatrix {
(x: Array2D<number>, y: number): FPMatrix;
}
/**
* A function that converts a scalar and a matrix to a matrix of acceptance
* intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface ScalarMatrixToMatrix {
(x: number, y: Array2D<number>): FPMatrix;
}
/**
* A function that converts a matrix and a vector to a vector of acceptance
* intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface MatrixVectorToVector {
(x: Array2D<number>, y: readonly number[]): FPVector;
}
/**
* A function that converts a vector and a matrix to a vector of acceptance
* intervals.
* This is the public facing API for builtin implementations that is called
* from tests.
*/
export interface VectorMatrixToVector {
(x: readonly number[], y: Array2D<number>): FPVector;
}
// Traits
/**
* Typed structure containing all the constants defined for each
* WGSL floating point kind
*/
interface FPConstants {
positive: {
min: number;
max: number;
infinity: number;
nearest_max: number;
less_than_one: number;
subnormal: {
min: number;
max: number;
};
pi: {
whole: number;
three_quarters: number;
half: number;
third: number;
quarter: number;
sixth: number;
};
e: number;
};
negative: {
min: number;
max: number;
infinity: number;
nearest_min: number;
less_than_one: number;
subnormal: {
min: number;
max: number;
};
pi: {
whole: number;
three_quarters: number;
half: number;
third: number;
quarter: number;
sixth: number;
};
};
bias: number;
unboundedInterval: FPInterval;
zeroInterval: FPInterval;
negPiToPiInterval: FPInterval;
greaterThanZeroInterval: FPInterval;
negOneToOneInterval: FPInterval;
zeroVector: {
2: FPVector;
3: FPVector;
4: FPVector;
};
unboundedVector: {
2: FPVector;
3: FPVector;
4: FPVector;
};
unboundedMatrix: {
2: {
2: FPMatrix;
3: FPMatrix;
4: FPMatrix;
};
3: {
2: FPMatrix;
3: FPMatrix;
4: FPMatrix;
};
4: {
2: FPMatrix;
3: FPMatrix;
4: FPMatrix;
};
};
}
/** A representation of an FPInterval for a case param */
export type FPIntervalParam = {
kind: FPKind;
interval: number | IntervalEndpoints;
};
/** Abstract base class for all floating-point traits */
export abstract class FPTraits {
public readonly kind: FPKind;
protected constructor(k: FPKind) {
this.kind = k;
}
public abstract constants(): FPConstants;
// Utilities - Implemented
/** @returns an interval containing the point or the original interval */
public toInterval(n: number | IntervalEndpoints | FPInterval): FPInterval {
if (n instanceof FPInterval) {
if (n.kind === this.kind) {
return n;
}
// Preserve if the original interval was unbounded or bounded
if (!n.isFinite()) {
return this.constants().unboundedInterval;
}
return new FPInterval(this.kind, ...n.endpoints());
}
if (n instanceof Array) {
return new FPInterval(this.kind, ...n);
}
return new FPInterval(this.kind, n, n);
}
/**
* WGSL specifies unbounded precision:
* https://www.w3.org/TR/WGSL/#floating-point-accuracy
* In most computations doubles (number in js) are equivalent
* to correctly rounded intervals. However in some cases
* (addition/subtraction) doubles do not provide enough numeric precision.
*
* These cases are whenever a small magnitude number, y
* (e.g. smallest positive normal) is added (or subtracted) from a much
* larger magnitude number x (1.0), such that the difference between them
* is smaller then ULP(x). When working in JS numbers the result will,
* incorrectly, simply be x, since it will get rounded. JS rounds to even
* but WGSL allows for rounding up and down (which in our context will be an interval).
* We must detect these cases where double precision does not represent
* infinitely accurate computations accurately then we must manually create the interval.
*
* @param sum the result of adding large_val + small_val, using Number
* @param large_val: the summand with larger magnitude
* @param small_val: the summand with smaller magnitude
* @returns an interval containing the correctly rounded val with respect to unbounded precision
*/
public correctlyRoundedIntervalWithUnboundedPrecisionForAddition(
val: number,
large_val: number,
small_val: number
): FPInterval {
if (val === large_val && !(small_val === 0.0)) {
if (Math.sign(small_val) >= 0) {
return this.correctlyRoundedInterval(
this.toInterval([large_val, nextAfterF64(large_val, 'positive', 'flush')])
);
} else {
return this.correctlyRoundedInterval(
this.toInterval([nextAfterF64(large_val, 'negative', 'flush'), large_val])
);
}
}
return this.correctlyRoundedInterval(val);
}
/**
* Makes a param that can be turned into an interval
*/
public toParam(n: number | IntervalEndpoints): FPIntervalParam {
return {
kind: this.kind,
interval: n,
};
}
/**
* Converts p into an FPInterval if it is an FPIntervalPAram
*/
public fromParam(
p: number | IntervalEndpoints | FPIntervalParam
): number | IntervalEndpoints | FPInterval {
const param = p as FPIntervalParam;
if (param.interval && param.kind) {
assert(param.kind === this.kind);
return this.toInterval(param.interval);
}
return p as number | IntervalEndpoints;
}
/**
* @returns an interval with the tightest endpoints that includes all provided
* intervals
*/
public spanIntervals(...intervals: readonly FPInterval[]): FPInterval {
assert(intervals.length > 0, `span of an empty list of FPIntervals is not allowed`);
assert(
intervals.every(i => i.kind === this.kind),
`span is only defined for intervals with the same kind`
);
let begin = Number.POSITIVE_INFINITY;
let end = Number.NEGATIVE_INFINITY;
intervals.forEach(i => {
begin = Math.min(i.begin, begin);
end = Math.max(i.end, end);
});
return this.toInterval([begin, end]);
}
/** Narrow an array of values to FPVector if possible */
public isVector(v: ReadonlyArray<number | IntervalEndpoints | FPInterval>): v is FPVector {
if (v.every(e => e instanceof FPInterval && e.kind === this.kind)) {
return v.length === 2 || v.length === 3 || v.length === 4;
}
return false;
}
/** @returns an FPVector representation of an array of values if possible */
public toVector(v: ReadonlyArray<number | IntervalEndpoints | FPInterval>): FPVector {
if (this.isVector(v) && v.every(e => e.kind === this.kind)) {
return v;
}
const f = v.map(e => this.toInterval(e));
// The return of the map above is a readonly FPInterval[], which needs to be narrowed
// to FPVector, since FPVector is defined as fixed length tuples.
if (this.isVector(f)) {
return f;
}
unreachable(`Cannot convert [${v}] to FPVector`);
}
/**
* @returns a FPVector where each element is the span for corresponding
* elements at the same index in the input vectors
*/
public spanVectors(...vectors: FPVector[]): FPVector {
assert(
vectors.every(e => this.isVector(e)),
'Vector span is not defined for vectors of differing floating point kinds'
);
const vector_length = vectors[0].length;
assert(
vectors.every(e => e.length === vector_length),
`Vector span is not defined for vectors of differing lengths`
);
const result: FPInterval[] = new Array<FPInterval>(vector_length);
for (let i = 0; i < vector_length; i++) {
result[i] = this.spanIntervals(...vectors.map(v => v[i]));
}
return this.toVector(result);
}
/** Narrow an array of an array of values to FPMatrix if possible */
public isMatrix(m: Array2D<number | IntervalEndpoints | FPInterval> | FPVector[]): m is FPMatrix {
if (!m.every(c => c.every(e => e instanceof FPInterval && e.kind === this.kind))) {
return false;
}
// At this point m guaranteed to be a ROArrayArray<FPInterval>, but maybe typed as a
// FPVector[].
// Coercing the type since FPVector[] is functionally equivalent to
// ROArrayArray<FPInterval> for .length and .every, but they are type compatible,
// since tuples are not equivalent to arrays, so TS considers c in .every to
// be unresolvable below, even though our usage is safe.
m = m as ROArrayArray<FPInterval>;
if (m.length > 4 || m.length < 2) {
return false;
}
const num_rows = m[0].length;
if (num_rows > 4 || num_rows < 2) {
return false;
}
return m.every(c => c.length === num_rows);
}
/** @returns an FPMatrix representation of an array of an array of values if possible */
public toMatrix(m: Array2D<number | IntervalEndpoints | FPInterval> | FPVector[]): FPMatrix {
if (
this.isMatrix(m) &&
every2DArray(m, (e: FPInterval) => {
return e.kind === this.kind;
})
) {
return m;
}
const result = map2DArray(m, this.toInterval.bind(this));
// The return of the map above is a ROArrayArray<FPInterval>, which needs to be
// narrowed to FPMatrix, since FPMatrix is defined as fixed length tuples.
if (this.isMatrix(result)) {
return result;
}
unreachable(`Cannot convert ${m} to FPMatrix`);
}
/**
* @returns a FPMatrix where each element is the span for corresponding
* elements at the same index in the input matrices
*/
public spanMatrices(...matrices: FPMatrix[]): FPMatrix {
// Coercing the type of matrices, since tuples are not generally compatible
// with Arrays, but they are functionally equivalent for the usages in this
// function.
const ms = matrices as Array2D<FPInterval>[];
const num_cols = ms[0].length;
const num_rows = ms[0][0].length;
assert(
ms.every(m => m.length === num_cols && m.every(r => r.length === num_rows)),
`Matrix span is not defined for Matrices of differing dimensions`
);
const result: FPInterval[][] = [...Array(num_cols)].map(_ => [...Array(num_rows)]);
for (let i = 0; i < num_cols; i++) {
for (let j = 0; j < num_rows; j++) {
result[i][j] = this.spanIntervals(...ms.map(m => m[i][j]));
}
}
return this.toMatrix(result);
}
/** @returns input with an appended 0, if inputs contains non-zero subnormals */
public addFlushedIfNeeded(values: readonly number[]): readonly number[] {
const subnormals = values.filter(this.isSubnormal);
const needs_zero = subnormals.length > 0 && subnormals.every(s => s !== 0);
return needs_zero ? values.concat(0) : values;
}
/** Stub for scalar to interval generator */
protected unimplementedScalarToInterval(name: string, _x: number | FPInterval): FPInterval {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for scalar pair to interval generator */
protected unimplementedScalarPairToInterval(
name: string,
_x: number | FPInterval,
_y: number | FPInterval
): FPInterval {
unreachable(`'${name}' is yet implemented for '${this.kind}'`);
}
/** Stub for scalar triple to interval generator */
protected unimplementedScalarTripleToInterval(
name: string,
_x: number | FPInterval,
_y: number | FPInterval,
_z: number | FPInterval
): FPInterval {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for scalar to vector generator */
protected unimplementedScalarToVector(name: string, _x: number | FPInterval): FPVector {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for vector to interval generator */
protected unimplementedVectorToInterval(name: string, _x: (number | FPInterval)[]): FPInterval {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for vector pair to interval generator */
protected unimplementedVectorPairToInterval(
name: string,
_x: readonly (number | FPInterval)[],
_y: readonly (number | FPInterval)[]
): FPInterval {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for vector to vector generator */
protected unimplementedVectorToVector(
name: string,
_x: readonly (number | FPInterval)[]
): FPVector {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for vector pair to vector generator */
protected unimplementedVectorPairToVector(
name: string,
_x: readonly (number | FPInterval)[],
_y: readonly (number | FPInterval)[]
): FPVector {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for vector-scalar to vector generator */
protected unimplementedVectorScalarToVector(
name: string,
_x: readonly (number | FPInterval)[],
_y: number | FPInterval
): FPVector {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for scalar-vector to vector generator */
protected unimplementedScalarVectorToVector(
name: string,
_x: number | FPInterval,
_y: (number | FPInterval)[]
): FPVector {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for matrix to interval generator */
protected unimplementedMatrixToInterval(name: string, _x: Array2D<number>): FPInterval {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for matrix to matirx generator */
protected unimplementedMatrixToMatrix(name: string, _x: Array2D<number>): FPMatrix {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for matrix pair to matrix generator */
protected unimplementedMatrixPairToMatrix(
name: string,
_x: Array2D<number>,
_y: Array2D<number>
): FPMatrix {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for matrix-scalar to matrix generator */
protected unimplementedMatrixScalarToMatrix(
name: string,
_x: Array2D<number>,
_y: number | FPInterval
): FPMatrix {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for scalar-matrix to matrix generator */
protected unimplementedScalarMatrixToMatrix(
name: string,
_x: number | FPInterval,
_y: Array2D<number>
): FPMatrix {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for matrix-vector to vector generator */
protected unimplementedMatrixVectorToVector(
name: string,
_x: Array2D<number>,
_y: readonly (number | FPInterval)[]
): FPVector {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for vector-matrix to vector generator */
protected unimplementedVectorMatrixToVector(
name: string,
_x: readonly (number | FPInterval)[],
_y: Array2D<number>
): FPVector {
unreachable(`'${name}' is not yet implemented for '${this.kind}'`);
}
/** Stub for distance generator */
protected unimplementedDistance(
_x: number | readonly number[],
_y: number | readonly number[]
): FPInterval {
unreachable(`'distance' is not yet implemented for '${this.kind}'`);
}
/** Stub for faceForward */
protected unimplementedFaceForward(
_x: readonly number[],
_y: readonly number[],
_z: readonly number[]
): (FPVector | undefined)[] {
unreachable(`'faceForward' is not yet implemented for '${this.kind}'`);
}
/** Stub for length generator */
protected unimplementedLength(
_x: number | FPInterval | readonly number[] | FPVector
): FPInterval {
unreachable(`'length' is not yet implemented for '${this.kind}'`);
}
/** Stub for modf generator */
protected unimplementedModf(_x: number): { fract: FPInterval; whole: FPInterval } {
unreachable(`'modf' is not yet implemented for '${this.kind}'`);
}
/** Stub for refract generator */
protected unimplementedRefract(
_i: readonly number[],
_s: readonly number[],
_r: number
): FPVector {
unreachable(`'refract' is not yet implemented for '${this.kind}'`);
}
/** Stub for absolute errors */
protected unimplementedAbsoluteErrorInterval(_n: number, _error_range: number): FPInterval {
unreachable(`Absolute Error is not implement for '${this.kind}'`);
}
/** Stub for ULP errors */
protected unimplementedUlpInterval(_n: number, _numULP: number): FPInterval {
unreachable(`ULP Error is not implement for '${this.kind}'`);
}
// Utilities - Defined by subclass
/**
* @returns the nearest precise value to the input. Rounding should be IEEE
* 'roundTiesToEven'.
*/
public abstract readonly quantize: (n: number) => number;
/** @returns all valid roundings of input */
public abstract readonly correctlyRounded: (n: number) => readonly number[];
/** @returns true if input is considered finite, otherwise false */
public abstract readonly isFinite: (n: number) => boolean;
/** @returns true if input is considered subnormal, otherwise false */
public abstract readonly isSubnormal: (n: number) => boolean;
/** @returns 0 if the provided number is subnormal, otherwise returns the proved number */
public abstract readonly flushSubnormal: (n: number) => number;
/** @returns 1 * ULP: (number) */
public abstract readonly oneULP: (target: number, mode?: FlushMode) => number;
/** @returns a builder for converting numbers to ScalarsValues */
public abstract readonly scalarBuilder: (n: number) => ScalarValue;
/** @returns a range of scalars for testing */
public abstract scalarRange(): readonly number[];
/** @returns a reduced range of scalars for testing */
public abstract sparseScalarRange(): readonly number[];
/** @returns a range of dim element vectors for testing */
public abstract vectorRange(dim: number): ROArrayArray<number>;
/** @returns a reduced range of dim element vectors for testing */
public abstract sparseVectorRange(dim: number): ROArrayArray<number>;
/** @returns a reduced range of cols x rows matrices for testing
*
* A non-sparse version of this generator is intentionally not provided due to
* runtime issues with more dense ranges.
*/
public abstract sparseMatrixRange(cols: number, rows: number): ROArrayArrayArray<number>;
// Framework - Cases
/**
* @returns a Case for the param and the interval generator provided.
* The Case will use an interval comparator for matching results.
* @param param the param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
private makeScalarToIntervalCase(
param: number,
filter: IntervalFilter,
...ops: ScalarToInterval[]
): Case | undefined {
param = this.quantize(param);
const intervals = ops.map(o => o(param));
if (filter === 'finite' && intervals.some(i => !i.isFinite())) {
return undefined;
}
return { input: [this.scalarBuilder(param)], expected: anyOf(...intervals) };
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param params array of inputs to try
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
public generateScalarToIntervalCases(
params: readonly number[],
filter: IntervalFilter,
...ops: ScalarToInterval[]
): Case[] {
return params.reduce((cases, e) => {
const c = this.makeScalarToIntervalCase(e, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the params and the interval generator provided.
* The Case will use an interval comparator for matching results.
* @param param0 the first param to pass in
* @param param1 the second param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
private makeScalarPairToIntervalCase(
param0: number,
param1: number,
filter: IntervalFilter,
...ops: ScalarPairToInterval[]
): Case | undefined {
param0 = this.quantize(param0);
param1 = this.quantize(param1);
const intervals = ops.map(o => o(param0, param1));
if (filter === 'finite' && intervals.some(i => !i.isFinite())) {
return undefined;
}
return {
input: [this.scalarBuilder(param0), this.scalarBuilder(param1)],
expected: anyOf(...intervals),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param param0s array of inputs to try for the first input
* @param param1s array of inputs to try for the second input
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
public generateScalarPairToIntervalCases(
param0s: readonly number[],
param1s: readonly number[],
filter: IntervalFilter,
...ops: ScalarPairToInterval[]
): Case[] {
return cartesianProduct(param0s, param1s).reduce((cases, e) => {
const c = this.makeScalarPairToIntervalCase(e[0], e[1], filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the params and the interval generator provided.
* The Case will use an interval comparator for matching results.
* @param param0 the first param to pass in
* @param param1 the second param to pass in
* @param param2 the third param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
public makeScalarTripleToIntervalCase(
param0: number,
param1: number,
param2: number,
filter: IntervalFilter,
...ops: ScalarTripleToInterval[]
): Case | undefined {
param0 = this.quantize(param0);
param1 = this.quantize(param1);
param2 = this.quantize(param2);
const intervals = ops.map(o => o(param0, param1, param2));
if (filter === 'finite' && intervals.some(i => !i.isFinite())) {
return undefined;
}
return {
input: [this.scalarBuilder(param0), this.scalarBuilder(param1), this.scalarBuilder(param2)],
expected: anyOf(...intervals),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param param0s array of inputs to try for the first input
* @param param1s array of inputs to try for the second input
* @param param2s array of inputs to try for the third input
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
public generateScalarTripleToIntervalCases(
param0s: readonly number[],
param1s: readonly number[],
param2s: readonly number[],
filter: IntervalFilter,
...ops: ScalarTripleToInterval[]
): Case[] {
return cartesianProduct(param0s, param1s, param2s).reduce((cases, e) => {
const c = this.makeScalarTripleToIntervalCase(e[0], e[1], e[2], filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the params and the interval generator provided.
* The Case will use an interval comparator for matching results.
* @param param the param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
private makeVectorToIntervalCase(
param: readonly number[],
filter: IntervalFilter,
...ops: VectorToInterval[]
): Case | undefined {
param = param.map(this.quantize);
const intervals = ops.map(o => o(param));
if (filter === 'finite' && intervals.some(i => !i.isFinite())) {
return undefined;
}
return {
input: [toVector(param, this.scalarBuilder)],
expected: anyOf(...intervals),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param params array of inputs to try
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
public generateVectorToIntervalCases(
params: ROArrayArray<number>,
filter: IntervalFilter,
...ops: VectorToInterval[]
): Case[] {
return params.reduce((cases, e) => {
const c = this.makeVectorToIntervalCase(e, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the params and the interval generator provided.
* The Case will use an interval comparator for matching results.
* @param param0 the first param to pass in
* @param param1 the second param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
private makeVectorPairToIntervalCase(
param0: readonly number[],
param1: readonly number[],
filter: IntervalFilter,
...ops: VectorPairToInterval[]
): Case | undefined {
param0 = param0.map(this.quantize);
param1 = param1.map(this.quantize);
const intervals = ops.map(o => o(param0, param1));
if (filter === 'finite' && intervals.some(i => !i.isFinite())) {
return undefined;
}
return {
input: [toVector(param0, this.scalarBuilder), toVector(param1, this.scalarBuilder)],
expected: anyOf(...intervals),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param param0s array of inputs to try for the first input
* @param param1s array of inputs to try for the second input
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
public generateVectorPairToIntervalCases(
param0s: ROArrayArray<number>,
param1s: ROArrayArray<number>,
filter: IntervalFilter,
...ops: VectorPairToInterval[]
): Case[] {
return cartesianProduct(param0s, param1s).reduce((cases, e) => {
const c = this.makeVectorPairToIntervalCase(e[0], e[1], filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the param and vector of intervals generator provided
* @param param the param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance
* intervals.
*/
private makeVectorToVectorCase(
param: readonly number[],
filter: IntervalFilter,
...ops: VectorToVector[]
): Case | undefined {
param = param.map(this.quantize);
const vectors = ops.map(o => o(param));
if (filter === 'finite' && vectors.some(v => v.some(e => !e.isFinite()))) {
return undefined;
}
return {
input: [toVector(param, this.scalarBuilder)],
expected: anyOf(...vectors),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param params array of inputs to try
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance
* intervals.
*/
public generateVectorToVectorCases(
params: ROArrayArray<number>,
filter: IntervalFilter,
...ops: VectorToVector[]
): Case[] {
return params.reduce((cases, e) => {
const c = this.makeVectorToVectorCase(e, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the params and the interval vector generator provided.
* The Case will use an interval comparator for matching results.
* @param scalar the scalar param to pass in
* @param vector the vector param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance intervals
*/
private makeScalarVectorToVectorCase(
scalar: number,
vector: readonly number[],
filter: IntervalFilter,
...ops: ScalarVectorToVector[]
): Case | undefined {
scalar = this.quantize(scalar);
vector = vector.map(this.quantize);
const results = ops.map(o => o(scalar, vector));
if (filter === 'finite' && results.some(r => r.some(e => !e.isFinite()))) {
return undefined;
}
return {
input: [this.scalarBuilder(scalar), toVector(vector, this.scalarBuilder)],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param scalars array of scalar inputs to try
* @param vectors array of vector inputs to try
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance intervals
*/
public generateScalarVectorToVectorCases(
scalars: readonly number[],
vectors: ROArrayArray<number>,
filter: IntervalFilter,
...ops: ScalarVectorToVector[]
): Case[] {
// Cannot use cartesianProduct here, due to heterogeneous types
const cases: Case[] = [];
scalars.forEach(scalar => {
vectors.forEach(vector => {
const c = this.makeScalarVectorToVectorCase(scalar, vector, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
});
});
return cases;
}
/**
* @returns a Case for the params and the interval vector generator provided.
* The Case will use an interval comparator for matching results.
* @param vector the vector param to pass in
* @param scalar the scalar param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance intervals
*/
private makeVectorScalarToVectorCase(
vector: readonly number[],
scalar: number,
filter: IntervalFilter,
...ops: VectorScalarToVector[]
): Case | undefined {
vector = vector.map(this.quantize);
scalar = this.quantize(scalar);
const results = ops.map(o => o(vector, scalar));
if (filter === 'finite' && results.some(r => r.some(e => !e.isFinite()))) {
return undefined;
}
return {
input: [toVector(vector, this.scalarBuilder), this.scalarBuilder(scalar)],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param vectors array of vector inputs to try
* @param scalars array of scalar inputs to try
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance intervals
*/
public generateVectorScalarToVectorCases(
vectors: ROArrayArray<number>,
scalars: readonly number[],
filter: IntervalFilter,
...ops: VectorScalarToVector[]
): Case[] {
// Cannot use cartesianProduct here, due to heterogeneous types
const cases: Case[] = [];
vectors.forEach(vector => {
scalars.forEach(scalar => {
const c = this.makeVectorScalarToVectorCase(vector, scalar, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
});
});
return cases;
}
/**
* @returns a Case for the param and vector of intervals generator provided
* @param param0 the first param to pass in
* @param param1 the second param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance
* intervals.
*/
private makeVectorPairToVectorCase(
param0: readonly number[],
param1: readonly number[],
filter: IntervalFilter,
...ops: VectorPairToVector[]
): Case | undefined {
param0 = param0.map(this.quantize);
param1 = param1.map(this.quantize);
const vectors = ops.map(o => o(param0, param1));
if (filter === 'finite' && vectors.some(v => v.some(e => !e.isFinite()))) {
return undefined;
}
return {
input: [toVector(param0, this.scalarBuilder), toVector(param1, this.scalarBuilder)],
expected: anyOf(...vectors),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param param0s array of inputs to try for the first input
* @param param1s array of inputs to try for the second input
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance
* intervals.
*/
public generateVectorPairToVectorCases(
param0s: ROArrayArray<number>,
param1s: ROArrayArray<number>,
filter: IntervalFilter,
...ops: VectorPairToVector[]
): Case[] {
return cartesianProduct(param0s, param1s).reduce((cases, e) => {
const c = this.makeVectorPairToVectorCase(e[0], e[1], filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the params and the component-wise interval generator provided.
* The Case will use an interval comparator for matching results.
* @param param0 the first vector param to pass in
* @param param1 the second vector param to pass in
* @param param2 the scalar param to pass in
* @param filter what interval filtering to apply
* @param componentWiseOps callbacks that implement generating a component-wise acceptance interval,
* one component result at a time.
*/
private makeVectorPairScalarToVectorComponentWiseCase(
param0: readonly number[],
param1: readonly number[],
param2: number,
filter: IntervalFilter,
...componentWiseOps: ScalarTripleToInterval[]
): Case | undefined {
// Width of input vector
const width = param0.length;
assert(2 <= width && width <= 4, 'input vector width must between 2 and 4');
assert(param1.length === width, 'two input vectors must have the same width');
param0 = param0.map(this.quantize);
param1 = param1.map(this.quantize);
param2 = this.quantize(param2);
// Call the component-wise interval generator and build the expectation FPVector
const results = componentWiseOps.map(o => {
return param0.map((el0, index) => o(el0, param1[index], param2)) as FPVector;
});
if (filter === 'finite' && results.some(r => r.some(e => !e.isFinite()))) {
return undefined;
}
return {
input: [
toVector(param0, this.scalarBuilder),
toVector(param1, this.scalarBuilder),
this.scalarBuilder(param2),
],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param param0s array of first vector inputs to try
* @param param1s array of second vector inputs to try
* @param param2s array of scalar inputs to try
* @param filter what interval filtering to apply
* @param componentWiseOpscallbacks that implement generating a component-wise acceptance interval
*/
public generateVectorPairScalarToVectorComponentWiseCase(
param0s: ROArrayArray<number>,
param1s: ROArrayArray<number>,
param2s: readonly number[],
filter: IntervalFilter,
...componentWiseOps: ScalarTripleToInterval[]
): Case[] {
// Cannot use cartesianProduct here, due to heterogeneous types
const cases: Case[] = [];
param0s.forEach(param0 => {
param1s.forEach(param1 => {
param2s.forEach(param2 => {
const c = this.makeVectorPairScalarToVectorComponentWiseCase(
param0,
param1,
param2,
filter,
...componentWiseOps
);
if (c !== undefined) {
cases.push(c);
}
});
});
});
return cases;
}
/**
* @returns a Case for the param and an array of interval generators provided
* @param param the param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
private makeMatrixToScalarCase(
param: ROArrayArray<number>,
filter: IntervalFilter,
...ops: MatrixToScalar[]
): Case | undefined {
param = map2DArray(param, this.quantize);
const results = ops.map(o => o(param));
if (filter === 'finite' && results.some(e => !e.isFinite())) {
return undefined;
}
return {
input: [toMatrix(param, this.scalarBuilder)],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param params array of inputs to try
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
public generateMatrixToScalarCases(
params: ROArrayArrayArray<number>,
filter: IntervalFilter,
...ops: MatrixToScalar[]
): Case[] {
return params.reduce((cases, e) => {
const c = this.makeMatrixToScalarCase(e, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the param and an array of interval generators provided
* @param param the param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a matrix of acceptance
* intervals
*/
private makeMatrixToMatrixCase(
param: ROArrayArray<number>,
filter: IntervalFilter,
...ops: MatrixToMatrix[]
): Case | undefined {
param = map2DArray(param, this.quantize);
const results = ops.map(o => o(param));
if (filter === 'finite' && results.some(m => m.some(c => c.some(r => !r.isFinite())))) {
return undefined;
}
return {
input: [toMatrix(param, this.scalarBuilder)],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param params array of inputs to try
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a matrix of acceptance
* intervals
*/
public generateMatrixToMatrixCases(
params: ROArrayArrayArray<number>,
filter: IntervalFilter,
...ops: MatrixToMatrix[]
): Case[] {
return params.reduce((cases, e) => {
const c = this.makeMatrixToMatrixCase(e, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the params and matrix of intervals generator provided
* @param param0 the first param to pass in
* @param param1 the second param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a matrix of acceptance
* intervals
*/
private makeMatrixPairToMatrixCase(
param0: ROArrayArray<number>,
param1: ROArrayArray<number>,
filter: IntervalFilter,
...ops: MatrixPairToMatrix[]
): Case | undefined {
param0 = map2DArray(param0, this.quantize);
param1 = map2DArray(param1, this.quantize);
const results = ops.map(o => o(param0, param1));
if (filter === 'finite' && results.some(m => m.some(c => c.some(r => !r.isFinite())))) {
return undefined;
}
return {
input: [toMatrix(param0, this.scalarBuilder), toMatrix(param1, this.scalarBuilder)],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param param0s array of inputs to try for the first input
* @param param1s array of inputs to try for the second input
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a matrix of acceptance
* intervals
*/
public generateMatrixPairToMatrixCases(
param0s: ROArrayArrayArray<number>,
param1s: ROArrayArrayArray<number>,
filter: IntervalFilter,
...ops: MatrixPairToMatrix[]
): Case[] {
return cartesianProduct(param0s, param1s).reduce((cases, e) => {
const c = this.makeMatrixPairToMatrixCase(e[0], e[1], filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
/**
* @returns a Case for the params and matrix of intervals generator provided
* @param mat the matrix param to pass in
* @param scalar the scalar to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a matrix of acceptance
* intervals
*/
private makeMatrixScalarToMatrixCase(
mat: ROArrayArray<number>,
scalar: number,
filter: IntervalFilter,
...ops: MatrixScalarToMatrix[]
): Case | undefined {
mat = map2DArray(mat, this.quantize);
scalar = this.quantize(scalar);
const results = ops.map(o => o(mat, scalar));
if (filter === 'finite' && results.some(m => m.some(c => c.some(r => !r.isFinite())))) {
return undefined;
}
return {
input: [toMatrix(mat, this.scalarBuilder), this.scalarBuilder(scalar)],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param mats array of inputs to try for the matrix input
* @param scalars array of inputs to try for the scalar input
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a matrix of acceptance
* intervals
*/
public generateMatrixScalarToMatrixCases(
mats: ROArrayArrayArray<number>,
scalars: readonly number[],
filter: IntervalFilter,
...ops: MatrixScalarToMatrix[]
): Case[] {
// Cannot use cartesianProduct here, due to heterogeneous types
const cases: Case[] = [];
mats.forEach(mat => {
scalars.forEach(scalar => {
const c = this.makeMatrixScalarToMatrixCase(mat, scalar, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
});
});
return cases;
}
/**
* @returns a Case for the params and matrix of intervals generator provided
* @param scalar the scalar to pass in
* @param mat the matrix param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a matrix of acceptance
* intervals
*/
private makeScalarMatrixToMatrixCase(
scalar: number,
mat: ROArrayArray<number>,
filter: IntervalFilter,
...ops: ScalarMatrixToMatrix[]
): Case | undefined {
scalar = this.quantize(scalar);
mat = map2DArray(mat, this.quantize);
const results = ops.map(o => o(scalar, mat));
if (filter === 'finite' && results.some(m => m.some(c => c.some(r => !r.isFinite())))) {
return undefined;
}
return {
input: [this.scalarBuilder(scalar), toMatrix(mat, this.scalarBuilder)],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param scalars array of inputs to try for the scalar input
* @param mats array of inputs to try for the matrix input
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a matrix of acceptance
* intervals
*/
public generateScalarMatrixToMatrixCases(
scalars: readonly number[],
mats: ROArrayArrayArray<number>,
filter: IntervalFilter,
...ops: ScalarMatrixToMatrix[]
): Case[] {
// Cannot use cartesianProduct here, due to heterogeneous types
const cases: Case[] = [];
mats.forEach(mat => {
scalars.forEach(scalar => {
const c = this.makeScalarMatrixToMatrixCase(scalar, mat, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
});
});
return cases;
}
/**
* @returns a Case for the params and the vector of intervals generator provided
* @param mat the matrix param to pass in
* @param vec the vector to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance
* intervals
*/
private makeMatrixVectorToVectorCase(
mat: ROArrayArray<number>,
vec: readonly number[],
filter: IntervalFilter,
...ops: MatrixVectorToVector[]
): Case | undefined {
mat = map2DArray(mat, this.quantize);
vec = vec.map(this.quantize);
const results = ops.map(o => o(mat, vec));
if (filter === 'finite' && results.some(v => v.some(e => !e.isFinite()))) {
return undefined;
}
return {
input: [toMatrix(mat, this.scalarBuilder), toVector(vec, this.scalarBuilder)],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param mats array of inputs to try for the matrix input
* @param vecs array of inputs to try for the vector input
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance
* intervals
*/
public generateMatrixVectorToVectorCases(
mats: ROArrayArrayArray<number>,
vecs: ROArrayArray<number>,
filter: IntervalFilter,
...ops: MatrixVectorToVector[]
): Case[] {
// Cannot use cartesianProduct here, due to heterogeneous types
const cases: Case[] = [];
mats.forEach(mat => {
vecs.forEach(vec => {
const c = this.makeMatrixVectorToVectorCase(mat, vec, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
});
});
return cases;
}
/**
* @returns a Case for the params and the vector of intervals generator provided
* @param vec the vector to pass in
* @param mat the matrix param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance
* intervals
*/
private makeVectorMatrixToVectorCase(
vec: readonly number[],
mat: ROArrayArray<number>,
filter: IntervalFilter,
...ops: VectorMatrixToVector[]
): Case | undefined {
vec = vec.map(this.quantize);
mat = map2DArray(mat, this.quantize);
const results = ops.map(o => o(vec, mat));
if (filter === 'finite' && results.some(v => v.some(e => !e.isFinite()))) {
return undefined;
}
return {
input: [toVector(vec, this.scalarBuilder), toMatrix(mat, this.scalarBuilder)],
expected: anyOf(...results),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param vecs array of inputs to try for the vector input
* @param mats array of inputs to try for the matrix input
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating a vector of acceptance
* intervals
*/
public generateVectorMatrixToVectorCases(
vecs: ROArrayArray<number>,
mats: ROArrayArrayArray<number>,
filter: IntervalFilter,
...ops: VectorMatrixToVector[]
): Case[] {
// Cannot use cartesianProduct here, due to heterogeneous types
const cases: Case[] = [];
vecs.forEach(vec => {
mats.forEach(mat => {
const c = this.makeVectorMatrixToVectorCase(vec, mat, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
});
});
return cases;
}
// Framework - Intervals
/**
* Converts a point to an acceptance interval, using a specific function
*
* This handles correctly rounding and flushing inputs as needed.
* Duplicate inputs are pruned before invoking op.impl.
* op.extrema is invoked before this point in the call stack.
* op.domain is tested before this point in the call stack.
*
* @param n value to flush & round then invoke op.impl on
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
private roundAndFlushScalarToInterval(n: number, op: ScalarToIntervalOp) {
assert(!Number.isNaN(n), `flush not defined for NaN`);
const values = this.correctlyRounded(n);
const inputs = this.addFlushedIfNeeded(values);
if (op.domain !== undefined) {
// Cannot invoke op.domain() directly in the .some, because the narrowing doesn't propegate.
const domain = op.domain();
if (inputs.some(i => !domain.contains(i))) {
return this.constants().unboundedInterval;
}
}
const results = new Set<FPInterval>(inputs.map(op.impl));
return this.spanIntervals(...results);
}
/**
* Converts a pair to an acceptance interval, using a specific function
*
* This handles correctly rounding and flushing inputs as needed.
* Duplicate inputs are pruned before invoking op.impl.
* All unique combinations of x & y are run.
* op.extrema is invoked before this point in the call stack.
* op.domain is tested before this point in the call stack.
*
* @param x first param to flush & round then invoke op.impl on
* @param y second param to flush & round then invoke op.impl on
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
private roundAndFlushScalarPairToInterval(
x: number,
y: number,
op: ScalarPairToIntervalOp
): FPInterval {
assert(!Number.isNaN(x), `flush not defined for NaN`);
assert(!Number.isNaN(y), `flush not defined for NaN`);
const x_values = this.correctlyRounded(x);
const y_values = this.correctlyRounded(y);
const x_inputs = this.addFlushedIfNeeded(x_values);
const y_inputs = this.addFlushedIfNeeded(y_values);
if (op.domain !== undefined) {
// Cannot invoke op.domain() directly in the .some, because the narrowing doesn't propegate.
const domain = op.domain();
if (x_inputs.some(i => !domain.x.some(e => e.contains(i)))) {
return this.constants().unboundedInterval;
}
if (y_inputs.some(j => !domain.y.some(e => e.contains(j)))) {
return this.constants().unboundedInterval;
}
}
const intervals = new Set<FPInterval>();
x_inputs.forEach(inner_x => {
y_inputs.forEach(inner_y => {
intervals.add(op.impl(inner_x, inner_y));
});
});
return this.spanIntervals(...intervals);
}
/**
* Converts a triplet to an acceptance interval, using a specific function
*
* This handles correctly rounding and flushing inputs as needed.
* Duplicate inputs are pruned before invoking op.impl.
* All unique combinations of x, y & z are run.
*
* @param x first param to flush & round then invoke op.impl on
* @param y second param to flush & round then invoke op.impl on
* @param z third param to flush & round then invoke op.impl on
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
private roundAndFlushScalarTripleToInterval(
x: number,
y: number,
z: number,
op: ScalarTripleToIntervalOp
): FPInterval {
assert(!Number.isNaN(x), `flush not defined for NaN`);
assert(!Number.isNaN(y), `flush not defined for NaN`);
assert(!Number.isNaN(z), `flush not defined for NaN`);
const x_values = this.correctlyRounded(x);
const y_values = this.correctlyRounded(y);
const z_values = this.correctlyRounded(z);
const x_inputs = this.addFlushedIfNeeded(x_values);
const y_inputs = this.addFlushedIfNeeded(y_values);
const z_inputs = this.addFlushedIfNeeded(z_values);
const intervals = new Set<FPInterval>();
// prettier-ignore
x_inputs.forEach(inner_x => {
y_inputs.forEach(inner_y => {
z_inputs.forEach(inner_z => {
intervals.add(op.impl(inner_x, inner_y, inner_z));
});
});
});
return this.spanIntervals(...intervals);
}
/**
* Converts a vector to an acceptance interval using a specific function
*
* This handles correctly rounding and flushing inputs as needed.
* Duplicate inputs are pruned before invoking op.impl.
*
* @param x param to flush & round then invoke op.impl on
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
private roundAndFlushVectorToInterval(x: readonly number[], op: VectorToIntervalOp): FPInterval {
assert(
x.every(e => !Number.isNaN(e)),
`flush not defined for NaN`
);
const x_rounded: ROArrayArray<number> = x.map(this.correctlyRounded);
const x_flushed: ROArrayArray<number> = x_rounded.map(this.addFlushedIfNeeded.bind(this));
const x_inputs = cartesianProduct<number>(...x_flushed);
const intervals = new Set<FPInterval>();
x_inputs.forEach(inner_x => {
intervals.add(op.impl(inner_x));
});
return this.spanIntervals(...intervals);
}
/**
* Converts a pair of vectors to an acceptance interval using a specific
* function
*
* This handles correctly rounding and flushing inputs as needed.
* Duplicate inputs are pruned before invoking op.impl.
* All unique combinations of x & y are run.
*
* @param x first param to flush & round then invoke op.impl on
* @param y second param to flush & round then invoke op.impl on
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
private roundAndFlushVectorPairToInterval(
x: readonly number[],
y: readonly number[],
op: VectorPairToIntervalOp
): FPInterval {
assert(
x.every(e => !Number.isNaN(e)),
`flush not defined for NaN`
);
assert(
y.every(e => !Number.isNaN(e)),
`flush not defined for NaN`
);
const x_rounded: ROArrayArray<number> = x.map(this.correctlyRounded);
const y_rounded: ROArrayArray<number> = y.map(this.correctlyRounded);
const x_flushed: ROArrayArray<number> = x_rounded.map(this.addFlushedIfNeeded.bind(this));
const y_flushed: ROArrayArray<number> = y_rounded.map(this.addFlushedIfNeeded.bind(this));
const x_inputs = cartesianProduct<number>(...x_flushed);
const y_inputs = cartesianProduct<number>(...y_flushed);
const intervals = new Set<FPInterval>();
x_inputs.forEach(inner_x => {
y_inputs.forEach(inner_y => {
intervals.add(op.impl(inner_x, inner_y));
});
});
return this.spanIntervals(...intervals);
}
/**
* Converts a vector to a vector of acceptance intervals using a specific
* function
*
* This handles correctly rounding and flushing inputs as needed.
* Duplicate inputs are pruned before invoking op.impl.
*
* @param x param to flush & round then invoke op.impl on
* @param op operation defining the function being run
* @returns a vector of spans for each outputs of op.impl
*/
private roundAndFlushVectorToVector(x: readonly number[], op: VectorToVectorOp): FPVector {
assert(
x.every(e => !Number.isNaN(e)),
`flush not defined for NaN`
);
const x_rounded: ROArrayArray<number> = x.map(this.correctlyRounded);
const x_flushed: ROArrayArray<number> = x_rounded.map(this.addFlushedIfNeeded.bind(this));
const x_inputs = cartesianProduct<number>(...x_flushed);
const interval_vectors = new Set<FPVector>();
x_inputs.forEach(inner_x => {
interval_vectors.add(op.impl(inner_x));
});
return this.spanVectors(...interval_vectors);
}
/**
* Converts a pair of vectors to a vector of acceptance intervals using a
* specific function
*
* This handles correctly rounding and flushing inputs as needed.
* Duplicate inputs are pruned before invoking op.impl.
*
* @param x first param to flush & round then invoke op.impl on
* @param y second param to flush & round then invoke op.impl on
* @param op operation defining the function being run
* @returns a vector of spans for each output of op.impl
*/
private roundAndFlushVectorPairToVector(
x: readonly number[],
y: readonly number[],
op: VectorPairToVectorOp
): FPVector {
assert(
x.every(e => !Number.isNaN(e)),
`flush not defined for NaN`
);
assert(
y.every(e => !Number.isNaN(e)),
`flush not defined for NaN`
);
const x_rounded: ROArrayArray<number> = x.map(this.correctlyRounded);
const y_rounded: ROArrayArray<number> = y.map(this.correctlyRounded);
const x_flushed: ROArrayArray<number> = x_rounded.map(this.addFlushedIfNeeded.bind(this));
const y_flushed: ROArrayArray<number> = y_rounded.map(this.addFlushedIfNeeded.bind(this));
const x_inputs = cartesianProduct<number>(...x_flushed);
const y_inputs = cartesianProduct<number>(...y_flushed);
const interval_vectors = new Set<FPVector>();
x_inputs.forEach(inner_x => {
y_inputs.forEach(inner_y => {
interval_vectors.add(op.impl(inner_x, inner_y));
});
});
return this.spanVectors(...interval_vectors);
}
/**
* Converts a matrix to a matrix of acceptance intervals using a specific
* function
*
* This handles correctly rounding and flushing inputs as needed.
* Duplicate inputs are pruned before invoking op.impl.
*
* @param m param to flush & round then invoke op.impl on
* @param op operation defining the function being run
* @returns a matrix of spans for each outputs of op.impl
*/
private roundAndFlushMatrixToMatrix(m: Array2D<number>, op: MatrixToMatrixOp): FPMatrix {
const num_cols = m.length;
const num_rows = m[0].length;
assert(
m.every(c => c.every(r => !Number.isNaN(r))),
`flush not defined for NaN`
);
const m_flat = flatten2DArray(m);
const m_rounded: ROArrayArray<number> = m_flat.map(this.correctlyRounded);
const m_flushed: ROArrayArray<number> = m_rounded.map(this.addFlushedIfNeeded.bind(this));
const m_options: ROArrayArray<number> = cartesianProduct<number>(...m_flushed);
const m_inputs: ROArrayArrayArray<number> = m_options.map(e =>
unflatten2DArray(e, num_cols, num_rows)
);
const interval_matrices = new Set<FPMatrix>();
m_inputs.forEach(inner_m => {
interval_matrices.add(op.impl(inner_m));
});
return this.spanMatrices(...interval_matrices);
}
/**
* Calculate the acceptance interval for a unary function over an interval
*
* If the interval is actually a point, this just decays to
* roundAndFlushScalarToInterval.
*
* The provided domain interval may be adjusted if the operation defines an
* extrema function.
*
* @param x input domain interval
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
protected runScalarToIntervalOp(x: FPInterval, op: ScalarToIntervalOp): FPInterval {
if (!x.isFinite()) {
return this.constants().unboundedInterval;
}
if (op.extrema !== undefined) {
x = op.extrema(x);
}
const result = this.spanIntervals(
...x.endpoints().map(b => this.roundAndFlushScalarToInterval(b, op))
);
return result.isFinite() ? result : this.constants().unboundedInterval;
}
/**
* Calculate the acceptance interval for a binary function over an interval
*
* The provided domain intervals may be adjusted if the operation defines an
* extrema function.
*
* @param x first input domain interval
* @param y second input domain interval
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
protected runScalarPairToIntervalOp(
x: FPInterval,
y: FPInterval,
op: ScalarPairToIntervalOp
): FPInterval {
if (!x.isFinite() || !y.isFinite()) {
return this.constants().unboundedInterval;
}
if (op.extrema !== undefined) {
[x, y] = op.extrema(x, y);
}
const outputs = new Set<FPInterval>();
x.endpoints().forEach(inner_x => {
y.endpoints().forEach(inner_y => {
outputs.add(this.roundAndFlushScalarPairToInterval(inner_x, inner_y, op));
});
});
const result = this.spanIntervals(...outputs);
return result.isFinite() ? result : this.constants().unboundedInterval;
}
/**
* Calculate the acceptance interval for a ternary function over an interval
*
* @param x first input domain interval
* @param y second input domain interval
* @param z third input domain interval
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
protected runScalarTripleToIntervalOp(
x: FPInterval,
y: FPInterval,
z: FPInterval,
op: ScalarTripleToIntervalOp
): FPInterval {
if (!x.isFinite() || !y.isFinite() || !z.isFinite()) {
return this.constants().unboundedInterval;
}
const outputs = new Set<FPInterval>();
x.endpoints().forEach(inner_x => {
y.endpoints().forEach(inner_y => {
z.endpoints().forEach(inner_z => {
outputs.add(this.roundAndFlushScalarTripleToInterval(inner_x, inner_y, inner_z, op));
});
});
});
const result = this.spanIntervals(...outputs);
return result.isFinite() ? result : this.constants().unboundedInterval;
}
/**
* Calculate the acceptance interval for a vector function over given
* intervals
*
* @param x input domain intervals vector
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
protected runVectorToIntervalOp(x: FPVector, op: VectorToIntervalOp): FPInterval {
if (x.some(e => !e.isFinite())) {
return this.constants().unboundedInterval;
}
const x_values = cartesianProduct<number>(...x.map(e => e.endpoints()));
const outputs = new Set<FPInterval>();
x_values.forEach(inner_x => {
outputs.add(this.roundAndFlushVectorToInterval(inner_x, op));
});
const result = this.spanIntervals(...outputs);
return result.isFinite() ? result : this.constants().unboundedInterval;
}
/**
* Calculate the acceptance interval for a vector pair function over given
* intervals
*
* @param x first input domain intervals vector
* @param y second input domain intervals vector
* @param op operation defining the function being run
* @returns a span over all the outputs of op.impl
*/
protected runVectorPairToIntervalOp(
x: FPVector,
y: FPVector,
op: VectorPairToIntervalOp
): FPInterval {
if (x.some(e => !e.isFinite()) || y.some(e => !e.isFinite())) {
return this.constants().unboundedInterval;
}
const x_values = cartesianProduct<number>(...x.map(e => e.endpoints()));
const y_values = cartesianProduct<number>(...y.map(e => e.endpoints()));
const outputs = new Set<FPInterval>();
x_values.forEach(inner_x => {
y_values.forEach(inner_y => {
outputs.add(this.roundAndFlushVectorPairToInterval(inner_x, inner_y, op));
});
});
const result = this.spanIntervals(...outputs);
return result.isFinite() ? result : this.constants().unboundedInterval;
}
/**
* Calculate the vector of acceptance intervals for a pair of vector function
* over given intervals
*
* @param x input domain intervals vector
* @param op operation defining the function being run
* @returns a vector of spans over all the outputs of op.impl
*/
protected runVectorToVectorOp(x: FPVector, op: VectorToVectorOp): FPVector {
if (x.some(e => !e.isFinite())) {
return this.constants().unboundedVector[x.length];
}
const x_values = cartesianProduct<number>(...x.map(e => e.endpoints()));
const outputs = new Set<FPVector>();
x_values.forEach(inner_x => {
outputs.add(this.roundAndFlushVectorToVector(inner_x, op));
});
const result = this.spanVectors(...outputs);
return result.every(e => e.isFinite())
? result
: this.constants().unboundedVector[result.length];
}
/**
* Calculate the vector of acceptance intervals by running a scalar operation
* component-wise over a vector.
*
* This is used for situations where a component-wise operation, like vector
* negation, is needed as part of an inherited accuracy, but the top-level
* operation test don't require an explicit vector definition of the function,
* due to the generated 'vectorize' tests being sufficient.
*
* @param x input domain intervals vector
* @param op scalar operation to be run component-wise
* @returns a vector of intervals with the outputs of op.impl
*/
protected runScalarToIntervalOpComponentWise(x: FPVector, op: ScalarToIntervalOp): FPVector {
return this.toVector(x.map(e => this.runScalarToIntervalOp(e, op)));
}
/**
* Calculate the vector of acceptance intervals for a vector function over
* given intervals
*
* @param x first input domain intervals vector
* @param y second input domain intervals vector
* @param op operation defining the function being run
* @returns a vector of spans over all the outputs of op.impl
*/
protected runVectorPairToVectorOp(x: FPVector, y: FPVector, op: VectorPairToVectorOp): FPVector {
if (x.some(e => !e.isFinite()) || y.some(e => !e.isFinite())) {
return this.constants().unboundedVector[x.length];
}
const x_values = cartesianProduct<number>(...x.map(e => e.endpoints()));
const y_values = cartesianProduct<number>(...y.map(e => e.endpoints()));
const outputs = new Set<FPVector>();
x_values.forEach(inner_x => {
y_values.forEach(inner_y => {
outputs.add(this.roundAndFlushVectorPairToVector(inner_x, inner_y, op));
});
});
const result = this.spanVectors(...outputs);
return result.every(e => e.isFinite())
? result
: this.constants().unboundedVector[result.length];
}
/**
* Calculate the vector of acceptance intervals by running a scalar operation
* component-wise over a pair of vectors.
*
* This is used for situations where a component-wise operation, like vector
* subtraction, is needed as part of an inherited accuracy, but the top-level
* operation test don't require an explicit vector definition of the function,
* due to the generated 'vectorize' tests being sufficient.
*
* @param x first input domain intervals vector
* @param y second input domain intervals vector
* @param op scalar operation to be run component-wise
* @returns a vector of intervals with the outputs of op.impl
*/
protected runScalarPairToIntervalOpVectorComponentWise(
x: FPVector,
y: FPVector,
op: ScalarPairToIntervalOp
): FPVector {
assert(
x.length === y.length,
`runScalarPairToIntervalOpVectorComponentWise requires vectors of the same dimensions`
);
return this.toVector(
x.map((i, idx) => {
return this.runScalarPairToIntervalOp(i, y[idx], op);
})
);
}
/**
* Calculate the matrix of acceptance intervals for a pair of matrix function over
* given intervals
*
* @param m input domain intervals matrix
* @param op operation defining the function being run
* @returns a matrix of spans over all the outputs of op.impl
*/
protected runMatrixToMatrixOp(m: FPMatrix, op: MatrixToMatrixOp): FPMatrix {
const num_cols = m.length;
const num_rows = m[0].length;
// Do not check for OOB inputs and exit early here, because the shape of
// the output matrix may be determined by the operation being run,
// i.e. transpose.
const m_flat: readonly FPInterval[] = flatten2DArray(m);
const m_values: ROArrayArray<number> = cartesianProduct<number>(
...m_flat.map(e => e.endpoints())
);
const outputs = new Set<FPMatrix>();
m_values.forEach(inner_m => {
const unflat_m = unflatten2DArray(inner_m, num_cols, num_rows);
outputs.add(this.roundAndFlushMatrixToMatrix(unflat_m, op));
});
const result = this.spanMatrices(...outputs);
const result_cols = result.length;
const result_rows = result[0].length;
// FPMatrix has to be coerced to ROArrayArray<FPInterval> to use .every. This should
// always be safe, since FPMatrix are defined as fixed length array of
// arrays.
return (result as ROArrayArray<FPInterval>).every(c => c.every(r => r.isFinite()))
? result
: this.constants().unboundedMatrix[result_cols][result_rows];
}
/**
* Calculate the Matrix of acceptance intervals by running a scalar operation
* component-wise over a scalar and a matrix.
*
* An example of this is performing constant scaling.
*
* @param i scalar input
* @param m matrix input
* @param op scalar operation to be run component-wise
* @returns a matrix of intervals with the outputs of op.impl
*/
protected runScalarPairToIntervalOpScalarMatrixComponentWise(
i: FPInterval,
m: FPMatrix,
op: ScalarPairToIntervalOp
): FPMatrix {
const cols = m.length;
const rows = m[0].length;
return this.toMatrix(
unflatten2DArray(
flatten2DArray(m).map(e => this.runScalarPairToIntervalOp(i, e, op)),
cols,
rows
)
);
}
/**
* Calculate the Matrix of acceptance intervals by running a scalar operation
* component-wise over a pair of matrices.
*
* An example of this is performing matrix addition.
*
* @param x first input domain intervals matrix
* @param y second input domain intervals matrix
* @param op scalar operation to be run component-wise
* @returns a matrix of intervals with the outputs of op.impl
*/
protected runScalarPairToIntervalOpMatrixMatrixComponentWise(
x: FPMatrix,
y: FPMatrix,
op: ScalarPairToIntervalOp
): FPMatrix {
assert(
x.length === y.length && x[0].length === y[0].length,
`runScalarPairToIntervalOpMatrixMatrixComponentWise requires matrices of the same dimensions`
);
const cols = x.length;
const rows = x[0].length;
const flat_x = flatten2DArray(x);
const flat_y = flatten2DArray(y);
return this.toMatrix(
unflatten2DArray(
flat_x.map((i, idx) => {
return this.runScalarPairToIntervalOp(i, flat_y[idx], op);
}),
cols,
rows
)
);
}
// API - Fundamental Error Intervals
/** @returns a ScalarToIntervalOp for [n - error_range, n + error_range] */
private AbsoluteErrorIntervalOp(error_range: number): ScalarToIntervalOp {
const op: ScalarToIntervalOp = {
impl: (_: number) => {
return this.constants().unboundedInterval;
},
};
assert(
error_range >= 0,
`absoluteErrorInterval must have non-negative error range, get ${error_range}`
);
if (this.isFinite(error_range)) {
op.impl = (n: number) => {
assert(!Number.isNaN(n), `absolute error not defined for NaN`);
// Return anyInterval if given center n is infinity.
if (!this.isFinite(n)) {
return this.constants().unboundedInterval;
}
return this.toInterval([n - error_range, n + error_range]);
};
}
return op;
}
protected absoluteErrorIntervalImpl(n: number, error_range: number): FPInterval {
error_range = Math.abs(error_range);
return this.runScalarToIntervalOp(
this.toInterval(n),
this.AbsoluteErrorIntervalOp(error_range)
);
}
/** @returns an interval of the absolute error around the point */
public abstract readonly absoluteErrorInterval: (n: number, error_range: number) => FPInterval;
/**
* Defines a ScalarToIntervalOp for an interval of the correctly rounded values
* around the point
*/
private readonly CorrectlyRoundedIntervalOp: ScalarToIntervalOp = {
impl: (n: number) => {
assert(!Number.isNaN(n), `absolute not defined for NaN`);
return this.toInterval(n);
},
};
protected correctlyRoundedIntervalImpl(n: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.CorrectlyRoundedIntervalOp);
}
/** @returns an interval of the correctly rounded values around the point */
public abstract readonly correctlyRoundedInterval: (n: number | FPInterval) => FPInterval;
protected correctlyRoundedMatrixImpl(m: Array2D<number>): FPMatrix {
return this.toMatrix(map2DArray(m, this.correctlyRoundedInterval));
}
/** @returns a matrix of correctly rounded intervals for the provided matrix */
public abstract readonly correctlyRoundedMatrix: (m: Array2D<number>) => FPMatrix;
/** @returns a ScalarToIntervalOp for [n - numULP * ULP(n), n + numULP * ULP(n)] */
private ULPIntervalOp(numULP: number): ScalarToIntervalOp {
const op: ScalarToIntervalOp = {
impl: (_: number) => {
return this.constants().unboundedInterval;
},
};
if (this.isFinite(numULP)) {
op.impl = (n: number) => {
assert(!Number.isNaN(n), `ULP error not defined for NaN`);
const ulp = this.oneULP(n);
const begin = n - numULP * ulp;
const end = n + numULP * ulp;
return this.toInterval([
Math.min(begin, this.flushSubnormal(begin)),
Math.max(end, this.flushSubnormal(end)),
]);
};
}
return op;
}
protected ulpIntervalImpl(n: number, numULP: number): FPInterval {
numULP = Math.abs(numULP);
return this.runScalarToIntervalOp(this.toInterval(n), this.ULPIntervalOp(numULP));
}
/** @returns an interval of N * ULP around the point */
public abstract readonly ulpInterval: (n: number, numULP: number) => FPInterval;
// API - Acceptance Intervals
private readonly AbsIntervalOp: ScalarToIntervalOp = {
impl: (n: number) => {
return this.correctlyRoundedInterval(Math.abs(n));
},
};
protected absIntervalImpl(n: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.AbsIntervalOp);
}
/** Calculate an acceptance interval for abs(n) */
public abstract readonly absInterval: (n: number | FPInterval) => FPInterval;
// This op is implemented differently for f32 and f16.
private readonly AcosIntervalOp: ScalarToIntervalOp = {
impl: (n: number) => {
assert(this.kind === 'f32' || this.kind === 'f16');
// acos(n) = atan2(sqrt(1.0 - n * n), n) or a polynomial approximation with absolute error
const y = this.sqrtInterval(this.subtractionInterval(1, this.multiplicationInterval(n, n)));
const approx_abs_error = this.kind === 'f32' ? 6.77e-5 : 3.91e-3;
return this.spanIntervals(
this.atan2Interval(y, n),
this.absoluteErrorInterval(Math.acos(n), approx_abs_error)
);
},
domain: () => {
return this.constants().negOneToOneInterval;
},
};
protected acosIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.AcosIntervalOp);
}
/** Calculate an acceptance interval for acos(n) */
public abstract readonly acosInterval: (n: number) => FPInterval;
private readonly AcoshAlternativeIntervalOp: ScalarToIntervalOp = {
impl: (x: number): FPInterval => {
// acosh(x) = log(x + sqrt((x + 1.0f) * (x - 1.0)))
const inner_value = this.multiplicationInterval(
this.additionInterval(x, 1.0),
this.subtractionInterval(x, 1.0)
);
const sqrt_value = this.sqrtInterval(inner_value);
return this.logInterval(this.additionInterval(x, sqrt_value));
},
};
protected acoshAlternativeIntervalImpl(x: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(x), this.AcoshAlternativeIntervalOp);
}
/** Calculate an acceptance interval of acosh(x) using log(x + sqrt((x + 1.0f) * (x - 1.0))) */
public abstract readonly acoshAlternativeInterval: (x: number | FPInterval) => FPInterval;
private readonly AcoshPrimaryIntervalOp: ScalarToIntervalOp = {
impl: (x: number): FPInterval => {
// acosh(x) = log(x + sqrt(x * x - 1.0))
const inner_value = this.subtractionInterval(this.multiplicationInterval(x, x), 1.0);
const sqrt_value = this.sqrtInterval(inner_value);
return this.logInterval(this.additionInterval(x, sqrt_value));
},
};
protected acoshPrimaryIntervalImpl(x: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(x), this.AcoshPrimaryIntervalOp);
}
/** Calculate an acceptance interval of acosh(x) using log(x + sqrt(x * x - 1.0)) */
protected abstract acoshPrimaryInterval: (x: number | FPInterval) => FPInterval;
/** All acceptance interval functions for acosh(x) */
public abstract readonly acoshIntervals: ScalarToInterval[];
private readonly AdditionIntervalOp: ScalarPairToIntervalOp = {
impl: (x: number, y: number): FPInterval => {
const sum = x + y;
const large_val = Math.abs(x) > Math.abs(y) ? x : y;
const small_val = Math.abs(x) > Math.abs(y) ? y : x;
return this.correctlyRoundedIntervalWithUnboundedPrecisionForAddition(
sum,
large_val,
small_val
);
},
};
protected additionIntervalImpl(x: number | FPInterval, y: number | FPInterval): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.AdditionIntervalOp
);
}
/** Calculate an acceptance interval of x + y, when x and y are both scalars */
public abstract readonly additionInterval: (
x: number | FPInterval,
y: number | FPInterval
) => FPInterval;
protected additionMatrixMatrixIntervalImpl(x: Array2D<number>, y: Array2D<number>): FPMatrix {
return this.runScalarPairToIntervalOpMatrixMatrixComponentWise(
this.toMatrix(x),
this.toMatrix(y),
this.AdditionIntervalOp
);
}
/** Calculate an acceptance interval of x + y, when x and y are matrices */
public abstract readonly additionMatrixMatrixInterval: (
x: Array2D<number>,
y: Array2D<number>
) => FPMatrix;
// This op is implemented differently for f32 and f16.
private readonly AsinIntervalOp: ScalarToIntervalOp = {
impl: (n: number) => {
assert(this.kind === 'f32' || this.kind === 'f16');
// asin(n) = atan2(n, sqrt(1.0 - n * n)) or a polynomial approximation with absolute error
const x = this.sqrtInterval(this.subtractionInterval(1, this.multiplicationInterval(n, n)));
const approx_abs_error = this.kind === 'f32' ? 6.81e-5 : 3.91e-3;
return this.spanIntervals(
this.atan2Interval(n, x),
this.absoluteErrorInterval(Math.asin(n), approx_abs_error)
);
},
domain: () => {
return this.constants().negOneToOneInterval;
},
};
/** Calculate an acceptance interval for asin(n) */
protected asinIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.AsinIntervalOp);
}
/** Calculate an acceptance interval for asin(n) */
public abstract readonly asinInterval: (n: number) => FPInterval;
private readonly AsinhIntervalOp: ScalarToIntervalOp = {
impl: (x: number): FPInterval => {
// asinh(x) = log(x + sqrt(x * x + 1.0))
const inner_value = this.additionInterval(this.multiplicationInterval(x, x), 1.0);
const sqrt_value = this.sqrtInterval(inner_value);
return this.logInterval(this.additionInterval(x, sqrt_value));
},
};
protected asinhIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.AsinhIntervalOp);
}
/** Calculate an acceptance interval of asinh(x) */
public abstract readonly asinhInterval: (n: number) => FPInterval;
private readonly AtanIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
assert(this.kind === 'f32' || this.kind === 'f16');
const ulp_error = this.kind === 'f32' ? 4096 : 5;
return this.ulpInterval(Math.atan(n), ulp_error);
},
};
/** Calculate an acceptance interval of atan(x) */
protected atanIntervalImpl(n: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.AtanIntervalOp);
}
/** Calculate an acceptance interval of atan(x) */
public abstract readonly atanInterval: (n: number | FPInterval) => FPInterval;
// This op is implemented differently for f32 and f16.
private Atan2IntervalOpBuilder(): ScalarPairToIntervalOp {
assert(this.kind === 'f32' || this.kind === 'f16');
const constants = this.constants();
// For atan2, the params are labelled (y, x), not (x, y), so domain.x is first parameter (y),
// and domain.y is the second parameter (x).
// The first param must be finite and normal.
const domain_x = [
this.toInterval([constants.negative.min, constants.negative.max]),
this.toInterval([constants.positive.min, constants.positive.max]),
];
// inherited from division
const domain_y =
this.kind === 'f32'
? [this.toInterval([-(2 ** 126), -(2 ** -126)]), this.toInterval([2 ** -126, 2 ** 126])]
: [this.toInterval([-(2 ** 14), -(2 ** -14)]), this.toInterval([2 ** -14, 2 ** 14])];
const ulp_error = this.kind === 'f32' ? 4096 : 5;
return {
impl: (y: number, x: number): FPInterval => {
// Accurate result in f64
let atan_yx = Math.atan(y / x);
// Offset by +/-pi according to the definition. Use pi value in f64 because we are
// handling accurate result.
if (x < 0) {
// x < 0, y > 0, result is atan(y/x) + π
if (y > 0) {
atan_yx = atan_yx + kValue.f64.positive.pi.whole;
} else {
// x < 0, y < 0, result is atan(y/x) - π
atan_yx = atan_yx - kValue.f64.positive.pi.whole;
}
}
return this.ulpInterval(atan_yx, ulp_error);
},
extrema: (y: FPInterval, x: FPInterval): [FPInterval, FPInterval] => {
// There is discontinuity, which generates an unbounded result, at y/x = 0 that will dominate the accuracy
if (y.contains(0)) {
if (x.contains(0)) {
return [this.toInterval(0), this.toInterval(0)];
}
return [this.toInterval(0), x];
}
return [y, x];
},
domain: () => {
return { x: domain_x, y: domain_y };
},
};
}
protected atan2IntervalImpl(y: number | FPInterval, x: number | FPInterval): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(y),
this.toInterval(x),
this.Atan2IntervalOpBuilder()
);
}
/** Calculate an acceptance interval of atan2(y, x) */
public abstract readonly atan2Interval: (
y: number | FPInterval,
x: number | FPInterval
) => FPInterval;
private readonly AtanhIntervalOp: ScalarToIntervalOp = {
impl: (n: number) => {
// atanh(x) = log((1.0 + x) / (1.0 - x)) * 0.5
const numerator = this.additionInterval(1.0, n);
const denominator = this.subtractionInterval(1.0, n);
const log_interval = this.logInterval(this.divisionInterval(numerator, denominator));
return this.multiplicationInterval(log_interval, 0.5);
},
};
protected atanhIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.AtanhIntervalOp);
}
/** Calculate an acceptance interval of atanh(x) */
public abstract readonly atanhInterval: (n: number) => FPInterval;
private readonly CeilIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.correctlyRoundedInterval(Math.ceil(n));
},
};
protected ceilIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.CeilIntervalOp);
}
/** Calculate an acceptance interval of ceil(x) */
public abstract readonly ceilInterval: (n: number) => FPInterval;
private readonly ClampMedianIntervalOp: ScalarTripleToIntervalOp = {
impl: (x: number, y: number, z: number): FPInterval => {
return this.correctlyRoundedInterval(
// Default sort is string sort, so have to implement numeric comparison.
// Cannot use the b-a one-liner, because that assumes no infinities.
[x, y, z].sort((a, b) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
})[1]
);
},
};
protected clampMedianIntervalImpl(
x: number | FPInterval,
y: number | FPInterval,
z: number | FPInterval
): FPInterval {
return this.runScalarTripleToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.toInterval(z),
this.ClampMedianIntervalOp
);
}
/** Calculate an acceptance interval of clamp(x, y, z) via median(x, y, z) */
public abstract readonly clampMedianInterval: (
x: number | FPInterval,
y: number | FPInterval,
z: number | FPInterval
) => FPInterval;
private readonly ClampMinMaxIntervalOp: ScalarTripleToIntervalOp = {
impl: (x: number, low: number, high: number): FPInterval => {
return this.minInterval(this.maxInterval(x, low), high);
},
};
protected clampMinMaxIntervalImpl(
x: number | FPInterval,
low: number | FPInterval,
high: number | FPInterval
): FPInterval {
return this.runScalarTripleToIntervalOp(
this.toInterval(x),
this.toInterval(low),
this.toInterval(high),
this.ClampMinMaxIntervalOp
);
}
/** Calculate an acceptance interval of clamp(x, high, low) via min(max(x, low), high) */
public abstract readonly clampMinMaxInterval: (
x: number | FPInterval,
low: number | FPInterval,
high: number | FPInterval
) => FPInterval;
/** All acceptance interval functions for clamp(x, y, z) */
public abstract readonly clampIntervals: ScalarTripleToInterval[];
private readonly CosIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
assert(this.kind === 'f32' || this.kind === 'f16');
const abs_error = this.kind === 'f32' ? 2 ** -11 : 2 ** -7;
return this.absoluteErrorInterval(Math.cos(n), abs_error);
},
domain: () => {
return this.constants().negPiToPiInterval;
},
};
protected cosIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.CosIntervalOp);
}
/** Calculate an acceptance interval of cos(x) */
public abstract readonly cosInterval: (n: number) => FPInterval;
private readonly CoshIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
// cosh(x) = (exp(x) + exp(-x)) * 0.5
const minus_n = this.negationInterval(n);
return this.multiplicationInterval(
this.additionInterval(this.expInterval(n), this.expInterval(minus_n)),
0.5
);
},
};
protected coshIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.CoshIntervalOp);
}
/** Calculate an acceptance interval of cosh(x) */
public abstract readonly coshInterval: (n: number) => FPInterval;
private readonly CrossIntervalOp: VectorPairToVectorOp = {
impl: (x: readonly number[], y: readonly number[]): FPVector => {
assert(x.length === 3, `CrossIntervalOp received x with ${x.length} instead of 3`);
assert(y.length === 3, `CrossIntervalOp received y with ${y.length} instead of 3`);
// cross(x, y) = r, where
// r[0] = x[1] * y[2] - x[2] * y[1]
// r[1] = x[2] * y[0] - x[0] * y[2]
// r[2] = x[0] * y[1] - x[1] * y[0]
const r0 = this.subtractionInterval(
this.multiplicationInterval(x[1], y[2]),
this.multiplicationInterval(x[2], y[1])
);
const r1 = this.subtractionInterval(
this.multiplicationInterval(x[2], y[0]),
this.multiplicationInterval(x[0], y[2])
);
const r2 = this.subtractionInterval(
this.multiplicationInterval(x[0], y[1]),
this.multiplicationInterval(x[1], y[0])
);
if (r0.isFinite() && r1.isFinite() && r2.isFinite()) {
return [r0, r1, r2];
}
return this.constants().unboundedVector[3];
},
};
protected crossIntervalImpl(x: readonly number[], y: readonly number[]): FPVector {
assert(x.length === 3, `Cross is only defined for vec3`);
assert(y.length === 3, `Cross is only defined for vec3`);
return this.runVectorPairToVectorOp(this.toVector(x), this.toVector(y), this.CrossIntervalOp);
}
/** Calculate a vector of acceptance intervals for cross(x, y) */
public abstract readonly crossInterval: (x: readonly number[], y: readonly number[]) => FPVector;
private readonly DegreesIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.multiplicationInterval(n, 57.295779513082322865);
},
};
protected degreesIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.DegreesIntervalOp);
}
/** Calculate an acceptance interval of degrees(x) */
public abstract readonly degreesInterval: (n: number) => FPInterval;
/**
* Calculate the minor of a NxN matrix.
*
* The ijth minor of a square matrix, is the N-1xN-1 matrix created by removing
* the ith column and jth row from the original matrix.
*/
private minorNxN(m: Array2D<number>, col: number, row: number): Array2D<number> {
const dim = m.length;
assert(m.length === m[0].length, `minorMatrix is only defined for square matrices`);
assert(col >= 0 && col < dim, `col ${col} needs be in [0, # of columns '${dim}')`);
assert(row >= 0 && row < dim, `row ${row} needs be in [0, # of rows '${dim}')`);
const result: number[][] = [...Array(dim - 1)].map(_ => [...Array(dim - 1)]);
const col_indices: readonly number[] = [...Array(dim).keys()].filter(e => e !== col);
const row_indices: readonly number[] = [...Array(dim).keys()].filter(e => e !== row);
col_indices.forEach((c, i) => {
row_indices.forEach((r, j) => {
result[i][j] = m[c][r];
});
});
return result;
}
/** Calculate an acceptance interval for determinant(m), where m is a 2x2 matrix */
private determinant2x2Interval(m: Array2D<number>): FPInterval {
assert(
m.length === m[0].length && m.length === 2,
`determinant2x2Interval called on non-2x2 matrix`
);
return this.subtractionInterval(
this.multiplicationInterval(m[0][0], m[1][1]),
this.multiplicationInterval(m[0][1], m[1][0])
);
}
/** Calculate an acceptance interval for determinant(m), where m is a 3x3 matrix */
private determinant3x3Interval(m: Array2D<number>): FPInterval {
assert(
m.length === m[0].length && m.length === 3,
`determinant3x3Interval called on non-3x3 matrix`
);
// M is a 3x3 matrix
// det(M) is A + B + C, where A, B, C are three elements in a row/column times
// their own co-factor.
// (The co-factor is the determinant of the minor of that position with the
// appropriate +/-)
// For simplicity sake A, B, C are calculated as the elements of the first
// column
const A = this.multiplicationInterval(
m[0][0],
this.determinant2x2Interval(this.minorNxN(m, 0, 0))
);
const B = this.multiplicationInterval(
-m[0][1],
this.determinant2x2Interval(this.minorNxN(m, 0, 1))
);
const C = this.multiplicationInterval(
m[0][2],
this.determinant2x2Interval(this.minorNxN(m, 0, 2))
);
// Need to calculate permutations, since for fp addition is not associative,
// so A + B + C is not guaranteed to equal B + C + A, etc.
const permutations: ROArrayArray<FPInterval> = calculatePermutations([A, B, C]);
return this.spanIntervals(
...permutations.map(p =>
p.reduce((prev: FPInterval, cur: FPInterval) => this.additionInterval(prev, cur))
)
);
}
/** Calculate an acceptance interval for determinant(m), where m is a 4x4 matrix */
private determinant4x4Interval(m: Array2D<number>): FPInterval {
assert(
m.length === m[0].length && m.length === 4,
`determinant3x3Interval called on non-4x4 matrix`
);
// M is a 4x4 matrix
// det(M) is A + B + C + D, where A, B, C, D are four elements in a row/column
// times their own co-factor.
// (The co-factor is the determinant of the minor of that position with the
// appropriate +/-)
// For simplicity sake A, B, C, D are calculated as the elements of the
// first column
const A = this.multiplicationInterval(
m[0][0],
this.determinant3x3Interval(this.minorNxN(m, 0, 0))
);
const B = this.multiplicationInterval(
-m[0][1],
this.determinant3x3Interval(this.minorNxN(m, 0, 1))
);
const C = this.multiplicationInterval(
m[0][2],
this.determinant3x3Interval(this.minorNxN(m, 0, 2))
);
const D = this.multiplicationInterval(
-m[0][3],
this.determinant3x3Interval(this.minorNxN(m, 0, 3))
);
// Need to calculate permutations, since for fp addition is not associative
// so A + B + C + D is not guaranteed to equal B + C + A + D, etc.
const permutations: ROArrayArray<FPInterval> = calculatePermutations([A, B, C, D]);
return this.spanIntervals(
...permutations.map(p =>
p.reduce((prev: FPInterval, cur: FPInterval) => this.additionInterval(prev, cur))
)
);
}
/**
* This code calculates 3x3 and 4x4 determinants using the textbook co-factor
* method, using the first column for the co-factor selection.
*
* For matrices composed of integer elements, e, with |e|^4 < 2**21, this
* should be fine.
*
* For e, where e is subnormal or 4*(e^4) might not be precisely expressible as
* a f32 values, this approach breaks down, because the rule of all co-factor
* definitions of determinant being equal doesn't hold in these cases.
*
* The general solution for this is to calculate all the permutations of the
* operations in the worked out formula for determinant.
* For 3x3 this is tractable, but for 4x4 this works out to ~23! permutations
* that need to be calculated.
* Thus, CTS testing and the spec definition of accuracy is restricted to the
* space that the simple implementation is valid.
*/
protected determinantIntervalImpl(x: Array2D<number>): FPInterval {
const dim = x.length;
assert(
x[0].length === dim && (dim === 2 || dim === 3 || dim === 4),
`determinantInterval only defined for 2x2, 3x3 and 4x4 matrices`
);
switch (dim) {
case 2:
return this.determinant2x2Interval(x);
case 3:
return this.determinant3x3Interval(x);
case 4:
return this.determinant4x4Interval(x);
}
unreachable(
"determinantInterval called on x, where which has an unexpected dimension of '${dim}'"
);
}
/** Calculate an acceptance interval for determinant(x) */
public abstract readonly determinantInterval: (x: Array2D<number>) => FPInterval;
private readonly DistanceIntervalScalarOp: ScalarPairToIntervalOp = {
impl: (x: number, y: number): FPInterval => {
return this.lengthInterval(this.subtractionInterval(x, y));
},
};
private readonly DistanceIntervalVectorOp: VectorPairToIntervalOp = {
impl: (x: readonly number[], y: readonly number[]): FPInterval => {
return this.lengthInterval(
this.runScalarPairToIntervalOpVectorComponentWise(
this.toVector(x),
this.toVector(y),
this.SubtractionIntervalOp
)
);
},
};
protected distanceIntervalImpl(
x: number | readonly number[],
y: number | readonly number[]
): FPInterval {
if (x instanceof Array && y instanceof Array) {
assert(
x.length === y.length,
`distanceInterval requires both params to have the same number of elements`
);
return this.runVectorPairToIntervalOp(
this.toVector(x),
this.toVector(y),
this.DistanceIntervalVectorOp
);
} else if (!(x instanceof Array) && !(y instanceof Array)) {
return this.runScalarPairToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.DistanceIntervalScalarOp
);
}
unreachable(
`distanceInterval requires both params to both the same type, either scalars or vectors`
);
}
/** Calculate an acceptance interval of distance(x, y) */
public abstract readonly distanceInterval: (
x: number | readonly number[],
y: number | readonly number[]
) => FPInterval;
// This op is implemented differently for f32 and f16.
private DivisionIntervalOpBuilder(): ScalarPairToIntervalOp {
const constants = this.constants();
const domain_x = [this.toInterval([constants.negative.min, constants.positive.max])];
const domain_y =
this.kind === 'f32' || this.kind === 'abstract'
? [this.toInterval([-(2 ** 126), -(2 ** -126)]), this.toInterval([2 ** -126, 2 ** 126])]
: [this.toInterval([-(2 ** 14), -(2 ** -14)]), this.toInterval([2 ** -14, 2 ** 14])];
return {
impl: (x: number, y: number): FPInterval => {
if (y === 0) {
return constants.unboundedInterval;
}
return this.ulpInterval(x / y, 2.5);
},
extrema: (x: FPInterval, y: FPInterval): [FPInterval, FPInterval] => {
// division has a discontinuity at y = 0.
if (y.contains(0)) {
y = this.toInterval(0);
}
return [x, y];
},
domain: () => {
return { x: domain_x, y: domain_y };
},
};
}
protected divisionIntervalImpl(x: number | FPInterval, y: number | FPInterval): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.DivisionIntervalOpBuilder()
);
}
/** Calculate an acceptance interval of x / y */
public abstract readonly divisionInterval: (
x: number | FPInterval,
y: number | FPInterval
) => FPInterval;
private readonly DotIntervalOp: VectorPairToIntervalOp = {
impl: (x: readonly number[], y: readonly number[]): FPInterval => {
// dot(x, y) = sum of x[i] * y[i]
const multiplications = this.runScalarPairToIntervalOpVectorComponentWise(
this.toVector(x),
this.toVector(y),
this.MultiplicationIntervalOp
);
// vec2 doesn't require permutations, since a + b = b + a for floats
if (multiplications.length === 2) {
return this.additionInterval(multiplications[0], multiplications[1]);
}
// The spec does not state the ordering of summation, so all the
// permutations are calculated and their results spanned, since addition
// of more than two floats is not transitive, i.e. a + b + c is not
// guaranteed to equal b + a + c
const permutations: ROArrayArray<FPInterval> = calculatePermutations(multiplications);
return this.spanIntervals(
...permutations.map(p => p.reduce((prev, cur) => this.additionInterval(prev, cur)))
);
},
};
protected dotIntervalImpl(
x: readonly number[] | readonly FPInterval[],
y: readonly number[] | readonly FPInterval[]
): FPInterval {
assert(
x.length === y.length,
`dot not defined for vectors with different lengths, x = ${x}, y = ${y}`
);
return this.runVectorPairToIntervalOp(this.toVector(x), this.toVector(y), this.DotIntervalOp);
}
/** Calculated the acceptance interval for dot(x, y) */
public abstract readonly dotInterval: (
x: readonly number[] | readonly FPInterval[],
y: readonly number[] | readonly FPInterval[]
) => FPInterval;
private readonly ExpIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
assert(this.kind === 'f32' || this.kind === 'f16');
const ulp_error = this.kind === 'f32' ? 3 + 2 * Math.abs(n) : 1 + 2 * Math.abs(n);
return this.ulpInterval(Math.exp(n), ulp_error);
},
};
protected expIntervalImpl(x: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(x), this.ExpIntervalOp);
}
/** Calculate an acceptance interval for exp(x) */
public abstract readonly expInterval: (x: number | FPInterval) => FPInterval;
private readonly Exp2IntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
assert(this.kind === 'f32' || this.kind === 'f16');
const ulp_error = this.kind === 'f32' ? 3 + 2 * Math.abs(n) : 1 + 2 * Math.abs(n);
return this.ulpInterval(Math.pow(2, n), ulp_error);
},
};
protected exp2IntervalImpl(x: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(x), this.Exp2IntervalOp);
}
/** Calculate an acceptance interval for exp2(x) */
public abstract readonly exp2Interval: (x: number | FPInterval) => FPInterval;
/**
* faceForward(x, y, z) = select(-x, x, dot(z, y) < 0.0)
*
* This builtin selects from two discrete results (delta rounding/flushing),
* so the majority of the framework code is not appropriate, since the
* framework attempts to span results.
*
* Thus, a bespoke implementation is used instead of
* defining an Op and running that through the framework.
*/
protected faceForwardIntervalsImpl(
x: readonly number[],
y: readonly number[],
z: readonly number[]
): (FPVector | undefined)[] {
const x_vec = this.toVector(x);
// Running vector through this.runScalarToIntervalOpComponentWise to make
// sure that flushing/rounding is handled, since toVector does not perform
// those operations.
const positive_x = this.runScalarToIntervalOpComponentWise(x_vec, {
impl: (i: number): FPInterval => {
return this.toInterval(i);
},
});
const negative_x = this.runScalarToIntervalOpComponentWise(x_vec, this.NegationIntervalOp);
const dot_interval = this.dotInterval(z, y);
const results: (FPVector | undefined)[] = [];
if (!dot_interval.isFinite()) {
// dot calculation went out of bounds
// Inserting undefined in the result, so that the test running framework
// is aware of this potential OOB.
// For const-eval tests, it means that the test case should be skipped,
// since the shader will fail to compile.
// For non-const-eval the undefined should be stripped out of the possible
// results.
results.push(undefined);
}
// Because the result of dot can be an interval, it might span across 0, thus
// it is possible that both -x and x are valid responses.
if (dot_interval.begin < 0 || dot_interval.end < 0) {
results.push(positive_x);
}
if (dot_interval.begin >= 0 || dot_interval.end >= 0) {
results.push(negative_x);
}
assert(
results.length > 0 || results.every(r => r === undefined),
`faceForwardInterval selected neither positive x or negative x for the result, this shouldn't be possible`
);
return results;
}
/** Calculate the acceptance intervals for faceForward(x, y, z) */
public abstract readonly faceForwardIntervals: (
x: readonly number[],
y: readonly number[],
z: readonly number[]
) => (FPVector | undefined)[];
private readonly FloorIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.correctlyRoundedInterval(Math.floor(n));
},
};
protected floorIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.FloorIntervalOp);
}
/** Calculate an acceptance interval of floor(x) */
public abstract readonly floorInterval: (n: number) => FPInterval;
private readonly FmaIntervalOp: ScalarTripleToIntervalOp = {
impl: (x: number, y: number, z: number): FPInterval => {
return this.additionInterval(this.multiplicationInterval(x, y), z);
},
};
protected fmaIntervalImpl(x: number, y: number, z: number): FPInterval {
return this.runScalarTripleToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.toInterval(z),
this.FmaIntervalOp
);
}
/** Calculate an acceptance interval for fma(x, y, z) */
public abstract readonly fmaInterval: (x: number, y: number, z: number) => FPInterval;
private readonly FractIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
// fract(x) = x - floor(x) is defined in the spec.
// For people coming from a non-graphics background this will cause some
// unintuitive results. For example,
// fract(-1.1) is not 0.1 or -0.1, but instead 0.9.
// This is how other shading languages operate and allows for a desirable
// wrap around in graphics programming.
const result = this.subtractionInterval(n, this.floorInterval(n));
assert(
// negative.subnormal.min instead of 0, because FTZ can occur
// selectively during the calculation
this.toInterval([this.constants().negative.subnormal.min, 1.0]).contains(result),
`fract(${n}) interval [${result}] unexpectedly extends beyond [~0.0, 1.0]`
);
if (result.contains(1)) {
// Very small negative numbers can lead to catastrophic cancellation,
// thus calculating a fract of 1.0, which is technically not a
// fractional part, so some implementations clamp the result to next
// nearest number.
return this.spanIntervals(result, this.toInterval(this.constants().positive.less_than_one));
}
return result;
},
};
protected fractIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.FractIntervalOp);
}
/** Calculate an acceptance interval of fract(x) */
public abstract readonly fractInterval: (n: number) => FPInterval;
private readonly InverseSqrtIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.ulpInterval(1 / Math.sqrt(n), 2);
},
domain: () => {
return this.constants().greaterThanZeroInterval;
},
};
protected inverseSqrtIntervalImpl(n: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.InverseSqrtIntervalOp);
}
/** Calculate an acceptance interval of inverseSqrt(x) */
public abstract readonly inverseSqrtInterval: (n: number | FPInterval) => FPInterval;
private readonly LdexpIntervalOp: ScalarPairToIntervalOp = {
impl: (e1: number, e2: number) => {
assert(Number.isInteger(e2), 'the second param of ldexp must be an integer');
// Spec explicitly calls indeterminate value if e2 > bias + 1
if (e2 > this.constants().bias + 1) {
return this.constants().unboundedInterval;
}
// The spec says the result of ldexp(e1, e2) = e1 * 2 ^ e2, and the
// accuracy is correctly rounded to the true value, so the inheritance
// framework does not need to be invoked to determine endpoints.
// Instead, the value at a higher precision is calculated and passed to
// correctlyRoundedInterval.
const result = e1 * 2 ** e2;
if (!Number.isFinite(result)) {
// Overflowed TS's number type, so definitely out of bounds
return this.constants().unboundedInterval;
}
// The result may be zero if e2 + bias <= 0, but we can't simply span the interval to 0.0.
// For example, for f32 input e1 = 2**120 and e2 = -130, e2 + bias = -3 <= 0, but
// e1 * 2 ** e2 = 2**-10, so the valid result is 2**-10 or 0.0, instead of [0.0, 2**-10].
// Always return the correctly-rounded interval, and special examination should be taken when
// using the result.
return this.correctlyRoundedInterval(result);
},
};
protected ldexpIntervalImpl(e1: number, e2: number): FPInterval {
// Only round and flush e1, as e2 is of integer type (i32 or abstract integer) and should be
// precise.
return this.roundAndFlushScalarToInterval(e1, {
impl: (e1: number) => this.LdexpIntervalOp.impl(e1, e2),
});
}
/**
* Calculate an acceptance interval of ldexp(e1, e2), where e2 is integer
*
* Spec indicate that the result may be zero if e2 + bias <= 0, no matter how large
* was e1 * 2 ** e2, i.e. the actual valid result is correctlyRounded(e1 * 2 ** e2) or 0.0, if
* e2 + bias <= 0. Such discontinious flush-to-zero behavior is hard to be expressed using
* FPInterval, therefore in the situation of e2 + bias <= 0 the returned interval would be just
* correctlyRounded(e1 * 2 ** e2), and special examination should be taken when using the result.
*
*/
public abstract readonly ldexpInterval: (e1: number, e2: number) => FPInterval;
private readonly LengthIntervalScalarOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.sqrtInterval(this.multiplicationInterval(n, n));
},
};
private readonly LengthIntervalVectorOp: VectorToIntervalOp = {
impl: (n: readonly number[]): FPInterval => {
return this.sqrtInterval(this.dotInterval(n, n));
},
};
protected lengthIntervalImpl(n: number | FPInterval | readonly number[] | FPVector): FPInterval {
if (n instanceof Array) {
return this.runVectorToIntervalOp(this.toVector(n), this.LengthIntervalVectorOp);
} else {
return this.runScalarToIntervalOp(this.toInterval(n), this.LengthIntervalScalarOp);
}
}
/** Calculate an acceptance interval of length(x) */
public abstract readonly lengthInterval: (
n: number | FPInterval | readonly number[] | FPVector
) => FPInterval;
private readonly LogIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
assert(this.kind === 'f32' || this.kind === 'f16');
const abs_error = this.kind === 'f32' ? 2 ** -21 : 2 ** -7;
if (n >= 0.5 && n <= 2.0) {
return this.absoluteErrorInterval(Math.log(n), abs_error);
}
return this.ulpInterval(Math.log(n), 3);
},
domain: () => {
return this.constants().greaterThanZeroInterval;
},
};
protected logIntervalImpl(x: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(x), this.LogIntervalOp);
}
/** Calculate an acceptance interval of log(x) */
public abstract readonly logInterval: (x: number | FPInterval) => FPInterval;
private readonly Log2IntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
assert(this.kind === 'f32' || this.kind === 'f16');
const abs_error = this.kind === 'f32' ? 2 ** -21 : 2 ** -7;
if (n >= 0.5 && n <= 2.0) {
return this.absoluteErrorInterval(Math.log2(n), abs_error);
}
return this.ulpInterval(Math.log2(n), 3);
},
domain: () => {
return this.constants().greaterThanZeroInterval;
},
};
protected log2IntervalImpl(x: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(x), this.Log2IntervalOp);
}
/** Calculate an acceptance interval of log2(x) */
public abstract readonly log2Interval: (x: number | FPInterval) => FPInterval;
private readonly MaxIntervalOp: ScalarPairToIntervalOp = {
impl: (x: number, y: number): FPInterval => {
// If both of the inputs are subnormal, then either of the inputs can be returned
if (this.isSubnormal(x) && this.isSubnormal(y)) {
return this.correctlyRoundedInterval(
this.spanIntervals(this.toInterval(x), this.toInterval(y))
);
}
return this.correctlyRoundedInterval(Math.max(x, y));
},
};
protected maxIntervalImpl(x: number | FPInterval, y: number | FPInterval): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.MaxIntervalOp
);
}
/** Calculate an acceptance interval of max(x, y) */
public abstract readonly maxInterval: (
x: number | FPInterval,
y: number | FPInterval
) => FPInterval;
private readonly MinIntervalOp: ScalarPairToIntervalOp = {
impl: (x: number, y: number): FPInterval => {
// If both of the inputs are subnormal, then either of the inputs can be returned
if (this.isSubnormal(x) && this.isSubnormal(y)) {
return this.correctlyRoundedInterval(
this.spanIntervals(this.toInterval(x), this.toInterval(y))
);
}
return this.correctlyRoundedInterval(Math.min(x, y));
},
};
protected minIntervalImpl(x: number | FPInterval, y: number | FPInterval): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.MinIntervalOp
);
}
/** Calculate an acceptance interval of min(x, y) */
public abstract readonly minInterval: (
x: number | FPInterval,
y: number | FPInterval
) => FPInterval;
private readonly MixImpreciseIntervalOp: ScalarTripleToIntervalOp = {
impl: (x: number, y: number, z: number): FPInterval => {
// x + (y - x) * z =
// x + t, where t = (y - x) * z
const t = this.multiplicationInterval(this.subtractionInterval(y, x), z);
return this.additionInterval(x, t);
},
};
protected mixImpreciseIntervalImpl(x: number, y: number, z: number): FPInterval {
return this.runScalarTripleToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.toInterval(z),
this.MixImpreciseIntervalOp
);
}
/** Calculate an acceptance interval of mix(x, y, z) using x + (y - x) * z */
public abstract readonly mixImpreciseInterval: (x: number, y: number, z: number) => FPInterval;
private readonly MixPreciseIntervalOp: ScalarTripleToIntervalOp = {
impl: (x: number, y: number, z: number): FPInterval => {
// x * (1.0 - z) + y * z =
// t + s, where t = x * (1.0 - z), s = y * z
const t = this.multiplicationInterval(x, this.subtractionInterval(1.0, z));
const s = this.multiplicationInterval(y, z);
return this.additionInterval(t, s);
},
};
protected mixPreciseIntervalImpl(x: number, y: number, z: number): FPInterval {
return this.runScalarTripleToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.toInterval(z),
this.MixPreciseIntervalOp
);
}
/** Calculate an acceptance interval of mix(x, y, z) using x * (1.0 - z) + y * z */
public abstract readonly mixPreciseInterval: (x: number, y: number, z: number) => FPInterval;
/** All acceptance interval functions for mix(x, y, z) */
public abstract readonly mixIntervals: ScalarTripleToInterval[];
protected modfIntervalImpl(n: number): { fract: FPInterval; whole: FPInterval } {
const fract = this.correctlyRoundedInterval(n % 1.0);
const whole = this.correctlyRoundedInterval(n - (n % 1.0));
return { fract, whole };
}
/** Calculate an acceptance interval of modf(x) */
public abstract readonly modfInterval: (n: number) => { fract: FPInterval; whole: FPInterval };
private readonly MultiplicationInnerOp = {
impl: (x: number, y: number): FPInterval => {
return this.correctlyRoundedInterval(x * y);
},
};
private readonly MultiplicationIntervalOp: ScalarPairToIntervalOp = {
impl: (x: number, y: number): FPInterval => {
return this.roundAndFlushScalarPairToInterval(x, y, this.MultiplicationInnerOp);
},
};
protected multiplicationIntervalImpl(x: number | FPInterval, y: number | FPInterval): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.MultiplicationIntervalOp
);
}
/** Calculate an acceptance interval of x * y */
public abstract readonly multiplicationInterval: (
x: number | FPInterval,
y: number | FPInterval
) => FPInterval;
/**
* @returns the vector result of multiplying the given vector by the given
* scalar
*/
private multiplyVectorByScalar(v: readonly number[], c: number | FPInterval): FPVector {
return this.toVector(v.map(x => this.multiplicationInterval(x, c)));
}
protected multiplicationMatrixScalarIntervalImpl(mat: Array2D<number>, scalar: number): FPMatrix {
return this.runScalarPairToIntervalOpScalarMatrixComponentWise(
this.toInterval(scalar),
this.toMatrix(mat),
this.MultiplicationIntervalOp
);
}
/** Calculate an acceptance interval of x * y, when x is a matrix and y is a scalar */
public abstract readonly multiplicationMatrixScalarInterval: (
mat: Array2D<number>,
scalar: number
) => FPMatrix;
protected multiplicationScalarMatrixIntervalImpl(scalar: number, mat: Array2D<number>): FPMatrix {
return this.multiplicationMatrixScalarInterval(mat, scalar);
}
/** Calculate an acceptance interval of x * y, when x is a scalar and y is a matrix */
public abstract readonly multiplicationScalarMatrixInterval: (
scalar: number,
mat: Array2D<number>
) => FPMatrix;
protected multiplicationMatrixMatrixIntervalImpl(
mat_x: Array2D<number>,
mat_y: Array2D<number>
): FPMatrix {
const x_cols = mat_x.length;
const x_rows = mat_x[0].length;
const y_cols = mat_y.length;
const y_rows = mat_y[0].length;
assert(x_cols === y_rows, `'mat${x_cols}x${x_rows} * mat${y_cols}x${y_rows}' is not defined`);
const x_transposed = this.transposeInterval(mat_x);
let oob_result: boolean = false;
const result: FPInterval[][] = [...Array(y_cols)].map(_ => [...Array(x_rows)]);
mat_y.forEach((y, i) => {
x_transposed.forEach((x, j) => {
result[i][j] = this.dotInterval(x, y);
if (!oob_result && !result[i][j].isFinite()) {
oob_result = true;
}
});
});
if (oob_result) {
return this.constants().unboundedMatrix[result.length as 2 | 3 | 4][
result[0].length as 2 | 3 | 4
];
}
return result as ROArrayArray<FPInterval> as FPMatrix;
}
/** Calculate an acceptance interval of x * y, when x is a matrix and y is a matrix */
public abstract readonly multiplicationMatrixMatrixInterval: (
mat_x: Array2D<number>,
mat_y: Array2D<number>
) => FPMatrix;
protected multiplicationMatrixVectorIntervalImpl(
x: Array2D<number>,
y: readonly number[]
): FPVector {
const cols = x.length;
const rows = x[0].length;
assert(y.length === cols, `'mat${cols}x${rows} * vec${y.length}' is not defined`);
return this.transposeInterval(x).map(e => this.dotInterval(e, y)) as FPVector;
}
/** Calculate an acceptance interval of x * y, when x is a matrix and y is a vector */
public abstract readonly multiplicationMatrixVectorInterval: (
x: Array2D<number>,
y: readonly number[]
) => FPVector;
protected multiplicationVectorMatrixIntervalImpl(
x: readonly number[],
y: Array2D<number>
): FPVector {
const cols = y.length;
const rows = y[0].length;
assert(x.length === rows, `'vec${x.length} * mat${cols}x${rows}' is not defined`);
return y.map(e => this.dotInterval(x, e)) as FPVector;
}
/** Calculate an acceptance interval of x * y, when x is a vector and y is a matrix */
public abstract readonly multiplicationVectorMatrixInterval: (
x: readonly number[],
y: Array2D<number>
) => FPVector;
private readonly NegationIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.correctlyRoundedInterval(-n);
},
};
protected negationIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.NegationIntervalOp);
}
/** Calculate an acceptance interval of -x */
public abstract readonly negationInterval: (n: number) => FPInterval;
private readonly NormalizeIntervalOp: VectorToVectorOp = {
impl: (n: readonly number[]): FPVector => {
const length = this.lengthInterval(n);
const result = this.toVector(n.map(e => this.divisionInterval(e, length)));
if (result.some(r => !r.isFinite())) {
return this.constants().unboundedVector[result.length];
}
return result;
},
};
protected normalizeIntervalImpl(n: readonly number[]): FPVector {
return this.runVectorToVectorOp(this.toVector(n), this.NormalizeIntervalOp);
}
public abstract readonly normalizeInterval: (n: readonly number[]) => FPVector;
private readonly PowIntervalOp: ScalarPairToIntervalOp = {
// pow(x, y) has no explicit domain restrictions, but inherits the x <= 0
// domain restriction from log2(x). Invoking log2Interval(x) in impl will
// enforce this, so there is no need to wrap the impl call here.
impl: (x: number, y: number): FPInterval => {
return this.exp2Interval(this.multiplicationInterval(y, this.log2Interval(x)));
},
};
protected powIntervalImpl(x: number | FPInterval, y: number | FPInterval): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.PowIntervalOp
);
}
/** Calculate an acceptance interval of pow(x, y) */
public abstract readonly powInterval: (
x: number | FPInterval,
y: number | FPInterval
) => FPInterval;
private readonly RadiansIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.multiplicationInterval(n, 0.017453292519943295474);
},
};
protected radiansIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.RadiansIntervalOp);
}
/** Calculate an acceptance interval of radians(x) */
public abstract readonly radiansInterval: (n: number) => FPInterval;
private readonly ReflectIntervalOp: VectorPairToVectorOp = {
impl: (x: readonly number[], y: readonly number[]): FPVector => {
assert(
x.length === y.length,
`ReflectIntervalOp received x (${x}) and y (${y}) with different numbers of elements`
);
// reflect(x, y) = x - 2.0 * dot(x, y) * y
// = x - t * y, t = 2.0 * dot(x, y)
// x = incident vector
// y = normal of reflecting surface
const t = this.multiplicationInterval(2.0, this.dotInterval(x, y));
const rhs = this.multiplyVectorByScalar(y, t);
const result = this.runScalarPairToIntervalOpVectorComponentWise(
this.toVector(x),
rhs,
this.SubtractionIntervalOp
);
if (result.some(r => !r.isFinite())) {
return this.constants().unboundedVector[result.length];
}
return result;
},
};
protected reflectIntervalImpl(x: readonly number[], y: readonly number[]): FPVector {
assert(
x.length === y.length,
`reflect is only defined for vectors with the same number of elements`
);
return this.runVectorPairToVectorOp(this.toVector(x), this.toVector(y), this.ReflectIntervalOp);
}
/** Calculate an acceptance interval of reflect(x, y) */
public abstract readonly reflectInterval: (
x: readonly number[],
y: readonly number[]
) => FPVector;
/**
* refract is a singular function in the sense that it is the only builtin that
* takes in (FPVector, FPVector, F32/F16) and returns FPVector and is basically
* defined in terms of other functions.
*
* Instead of implementing all the framework code to integrate it with its
* own operation type, etc, it instead has a bespoke implementation that is a
* composition of other builtin functions that use the framework.
*/
protected refractIntervalImpl(i: readonly number[], s: readonly number[], r: number): FPVector {
assert(
i.length === s.length,
`refract is only defined for vectors with the same number of elements`
);
const r_squared = this.multiplicationInterval(r, r);
const dot = this.dotInterval(s, i);
const dot_squared = this.multiplicationInterval(dot, dot);
const one_minus_dot_squared = this.subtractionInterval(1, dot_squared);
const k = this.subtractionInterval(
1.0,
this.multiplicationInterval(r_squared, one_minus_dot_squared)
);
if (!k.isFinite() || k.containsZeroOrSubnormals()) {
// There is a discontinuity at k == 0, due to sqrt(k) being calculated, so exiting early
return this.constants().unboundedVector[this.toVector(i).length];
}
if (k.end < 0.0) {
// if k is negative, then the zero vector is the valid response
return this.constants().zeroVector[this.toVector(i).length];
}
const dot_times_r = this.multiplicationInterval(dot, r);
const k_sqrt = this.sqrtInterval(k);
const t = this.additionInterval(dot_times_r, k_sqrt); // t = r * dot(i, s) + sqrt(k)
const result = this.runScalarPairToIntervalOpVectorComponentWise(
this.multiplyVectorByScalar(i, r),
this.multiplyVectorByScalar(s, t),
this.SubtractionIntervalOp
); // (i * r) - (s * t)
if (result.some(r => !r.isFinite())) {
return this.constants().unboundedVector[result.length];
}
return result;
}
/** Calculate acceptance interval vectors of reflect(i, s, r) */
public abstract readonly refractInterval: (
i: readonly number[],
s: readonly number[],
r: number
) => FPVector;
private readonly RemainderIntervalOp: ScalarPairToIntervalOp = {
impl: (x: number, y: number): FPInterval => {
// x % y = x - y * trunc(x/y)
return this.subtractionInterval(
x,
this.multiplicationInterval(y, this.truncInterval(this.divisionInterval(x, y)))
);
},
};
/** Calculate an acceptance interval for x % y */
protected remainderIntervalImpl(x: number, y: number): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.RemainderIntervalOp
);
}
/** Calculate an acceptance interval for x % y */
public abstract readonly remainderInterval: (x: number, y: number) => FPInterval;
private readonly RoundIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
const k = Math.floor(n);
const diff_before = n - k;
const diff_after = k + 1 - n;
if (diff_before < diff_after) {
return this.correctlyRoundedInterval(k);
} else if (diff_before > diff_after) {
return this.correctlyRoundedInterval(k + 1);
}
// n is in the middle of two integers.
// The tie breaking rule is 'k if k is even, k + 1 if k is odd'
if (k % 2 === 0) {
return this.correctlyRoundedInterval(k);
}
return this.correctlyRoundedInterval(k + 1);
},
};
protected roundIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.RoundIntervalOp);
}
/** Calculate an acceptance interval of round(x) */
public abstract readonly roundInterval: (n: number) => FPInterval;
/**
* The definition of saturate does not specify which version of clamp to use.
* Using min-max here, since it has wider acceptance intervals, that include
* all of median's.
*/
protected saturateIntervalImpl(n: number): FPInterval {
return this.runScalarTripleToIntervalOp(
this.toInterval(n),
this.toInterval(0.0),
this.toInterval(1.0),
this.ClampMinMaxIntervalOp
);
}
/*** Calculate an acceptance interval of saturate(n) as clamp(n, 0.0, 1.0) */
public abstract readonly saturateInterval: (n: number) => FPInterval;
private readonly SignIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
if (n > 0.0) {
return this.correctlyRoundedInterval(1.0);
}
if (n < 0.0) {
return this.correctlyRoundedInterval(-1.0);
}
return this.correctlyRoundedInterval(0.0);
},
};
protected signIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.SignIntervalOp);
}
/** Calculate an acceptance interval of sign(x) */
public abstract readonly signInterval: (n: number) => FPInterval;
private readonly SinIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
assert(this.kind === 'f32' || this.kind === 'f16');
const abs_error = this.kind === 'f32' ? 2 ** -11 : 2 ** -7;
return this.absoluteErrorInterval(Math.sin(n), abs_error);
},
domain: () => {
return this.constants().negPiToPiInterval;
},
};
protected sinIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.SinIntervalOp);
}
/** Calculate an acceptance interval of sin(x) */
public abstract readonly sinInterval: (n: number) => FPInterval;
private readonly SinhIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
// sinh(x) = (exp(x) - exp(-x)) * 0.5
const minus_n = this.negationInterval(n);
return this.multiplicationInterval(
this.subtractionInterval(this.expInterval(n), this.expInterval(minus_n)),
0.5
);
},
};
protected sinhIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.SinhIntervalOp);
}
/** Calculate an acceptance interval of sinh(x) */
public abstract readonly sinhInterval: (n: number) => FPInterval;
private readonly SmoothStepOp: ScalarTripleToIntervalOp = {
impl: (low: number, high: number, x: number): FPInterval => {
// For clamp(foo, 0.0, 1.0) the different implementations of clamp provide
// the same value, so arbitrarily picking the minmax version to use.
// t = clamp((x - low) / (high - low), 0.0, 1.0)
// prettier-ignore
const t = this.clampMedianInterval(
this.divisionInterval(
this.subtractionInterval(x, low),
this.subtractionInterval(high, low)),
0.0,
1.0);
// Inherited from t * t * (3.0 - 2.0 * t)
// prettier-ignore
return this.multiplicationInterval(
t,
this.multiplicationInterval(t,
this.subtractionInterval(3.0,
this.multiplicationInterval(2.0, t))));
},
};
protected smoothStepIntervalImpl(low: number, high: number, x: number): FPInterval {
return this.runScalarTripleToIntervalOp(
this.toInterval(low),
this.toInterval(high),
this.toInterval(x),
this.SmoothStepOp
);
}
/** Calculate an acceptance interval of smoothStep(low, high, x) */
public abstract readonly smoothStepInterval: (low: number, high: number, x: number) => FPInterval;
private readonly SqrtIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.divisionInterval(1.0, this.inverseSqrtInterval(n));
},
};
protected sqrtIntervalImpl(n: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.SqrtIntervalOp);
}
/** Calculate an acceptance interval of sqrt(x) */
public abstract readonly sqrtInterval: (n: number | FPInterval) => FPInterval;
private readonly StepIntervalOp: ScalarPairToIntervalOp = {
impl: (edge: number, x: number): FPInterval => {
if (edge <= x) {
return this.correctlyRoundedInterval(1.0);
}
return this.correctlyRoundedInterval(0.0);
},
};
protected stepIntervalImpl(edge: number, x: number): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(edge),
this.toInterval(x),
this.StepIntervalOp
);
}
/**
* Calculate an acceptance 'interval' for step(edge, x)
*
* step only returns two possible values, so its interval requires special
* interpretation in CTS tests.
* This interval will be one of four values: [0, 0], [0, 1], [1, 1] & [-∞, +∞].
* [0, 0] and [1, 1] indicate that the correct answer in point they encapsulate.
* [0, 1] should not be treated as a span, i.e. 0.1 is acceptable, but instead
* indicate either 0.0 or 1.0 are acceptable answers.
* [-∞, +∞] is treated as unbounded interval, since an unbounded or
* infinite value was passed in.
*/
public abstract readonly stepInterval: (edge: number, x: number) => FPInterval;
private readonly SubtractionIntervalOp: ScalarPairToIntervalOp = {
impl: (x: number, y: number): FPInterval => {
const difference: number = x - y;
// To support unbounded precision we need to handle the special case for very large vs small values
// We can resuse the function that is used by addition since floating point is symmetric for negative
// and positive values.
const large_val = Math.abs(x) > Math.abs(y) ? x : -y;
const small_val = Math.abs(x) > Math.abs(y) ? -y : x;
return this.correctlyRoundedIntervalWithUnboundedPrecisionForAddition(
difference,
large_val,
small_val
);
},
};
protected subtractionIntervalImpl(x: number | FPInterval, y: number | FPInterval): FPInterval {
return this.runScalarPairToIntervalOp(
this.toInterval(x),
this.toInterval(y),
this.SubtractionIntervalOp
);
}
/** Calculate an acceptance interval of x - y */
public abstract readonly subtractionInterval: (
x: number | FPInterval,
y: number | FPInterval
) => FPInterval;
protected subtractionMatrixMatrixIntervalImpl(x: Array2D<number>, y: Array2D<number>): FPMatrix {
return this.runScalarPairToIntervalOpMatrixMatrixComponentWise(
this.toMatrix(x),
this.toMatrix(y),
this.SubtractionIntervalOp
);
}
/** Calculate an acceptance interval of x - y, when x and y are matrices */
public abstract readonly subtractionMatrixMatrixInterval: (
x: Array2D<number>,
y: Array2D<number>
) => FPMatrix;
private readonly TanIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.divisionInterval(this.sinInterval(n), this.cosInterval(n));
},
};
protected tanIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.TanIntervalOp);
}
/** Calculate an acceptance interval of tan(x) */
public abstract readonly tanInterval: (n: number) => FPInterval;
private readonly TanhIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
const approx_abs_error = 1.0e-5;
return this.spanIntervals(
this.divisionInterval(this.sinhInterval(n), this.coshInterval(n)),
this.absoluteErrorInterval(Math.tanh(n), approx_abs_error)
);
},
};
protected tanhIntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.TanhIntervalOp);
}
/** Calculate an acceptance interval of tanh(x) */
public abstract readonly tanhInterval: (n: number) => FPInterval;
private readonly TransposeIntervalOp: MatrixToMatrixOp = {
impl: (m: Array2D<number>): FPMatrix => {
const num_cols = m.length;
const num_rows = m[0].length;
const result: FPInterval[][] = [...Array(num_rows)].map(_ => [...Array(num_cols)]);
for (let i = 0; i < num_cols; i++) {
for (let j = 0; j < num_rows; j++) {
result[j][i] = this.correctlyRoundedInterval(m[i][j]);
}
}
return this.toMatrix(result);
},
};
protected transposeIntervalImpl(m: Array2D<number>): FPMatrix {
return this.runMatrixToMatrixOp(this.toMatrix(m), this.TransposeIntervalOp);
}
/** Calculate an acceptance interval of transpose(m) */
public abstract readonly transposeInterval: (m: Array2D<number>) => FPMatrix;
private readonly TruncIntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
return this.correctlyRoundedInterval(Math.trunc(n));
},
};
protected truncIntervalImpl(n: number | FPInterval): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.TruncIntervalOp);
}
/** Calculate an acceptance interval of trunc(x) */
public abstract readonly truncInterval: (n: number | FPInterval) => FPInterval;
}
// Pre-defined values that get used multiple times in _constants' initializers. Cannot use FPTraits members, since this
// executes before they are defined.
const kF32UnboundedInterval = new FPInterval(
'f32',
Number.NEGATIVE_INFINITY,
Number.POSITIVE_INFINITY
);
const kF32ZeroInterval = new FPInterval('f32', 0);
class F32Traits extends FPTraits {
private static _constants: FPConstants = {
positive: {
min: kValue.f32.positive.min,
max: kValue.f32.positive.max,
infinity: kValue.f32.positive.infinity,
nearest_max: kValue.f32.positive.nearest_max,
less_than_one: kValue.f32.positive.less_than_one,
subnormal: {
min: kValue.f32.positive.subnormal.min,
max: kValue.f32.positive.subnormal.max,
},
pi: {
whole: kValue.f32.positive.pi.whole,
three_quarters: kValue.f32.positive.pi.three_quarters,
half: kValue.f32.positive.pi.half,
third: kValue.f32.positive.pi.third,
quarter: kValue.f32.positive.pi.quarter,
sixth: kValue.f32.positive.pi.sixth,
},
e: kValue.f32.positive.e,
},
negative: {
min: kValue.f32.negative.min,
max: kValue.f32.negative.max,
infinity: kValue.f32.negative.infinity,
nearest_min: kValue.f32.negative.nearest_min,
less_than_one: kValue.f32.negative.less_than_one,
subnormal: {
min: kValue.f32.negative.subnormal.min,
max: kValue.f32.negative.subnormal.max,
},
pi: {
whole: kValue.f32.negative.pi.whole,
three_quarters: kValue.f32.negative.pi.three_quarters,
half: kValue.f32.negative.pi.half,
third: kValue.f32.negative.pi.third,
quarter: kValue.f32.negative.pi.quarter,
sixth: kValue.f32.negative.pi.sixth,
},
},
bias: 127,
unboundedInterval: kF32UnboundedInterval,
zeroInterval: kF32ZeroInterval,
// Have to use the constants.ts values here, because values defined in the
// initializer cannot be referenced in the initializer
negPiToPiInterval: new FPInterval(
'f32',
kValue.f32.negative.pi.whole,
kValue.f32.positive.pi.whole
),
greaterThanZeroInterval: new FPInterval(
'f32',
kValue.f32.positive.subnormal.min,
kValue.f32.positive.max
),
negOneToOneInterval: new FPInterval('f32', -1, 1),
zeroVector: {
2: [kF32ZeroInterval, kF32ZeroInterval],
3: [kF32ZeroInterval, kF32ZeroInterval, kF32ZeroInterval],
4: [kF32ZeroInterval, kF32ZeroInterval, kF32ZeroInterval, kF32ZeroInterval],
},
unboundedVector: {
2: [kF32UnboundedInterval, kF32UnboundedInterval],
3: [kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
4: [
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
},
unboundedMatrix: {
2: {
2: [
[kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval],
],
3: [
[kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
],
4: [
[
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
[
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
],
},
3: {
2: [
[kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval],
],
3: [
[kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
],
4: [
[
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
[
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
[
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
],
},
4: {
2: [
[kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval],
],
3: [
[kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
[kF32UnboundedInterval, kF32UnboundedInterval, kF32UnboundedInterval],
],
4: [
[
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
[
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
[
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
[
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
kF32UnboundedInterval,
],
],
},
},
};
public constructor() {
super('f32');
}
public constants(): FPConstants {
return F32Traits._constants;
}
// Utilities - Overrides
public readonly quantize = quantizeToF32;
public readonly correctlyRounded = correctlyRoundedF32;
public readonly isFinite = isFiniteF32;
public readonly isSubnormal = isSubnormalNumberF32;
public readonly flushSubnormal = flushSubnormalNumberF32;
public readonly oneULP = oneULPF32;
public readonly scalarBuilder = f32;
public readonly scalarRange = scalarF32Range;
public readonly sparseScalarRange = sparseScalarF32Range;
public readonly vectorRange = vectorF32Range;
public readonly sparseVectorRange = sparseVectorF32Range;
public readonly sparseMatrixRange = sparseMatrixF32Range;
// Framework - Fundamental Error Intervals - Overrides
public readonly absoluteErrorInterval = this.absoluteErrorIntervalImpl.bind(this);
public readonly correctlyRoundedInterval = this.correctlyRoundedIntervalImpl.bind(this);
public readonly correctlyRoundedMatrix = this.correctlyRoundedMatrixImpl.bind(this);
public readonly ulpInterval = this.ulpIntervalImpl.bind(this);
// Framework - API - Overrides
public readonly absInterval = this.absIntervalImpl.bind(this);
public readonly acosInterval = this.acosIntervalImpl.bind(this);
public readonly acoshAlternativeInterval = this.acoshAlternativeIntervalImpl.bind(this);
public readonly acoshPrimaryInterval = this.acoshPrimaryIntervalImpl.bind(this);
public readonly acoshIntervals = [this.acoshAlternativeInterval, this.acoshPrimaryInterval];
public readonly additionInterval = this.additionIntervalImpl.bind(this);
public readonly additionMatrixMatrixInterval = this.additionMatrixMatrixIntervalImpl.bind(this);
public readonly asinInterval = this.asinIntervalImpl.bind(this);
public readonly asinhInterval = this.asinhIntervalImpl.bind(this);
public readonly atanInterval = this.atanIntervalImpl.bind(this);
public readonly atan2Interval = this.atan2IntervalImpl.bind(this);
public readonly atanhInterval = this.atanhIntervalImpl.bind(this);
public readonly ceilInterval = this.ceilIntervalImpl.bind(this);
public readonly clampMedianInterval = this.clampMedianIntervalImpl.bind(this);
public readonly clampMinMaxInterval = this.clampMinMaxIntervalImpl.bind(this);
public readonly clampIntervals = [this.clampMedianInterval, this.clampMinMaxInterval];
public readonly cosInterval = this.cosIntervalImpl.bind(this);
public readonly coshInterval = this.coshIntervalImpl.bind(this);
public readonly crossInterval = this.crossIntervalImpl.bind(this);
public readonly degreesInterval = this.degreesIntervalImpl.bind(this);
public readonly determinantInterval = this.determinantIntervalImpl.bind(this);
public readonly distanceInterval = this.distanceIntervalImpl.bind(this);
public readonly divisionInterval = this.divisionIntervalImpl.bind(this);
public readonly dotInterval = this.dotIntervalImpl.bind(this);
public readonly expInterval = this.expIntervalImpl.bind(this);
public readonly exp2Interval = this.exp2IntervalImpl.bind(this);
public readonly faceForwardIntervals = this.faceForwardIntervalsImpl.bind(this);
public readonly floorInterval = this.floorIntervalImpl.bind(this);
public readonly fmaInterval = this.fmaIntervalImpl.bind(this);
public readonly fractInterval = this.fractIntervalImpl.bind(this);
public readonly inverseSqrtInterval = this.inverseSqrtIntervalImpl.bind(this);
public readonly ldexpInterval = this.ldexpIntervalImpl.bind(this);
public readonly lengthInterval = this.lengthIntervalImpl.bind(this);
public readonly logInterval = this.logIntervalImpl.bind(this);
public readonly log2Interval = this.log2IntervalImpl.bind(this);
public readonly maxInterval = this.maxIntervalImpl.bind(this);
public readonly minInterval = this.minIntervalImpl.bind(this);
public readonly mixImpreciseInterval = this.mixImpreciseIntervalImpl.bind(this);
public readonly mixPreciseInterval = this.mixPreciseIntervalImpl.bind(this);
public readonly mixIntervals = [this.mixImpreciseInterval, this.mixPreciseInterval];
public readonly modfInterval = this.modfIntervalImpl.bind(this);
public readonly multiplicationInterval = this.multiplicationIntervalImpl.bind(this);
public readonly multiplicationMatrixMatrixInterval =
this.multiplicationMatrixMatrixIntervalImpl.bind(this);
public readonly multiplicationMatrixScalarInterval =
this.multiplicationMatrixScalarIntervalImpl.bind(this);
public readonly multiplicationScalarMatrixInterval =
this.multiplicationScalarMatrixIntervalImpl.bind(this);
public readonly multiplicationMatrixVectorInterval =
this.multiplicationMatrixVectorIntervalImpl.bind(this);
public readonly multiplicationVectorMatrixInterval =
this.multiplicationVectorMatrixIntervalImpl.bind(this);
public readonly negationInterval = this.negationIntervalImpl.bind(this);
public readonly normalizeInterval = this.normalizeIntervalImpl.bind(this);
public readonly powInterval = this.powIntervalImpl.bind(this);
public readonly radiansInterval = this.radiansIntervalImpl.bind(this);
public readonly reflectInterval = this.reflectIntervalImpl.bind(this);
public readonly refractInterval = this.refractIntervalImpl.bind(this);
public readonly remainderInterval = this.remainderIntervalImpl.bind(this);
public readonly roundInterval = this.roundIntervalImpl.bind(this);
public readonly saturateInterval = this.saturateIntervalImpl.bind(this);
public readonly signInterval = this.signIntervalImpl.bind(this);
public readonly sinInterval = this.sinIntervalImpl.bind(this);
public readonly sinhInterval = this.sinhIntervalImpl.bind(this);
public readonly smoothStepInterval = this.smoothStepIntervalImpl.bind(this);
public readonly sqrtInterval = this.sqrtIntervalImpl.bind(this);
public readonly stepInterval = this.stepIntervalImpl.bind(this);
public readonly subtractionInterval = this.subtractionIntervalImpl.bind(this);
public readonly subtractionMatrixMatrixInterval =
this.subtractionMatrixMatrixIntervalImpl.bind(this);
public readonly tanInterval = this.tanIntervalImpl.bind(this);
public readonly tanhInterval = this.tanhIntervalImpl.bind(this);
public readonly transposeInterval = this.transposeIntervalImpl.bind(this);
public readonly truncInterval = this.truncIntervalImpl.bind(this);
// Framework - Cases
// U32 -> Interval is used for testing f32 specific unpack* functions
/**
* @returns a Case for the param and the interval generator provided.
* The Case will use an interval comparator for matching results.
* @param param the param to pass in
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
private makeU32ToVectorCase(
param: number,
filter: IntervalFilter,
...ops: ScalarToVector[]
): Case | undefined {
param = Math.trunc(param);
const vectors = ops.map(o => o(param));
if (filter === 'finite' && vectors.some(v => !v.every(e => e.isFinite()))) {
return undefined;
}
return {
input: u32(param),
expected: anyOf(...vectors),
};
}
/**
* @returns an array of Cases for operations over a range of inputs
* @param params array of inputs to try
* @param filter what interval filtering to apply
* @param ops callbacks that implement generating an acceptance interval
*/
public generateU32ToIntervalCases(
params: readonly number[],
filter: IntervalFilter,
...ops: ScalarToVector[]
): Case[] {
return params.reduce((cases, e) => {
const c = this.makeU32ToVectorCase(e, filter, ...ops);
if (c !== undefined) {
cases.push(c);
}
return cases;
}, new Array<Case>());
}
// Framework - API
private readonly QuantizeToF16IntervalOp: ScalarToIntervalOp = {
impl: (n: number): FPInterval => {
const rounded = correctlyRoundedF16(n);
const flushed = addFlushedIfNeededF16(rounded);
return this.spanIntervals(...flushed.map(f => this.toInterval(f)));
},
};
protected quantizeToF16IntervalImpl(n: number): FPInterval {
return this.runScalarToIntervalOp(this.toInterval(n), this.QuantizeToF16IntervalOp);
}
/** Calculate an acceptance interval of quantizeToF16(x) */
public readonly quantizeToF16Interval = this.quantizeToF16IntervalImpl.bind(this);
/**
* Once-allocated ArrayBuffer/views to avoid overhead of allocation when
* converting between numeric formats
*
* unpackData* is shared between all the unpack*Interval functions, so to
* avoid re-entrancy problems, they should not call each other or themselves
* directly or indirectly.
*/
private readonly unpackData = new ArrayBuffer(4);
private readonly unpackDataU32 = new Uint32Array(this.unpackData);
private readonly unpackDataU16 = new Uint16Array(this.unpackData);
private readonly unpackDataU8 = new Uint8Array(this.unpackData);
private readonly unpackDataI16 = new Int16Array(this.unpackData);
private readonly unpackDataI8 = new Int8Array(this.unpackData);
private readonly unpackDataF16 = new Float16Array(this.unpackData);
private unpack2x16floatIntervalImpl(n: number): FPVector {
assert(
n >= kValue.u32.min && n <= kValue.u32.max,
'unpack2x16floatInterval only accepts valid u32 values'
);
this.unpackDataU32[0] = n;
if (this.unpackDataF16.some(f => !isFiniteF16(f))) {
return [this.constants().unboundedInterval, this.constants().unboundedInterval];
}
const result: FPVector = [
this.quantizeToF16Interval(this.unpackDataF16[0]),
this.quantizeToF16Interval(this.unpackDataF16[1]),
];
if (result.some(r => !r.isFinite())) {
return [this.constants().unboundedInterval, this.constants().unboundedInterval];
}
return result;
}
/** Calculate an acceptance interval vector for unpack2x16float(x) */
public readonly unpack2x16floatInterval = this.unpack2x16floatIntervalImpl.bind(this);
private unpack2x16snormIntervalImpl(n: number): FPVector {
assert(
n >= kValue.u32.min && n <= kValue.u32.max,
'unpack2x16snormInterval only accepts valid u32 values'
);
const op = (n: number): FPInterval => {
return this.ulpInterval(Math.max(n / 32767, -1), 3);
};
this.unpackDataU32[0] = n;
return [op(this.unpackDataI16[0]), op(this.unpackDataI16[1])];
}
/** Calculate an acceptance interval vector for unpack2x16snorm(x) */
public readonly unpack2x16snormInterval = this.unpack2x16snormIntervalImpl.bind(this);
private unpack2x16unormIntervalImpl(n: number): FPVector {
assert(
n >= kValue.u32.min && n <= kValue.u32.max,
'unpack2x16unormInterval only accepts valid u32 values'
);
const op = (n: number): FPInterval => {
return this.ulpInterval(n / 65535, 3);
};
this.unpackDataU32[0] = n;
return [op(this.unpackDataU16[0]), op(this.unpackDataU16[1])];
}
/** Calculate an acceptance interval vector for unpack2x16unorm(x) */
public readonly unpack2x16unormInterval = this.unpack2x16unormIntervalImpl.bind(this);
private unpack4x8snormIntervalImpl(n: number): FPVector {
assert(
n >= kValue.u32.min && n <= kValue.u32.max,
'unpack4x8snormInterval only accepts valid u32 values'
);
const op = (n: number): FPInterval => {
return this.ulpInterval(Math.max(n / 127, -1), 3);
};
this.unpackDataU32[0] = n;
return [
op(this.unpackDataI8[0]),
op(this.unpackDataI8[1]),
op(this.unpackDataI8[2]),
op(this.unpackDataI8[3]),
];
}
/** Calculate an acceptance interval vector for unpack4x8snorm(x) */
public readonly unpack4x8snormInterval = this.unpack4x8snormIntervalImpl.bind(this);
private unpack4x8unormIntervalImpl(n: number): FPVector {
assert(
n >= kValue.u32.min && n <= kValue.u32.max,
'unpack4x8unormInterval only accepts valid u32 values'
);
const op = (n: number): FPInterval => {
return this.ulpInterval(n / 255, 3);
};
this.unpackDataU32[0] = n;
return [
op(this.unpackDataU8[0]),
op(this.unpackDataU8[1]),
op(this.unpackDataU8[2]),
op(this.unpackDataU8[3]),
];
}
/** Calculate an acceptance interval vector for unpack4x8unorm(x) */
public readonly unpack4x8unormInterval = this.unpack4x8unormIntervalImpl.bind(this);
}
// Need to separately allocate f32 traits, so they can be referenced by
// FPAbstractTraits for forwarding.
const kF32Traits = new F32Traits();
// Pre-defined values that get used multiple times in _constants' initializers. Cannot use FPTraits members, since this
// executes before they are defined.
const kAbstractUnboundedInterval = new FPInterval(
'abstract',
Number.NEGATIVE_INFINITY,
Number.POSITIVE_INFINITY
);
const kAbstractZeroInterval = new FPInterval('abstract', 0);
// This is implementation is incomplete
class FPAbstractTraits extends FPTraits {
private static _constants: FPConstants = {
positive: {
min: kValue.f64.positive.min,
max: kValue.f64.positive.max,
infinity: kValue.f64.positive.infinity,
nearest_max: kValue.f64.positive.nearest_max,
less_than_one: kValue.f64.positive.less_than_one,
subnormal: {
min: kValue.f64.positive.subnormal.min,
max: kValue.f64.positive.subnormal.max,
},
pi: {
whole: kValue.f64.positive.pi.whole,
three_quarters: kValue.f64.positive.pi.three_quarters,
half: kValue.f64.positive.pi.half,
third: kValue.f64.positive.pi.third,
quarter: kValue.f64.positive.pi.quarter,
sixth: kValue.f64.positive.pi.sixth,
},
e: kValue.f64.positive.e,
},
negative: {
min: kValue.f64.negative.min,
max: kValue.f64.negative.max,
infinity: kValue.f64.negative.infinity,
nearest_min: kValue.f64.negative.nearest_min,
less_than_one: kValue.f64.negative.less_than_one,
subnormal: {
min: kValue.f64.negative.subnormal.min,
max: kValue.f64.negative.subnormal.max,
},
pi: {
whole: kValue.f64.negative.pi.whole,
three_quarters: kValue.f64.negative.pi.three_quarters,
half: kValue.f64.negative.pi.half,
third: kValue.f64.negative.pi.third,
quarter: kValue.f64.negative.pi.quarter,
sixth: kValue.f64.negative.pi.sixth,
},
},
bias: 1023,
unboundedInterval: kAbstractUnboundedInterval,
zeroInterval: kAbstractZeroInterval,
// Have to use the constants.ts values here, because values defined in the
// initializer cannot be referenced in the initializer
negPiToPiInterval: new FPInterval(
'abstract',
kValue.f64.negative.pi.whole,
kValue.f64.positive.pi.whole
),
greaterThanZeroInterval: new FPInterval(
'abstract',
kValue.f64.positive.subnormal.min,
kValue.f64.positive.max
),
negOneToOneInterval: new FPInterval('abstract', -1, 1),
zeroVector: {
2: [kAbstractZeroInterval, kAbstractZeroInterval],
3: [kAbstractZeroInterval, kAbstractZeroInterval, kAbstractZeroInterval],
4: [
kAbstractZeroInterval,
kAbstractZeroInterval,
kAbstractZeroInterval,
kAbstractZeroInterval,
],
},
unboundedVector: {
2: [kAbstractUnboundedInterval, kAbstractUnboundedInterval],
3: [kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
4: [
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
},
unboundedMatrix: {
2: {
2: [
[kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval],
],
3: [
[kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
],
4: [
[
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
[
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
],
},
3: {
2: [
[kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval],
],
3: [
[kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
],
4: [
[
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
[
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
[
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
],
},
4: {
2: [
[kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval],
],
3: [
[kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
[kAbstractUnboundedInterval, kAbstractUnboundedInterval, kAbstractUnboundedInterval],
],
4: [
[
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
[
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
[
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
[
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
kAbstractUnboundedInterval,
],
],
},
},
};
public constructor() {
super('abstract');
}
public constants(): FPConstants {
return FPAbstractTraits._constants;
}
// Utilities - Overrides
// number is represented as a f64 internally, so all number values are already
// quantized to f64
public readonly quantize = (n: number) => {
return n;
};
public readonly correctlyRounded = correctlyRoundedF64;
public readonly isFinite = Number.isFinite;
public readonly isSubnormal = isSubnormalNumberF64;
public readonly flushSubnormal = flushSubnormalNumberF64;
public readonly oneULP = (_target: number, _mode: FlushMode = 'flush'): number => {
unreachable(`'FPAbstractTraits.oneULP should never be called`);
};
public readonly scalarBuilder = abstractFloat;
public readonly scalarRange = scalarF64Range;
public readonly sparseScalarRange = sparseScalarF64Range;
public readonly vectorRange = vectorF64Range;
public readonly sparseVectorRange = sparseVectorF64Range;
public readonly sparseMatrixRange = sparseMatrixF64Range;
// Framework - Fundamental Error Intervals - Overrides
public readonly absoluteErrorInterval = this.unimplementedAbsoluteErrorInterval.bind(this); // Should use FP.f32 instead
public readonly correctlyRoundedInterval = this.correctlyRoundedIntervalImpl.bind(this);
public readonly correctlyRoundedMatrix = this.correctlyRoundedMatrixImpl.bind(this);
public readonly ulpInterval = this.unimplementedUlpInterval.bind(this); // Should use FP.f32 instead
// Framework - API - Overrides
public readonly absInterval = this.absIntervalImpl.bind(this);
public readonly acosInterval = this.unimplementedScalarToInterval.bind(this, 'acosInterval');
public readonly acoshAlternativeInterval = this.unimplementedScalarToInterval.bind(
this,
'acoshAlternativeInterval'
);
public readonly acoshPrimaryInterval = this.unimplementedScalarToInterval.bind(
this,
'acoshPrimaryInterval'
);
public readonly acoshIntervals = [this.acoshAlternativeInterval, this.acoshPrimaryInterval];
public readonly additionInterval = this.unimplementedScalarPairToInterval.bind(
this,
'additionInterval'
);
public readonly additionMatrixMatrixInterval = this.unimplementedMatrixPairToMatrix.bind(
this,
'additionMatrixMatrixInterval'
);
public readonly asinInterval = this.unimplementedScalarToInterval.bind(this, 'asinInterval');
public readonly asinhInterval = this.unimplementedScalarToInterval.bind(this, 'asinhInterval');
public readonly atanInterval = this.unimplementedScalarToInterval.bind(this, 'atanInterval');
public readonly atan2Interval = this.unimplementedScalarPairToInterval.bind(
this,
'atan2Interval'
);
public readonly atanhInterval = this.unimplementedScalarToInterval.bind(this, 'atanhInterval');
public readonly ceilInterval = this.ceilIntervalImpl.bind(this);
public readonly clampMedianInterval = this.clampMedianIntervalImpl.bind(this);
public readonly clampMinMaxInterval = this.clampMinMaxIntervalImpl.bind(this);
public readonly clampIntervals = [this.clampMedianInterval, this.clampMinMaxInterval];
public readonly cosInterval = this.unimplementedScalarToInterval.bind(this, 'cosInterval');
public readonly coshInterval = this.unimplementedScalarToInterval.bind(this, 'coshInterval');
public readonly crossInterval = this.unimplementedVectorPairToVector.bind(this, 'crossInterval');
public readonly degreesInterval = this.unimplementedScalarToInterval.bind(
this,
'degreesInterval'
);
public readonly determinantInterval = this.unimplementedMatrixToInterval.bind(
this,
'determinant'
);
public readonly distanceInterval = this.unimplementedDistance.bind(this);
public readonly divisionInterval = this.unimplementedScalarPairToInterval.bind(
this,
'divisionInterval'
);
public readonly dotInterval = this.unimplementedVectorPairToInterval.bind(this, 'dotInterval');
public readonly expInterval = this.unimplementedScalarToInterval.bind(this, 'expInterval');
public readonly exp2Interval = this.unimplementedScalarToInterval.bind(this, 'exp2Interval');
public readonly faceForwardIntervals = this.unimplementedFaceForward.bind(this);
public readonly floorInterval = this.floorIntervalImpl.bind(this);
public readonly fmaInterval = this.unimplementedScalarTripleToInterval.bind(this, 'fmaInterval');
public readonly fractInterval = this.unimplementedScalarToInterval.bind(this, 'fractInterval');
public readonly inverseSqrtInterval = this.unimplementedScalarToInterval.bind(
this,
'inverseSqrtInterval'
);
public readonly ldexpInterval = this.ldexpIntervalImpl.bind(this);
public readonly lengthInterval = this.unimplementedLength.bind(this);
public readonly logInterval = this.unimplementedScalarToInterval.bind(this, 'logInterval');
public readonly log2Interval = this.unimplementedScalarToInterval.bind(this, 'log2Interval');
public readonly maxInterval = this.maxIntervalImpl.bind(this);
public readonly minInterval = this.minIntervalImpl.bind(this);
public readonly mixImpreciseInterval = this.unimplementedScalarTripleToInterval.bind(
this,
'mixImpreciseInterval'
);
public readonly mixPreciseInterval = this.unimplementedScalarTripleToInterval.bind(
this,
'mixPreciseInterval'
);
public readonly mixIntervals = [this.mixImpreciseInterval, this.mixPreciseInterval];
public readonly modfInterval = this.modfIntervalImpl.bind(this);
public readonly multiplicationInterval = this.unimplementedScalarPairToInterval.bind(
this,
'multiplicationInterval'
);
public readonly multiplicationMatrixMatrixInterval = this.unimplementedMatrixPairToMatrix.bind(
this,
'multiplicationMatrixMatrixInterval'
);
public readonly multiplicationMatrixScalarInterval = this.unimplementedMatrixScalarToMatrix.bind(
this,
'multiplicationMatrixScalarInterval'
);
public readonly multiplicationScalarMatrixInterval = this.unimplementedScalarMatrixToMatrix.bind(
this,
'multiplicationScalarMatrixInterval'
);
public readonly multiplicationMatrixVectorInterval = this.unimplementedMatrixVectorToVector.bind(
this,
'multiplicationMatrixVectorInterval'
);
public readonly multiplicationVectorMatrixInterval = this.unimplementedVectorMatrixToVector.bind(
this,
'multiplicationVectorMatrixInterval'
);
public readonly negationInterval = this.negationIntervalImpl.bind(this);
public readonly normalizeInterval = this.unimplementedVectorToVector.bind(
this,
'normalizeInterval'
);
public readonly powInterval = this.unimplementedScalarPairToInterval.bind(this, 'powInterval');
public readonly radiansInterval = this.unimplementedScalarToInterval.bind(this, 'radiansImpl');
public readonly reflectInterval = this.unimplementedVectorPairToVector.bind(
this,
'reflectInterval'
);
public readonly refractInterval = this.unimplementedRefract.bind(this);
public readonly remainderInterval = this.unimplementedScalarPairToInterval.bind(
this,
'remainderInterval'
);
public readonly roundInterval = this.roundIntervalImpl.bind(this);
public readonly saturateInterval = this.saturateIntervalImpl.bind(this);
public readonly signInterval = this.signIntervalImpl.bind(this);
public readonly sinInterval = this.unimplementedScalarToInterval.bind(this, 'sinInterval');
public readonly sinhInterval = this.unimplementedScalarToInterval.bind(this, 'sinhInterval');
public readonly smoothStepInterval = this.unimplementedScalarTripleToInterval.bind(
this,
'smoothStepInterval'
);
public readonly sqrtInterval = this.unimplementedScalarToInterval.bind(this, 'sqrtInterval');
public readonly stepInterval = this.stepIntervalImpl.bind(this);
public readonly subtractionInterval = this.unimplementedScalarPairToInterval.bind(
this,
'subtractionInterval'
);
public readonly subtractionMatrixMatrixInterval = this.unimplementedMatrixPairToMatrix.bind(
this,
'subtractionMatrixMatrixInterval'
);
public readonly tanInterval = this.unimplementedScalarToInterval.bind(this, 'tanInterval');
public readonly tanhInterval = this.unimplementedScalarToInterval.bind(this, 'tanhInterval');
public readonly transposeInterval = this.transposeIntervalImpl.bind(this);
public readonly truncInterval = this.truncIntervalImpl.bind(this);
}
// Pre-defined values that get used multiple times in _constants' initializers. Cannot use FPTraits members, since this
// executes before they are defined.
const kF16UnboundedInterval = new FPInterval(
'f16',
Number.NEGATIVE_INFINITY,
Number.POSITIVE_INFINITY
);
const kF16ZeroInterval = new FPInterval('f16', 0);
// This is implementation is incomplete
class F16Traits extends FPTraits {
private static _constants: FPConstants = {
positive: {
min: kValue.f16.positive.min,
max: kValue.f16.positive.max,
infinity: kValue.f16.positive.infinity,
nearest_max: kValue.f16.positive.nearest_max,
less_than_one: kValue.f16.positive.less_than_one,
subnormal: {
min: kValue.f16.positive.subnormal.min,
max: kValue.f16.positive.subnormal.max,
},
pi: {
whole: kValue.f16.positive.pi.whole,
three_quarters: kValue.f16.positive.pi.three_quarters,
half: kValue.f16.positive.pi.half,
third: kValue.f16.positive.pi.third,
quarter: kValue.f16.positive.pi.quarter,
sixth: kValue.f16.positive.pi.sixth,
},
e: kValue.f16.positive.e,
},
negative: {
min: kValue.f16.negative.min,
max: kValue.f16.negative.max,
infinity: kValue.f16.negative.infinity,
nearest_min: kValue.f16.negative.nearest_min,
less_than_one: kValue.f16.negative.less_than_one,
subnormal: {
min: kValue.f16.negative.subnormal.min,
max: kValue.f16.negative.subnormal.max,
},
pi: {
whole: kValue.f16.negative.pi.whole,
three_quarters: kValue.f16.negative.pi.three_quarters,
half: kValue.f16.negative.pi.half,
third: kValue.f16.negative.pi.third,
quarter: kValue.f16.negative.pi.quarter,
sixth: kValue.f16.negative.pi.sixth,
},
},
bias: 15,
unboundedInterval: kF16UnboundedInterval,
zeroInterval: kF16ZeroInterval,
// Have to use the constants.ts values here, because values defined in the
// initializer cannot be referenced in the initializer
negPiToPiInterval: new FPInterval(
'f16',
kValue.f16.negative.pi.whole,
kValue.f16.positive.pi.whole
),
greaterThanZeroInterval: new FPInterval(
'f16',
kValue.f16.positive.subnormal.min,
kValue.f16.positive.max
),
negOneToOneInterval: new FPInterval('f16', -1, 1),
zeroVector: {
2: [kF16ZeroInterval, kF16ZeroInterval],
3: [kF16ZeroInterval, kF16ZeroInterval, kF16ZeroInterval],
4: [kF16ZeroInterval, kF16ZeroInterval, kF16ZeroInterval, kF16ZeroInterval],
},
unboundedVector: {
2: [kF16UnboundedInterval, kF16UnboundedInterval],
3: [kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
4: [
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
},
unboundedMatrix: {
2: {
2: [
[kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval],
],
3: [
[kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
],
4: [
[
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
[
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
],
},
3: {
2: [
[kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval],
],
3: [
[kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
],
4: [
[
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
[
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
[
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
],
},
4: {
2: [
[kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval],
],
3: [
[kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
[kF16UnboundedInterval, kF16UnboundedInterval, kF16UnboundedInterval],
],
4: [
[
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
[
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
[
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
[
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
kF16UnboundedInterval,
],
],
},
},
};
public constructor() {
super('f16');
}
public constants(): FPConstants {
return F16Traits._constants;
}
// Utilities - Overrides
public readonly quantize = quantizeToF16;
public readonly correctlyRounded = correctlyRoundedF16;
public readonly isFinite = isFiniteF16;
public readonly isSubnormal = isSubnormalNumberF16;
public readonly flushSubnormal = flushSubnormalNumberF16;
public readonly oneULP = oneULPF16;
public readonly scalarBuilder = f16;
public readonly scalarRange = scalarF16Range;
public readonly sparseScalarRange = sparseScalarF16Range;
public readonly vectorRange = vectorF16Range;
public readonly sparseVectorRange = sparseVectorF16Range;
public readonly sparseMatrixRange = sparseMatrixF16Range;
// Framework - Fundamental Error Intervals - Overrides
public readonly absoluteErrorInterval = this.absoluteErrorIntervalImpl.bind(this);
public readonly correctlyRoundedInterval = this.correctlyRoundedIntervalImpl.bind(this);
public readonly correctlyRoundedMatrix = this.correctlyRoundedMatrixImpl.bind(this);
public readonly ulpInterval = this.ulpIntervalImpl.bind(this);
// Framework - API - Overrides
public readonly absInterval = this.absIntervalImpl.bind(this);
public readonly acosInterval = this.acosIntervalImpl.bind(this);
public readonly acoshAlternativeInterval = this.acoshAlternativeIntervalImpl.bind(this);
public readonly acoshPrimaryInterval = this.acoshPrimaryIntervalImpl.bind(this);
public readonly acoshIntervals = [this.acoshAlternativeInterval, this.acoshPrimaryInterval];
public readonly additionInterval = this.additionIntervalImpl.bind(this);
public readonly additionMatrixMatrixInterval = this.additionMatrixMatrixIntervalImpl.bind(this);
public readonly asinInterval = this.asinIntervalImpl.bind(this);
public readonly asinhInterval = this.asinhIntervalImpl.bind(this);
public readonly atanInterval = this.atanIntervalImpl.bind(this);
public readonly atan2Interval = this.atan2IntervalImpl.bind(this);
public readonly atanhInterval = this.atanhIntervalImpl.bind(this);
public readonly ceilInterval = this.ceilIntervalImpl.bind(this);
public readonly clampMedianInterval = this.clampMedianIntervalImpl.bind(this);
public readonly clampMinMaxInterval = this.clampMinMaxIntervalImpl.bind(this);
public readonly clampIntervals = [this.clampMedianInterval, this.clampMinMaxInterval];
public readonly cosInterval = this.cosIntervalImpl.bind(this);
public readonly coshInterval = this.coshIntervalImpl.bind(this);
public readonly crossInterval = this.crossIntervalImpl.bind(this);
public readonly degreesInterval = this.degreesIntervalImpl.bind(this);
public readonly determinantInterval = this.determinantIntervalImpl.bind(this);
public readonly distanceInterval = this.distanceIntervalImpl.bind(this);
public readonly divisionInterval = this.divisionIntervalImpl.bind(this);
public readonly dotInterval = this.dotIntervalImpl.bind(this);
public readonly expInterval = this.expIntervalImpl.bind(this);
public readonly exp2Interval = this.exp2IntervalImpl.bind(this);
public readonly faceForwardIntervals = this.faceForwardIntervalsImpl.bind(this);
public readonly floorInterval = this.floorIntervalImpl.bind(this);
public readonly fmaInterval = this.fmaIntervalImpl.bind(this);
public readonly fractInterval = this.fractIntervalImpl.bind(this);
public readonly inverseSqrtInterval = this.inverseSqrtIntervalImpl.bind(this);
public readonly ldexpInterval = this.ldexpIntervalImpl.bind(this);
public readonly lengthInterval = this.lengthIntervalImpl.bind(this);
public readonly logInterval = this.logIntervalImpl.bind(this);
public readonly log2Interval = this.log2IntervalImpl.bind(this);
public readonly maxInterval = this.maxIntervalImpl.bind(this);
public readonly minInterval = this.minIntervalImpl.bind(this);
public readonly mixImpreciseInterval = this.mixImpreciseIntervalImpl.bind(this);
public readonly mixPreciseInterval = this.mixPreciseIntervalImpl.bind(this);
public readonly mixIntervals = [this.mixImpreciseInterval, this.mixPreciseInterval];
public readonly modfInterval = this.modfIntervalImpl.bind(this);
public readonly multiplicationInterval = this.multiplicationIntervalImpl.bind(this);
public readonly multiplicationMatrixMatrixInterval =
this.multiplicationMatrixMatrixIntervalImpl.bind(this);
public readonly multiplicationMatrixScalarInterval =
this.multiplicationMatrixScalarIntervalImpl.bind(this);
public readonly multiplicationScalarMatrixInterval =
this.multiplicationScalarMatrixIntervalImpl.bind(this);
public readonly multiplicationMatrixVectorInterval =
this.multiplicationMatrixVectorIntervalImpl.bind(this);
public readonly multiplicationVectorMatrixInterval =
this.multiplicationVectorMatrixIntervalImpl.bind(this);
public readonly negationInterval = this.negationIntervalImpl.bind(this);
public readonly normalizeInterval = this.normalizeIntervalImpl.bind(this);
public readonly powInterval = this.powIntervalImpl.bind(this);
public readonly radiansInterval = this.radiansIntervalImpl.bind(this);
public readonly reflectInterval = this.reflectIntervalImpl.bind(this);
public readonly refractInterval = this.refractIntervalImpl.bind(this);
public readonly remainderInterval = this.remainderIntervalImpl.bind(this);
public readonly roundInterval = this.roundIntervalImpl.bind(this);
public readonly saturateInterval = this.saturateIntervalImpl.bind(this);
public readonly signInterval = this.signIntervalImpl.bind(this);
public readonly sinInterval = this.sinIntervalImpl.bind(this);
public readonly sinhInterval = this.sinhIntervalImpl.bind(this);
public readonly smoothStepInterval = this.smoothStepIntervalImpl.bind(this);
public readonly sqrtInterval = this.sqrtIntervalImpl.bind(this);
public readonly stepInterval = this.stepIntervalImpl.bind(this);
public readonly subtractionInterval = this.subtractionIntervalImpl.bind(this);
public readonly subtractionMatrixMatrixInterval =
this.subtractionMatrixMatrixIntervalImpl.bind(this);
public readonly tanInterval = this.tanIntervalImpl.bind(this);
public readonly tanhInterval = this.tanhIntervalImpl.bind(this);
public readonly transposeInterval = this.transposeIntervalImpl.bind(this);
public readonly truncInterval = this.truncIntervalImpl.bind(this);
}
export const FP = {
f32: kF32Traits,
f16: new F16Traits(),
abstract: new FPAbstractTraits(),
};
/** @returns the floating-point traits for `type` */
export function fpTraitsFor(type: ScalarType): FPTraits {
switch (type.kind) {
case 'abstract-float':
return FP.abstract;
case 'f32':
return FP.f32;
case 'f16':
return FP.f16;
default:
unreachable(`unsupported type: ${type}`);
}
}
/** @returns true if the value `value` is representable with `type` */
export function isRepresentable(value: number, type: ScalarType) {
if (!Number.isFinite(value)) {
return false;
}
if (isFloatType(type)) {
const constants = fpTraitsFor(type).constants();
return value >= constants.negative.min && value <= constants.positive.max;
}
assert(false, `isRepresentable() is not yet implemented for type ${type}`);
}