Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nearly equal with relative and absolute tolerance #3152

Merged
merged 17 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/core/configuration.md
Expand Up @@ -8,7 +8,8 @@ import { create, all } from 'mathjs'

// create a mathjs instance with configuration
const config = {
epsilon: 1e-12,
relTol: 1e-12,
absTol: 1e-15,
matrix: 'Matrix',
number: 'number',
precision: 64,
Expand All @@ -28,10 +29,14 @@ math.config({

The following configuration options are available:

- `epsilon`. The minimum relative difference used to test equality between two
- `relTol`. The minimum relative difference used to test equality between two
compared values. This value is used by all relational functions.
Default value is `1e-12`.

- `absTol`. The minimum absolute difference used to test equality between two
compared values. This value is used by all relational functions.
Default value is `1e-15`.

- `matrix`. The default type of matrix output for functions.
Available values are: `'Matrix'` (default) or `'Array'`.
Where possible, the type of matrix output from functions is determined from
Expand Down
11 changes: 6 additions & 5 deletions docs/datatypes/bignumbers.md
Expand Up @@ -24,7 +24,8 @@ math.config({
number: 'BigNumber', // Default type of number:
// 'number' (default), 'BigNumber', or 'Fraction'
precision: 64, // Number of significant digits for BigNumbers
epsilon: 1e-60
relTol: 1e-60,
absTol: 1e-63
})

// use math
Expand All @@ -34,12 +35,12 @@ math.evaluate('0.1 + 0.2') // BigNumber, 0.3
The default precision for BigNumber is 64 digits, and can be configured with
the option `precision`.

Note that we also change the configuration of `epsilon`
to be close to the precision limit of our BigNumbers. `epsilon` is used for
Note that we also change the configuration of `relTol` and `absTol`
to be close to the precision limit of our BigNumbers. `relTol` and `absTol` are used for
example in relational and rounding functions (`equal`, `larger`, `smaller`,
`round`, `floor`, etc) to determine when a value is nearly equal,
see [Equality](numbers.md#equality). If we would leave `epsilon` unchanged,
having the default value of `1e-12`, we could get inaccurate and misleading
see [Equality](numbers.md#equality). If we would leave `relTol` and `absTol` unchanged,
having the default value of `1e-12` and `1e-15` respectively, we could get inaccurate and misleading
results since we're now working with a higher precision.


Expand Down
10 changes: 4 additions & 6 deletions docs/datatypes/numbers.md
Expand Up @@ -73,16 +73,14 @@ false, as the addition `0.1 + 0.2` introduces a round-off error and does not
return exactly `0.3`.

To solve this problem, the relational functions of math.js check whether the
relative difference between the compared values is smaller than the configured
option `epsilon`. In pseudo code (without exceptions for 0, Infinity and NaN):
relative and absolute differences between the compared values is smaller than the configured
option `relTol` and `absTol`. In pseudo code (without exceptions for 0, Infinity and NaN):

diff = abs(x - y)
nearlyEqual = (diff <= max(abs(x), abs(y)) * EPSILON) OR (diff < DBL_EPSILON)
abs(a-b) <= max(relTol * max(abs(a), abs(b)), absTol)

where:

- `EPSILON` is the relative difference between x and y. Epsilon is configurable
and is `1e-12` by default. See [Configuration](../core/configuration.md).
- `relTol` is the relative tolerance between x and y and `absTol` the absolute tolerance. Relative tolerance and absolute tolerance are configurable and are `1e-12` and `1e-15` respectively by default. See [Configuration](../core/configuration.md).
- `DBL_EPSILON` is the minimum positive floating point number such that
`1.0 + DBL_EPSILON !== 1.0`. This is a constant with a value of approximately
`2.2204460492503130808472633361816e-16`.
Expand Down
6 changes: 5 additions & 1 deletion src/core/config.js
@@ -1,7 +1,11 @@
export const DEFAULT_CONFIG = {
// minimum relative difference between two compared values,
// used by all comparison functions
epsilon: 1e-12,
relTol: 1e-12,

// minimum absolute difference between two compared values,
// used by all comparison functions
absTol: 1e-15,

// type of default matrix output. Choose 'matrix' (default) or 'array'
matrix: 'Matrix',
Expand Down
5 changes: 4 additions & 1 deletion src/core/create.js
Expand Up @@ -63,9 +63,12 @@ import { DEFAULT_CONFIG } from './config.js'
* The object can contain nested objects,
* all nested objects will be flattened.
* @param {Object} [config] Available options:
* {number} epsilon
* {number} relTol
* Minimum relative difference between two
* compared values, used by all comparison functions.
* {number} absTol
* Minimum absolute difference between two
* compared values, used by all comparison functions.
* {string} matrix
* A string 'Matrix' (default) or 'Array'.
* {string} number
Expand Down
24 changes: 18 additions & 6 deletions src/core/function/config.js
@@ -1,4 +1,4 @@
import { clone, mapObject, deepExtend } from '../../utils/object.js'
import { clone, deepExtend } from '../../utils/object.js'
import { DEFAULT_CONFIG } from '../config.js'

export const MATRIX_OPTIONS = ['Matrix', 'Array'] // valid values for option matrix
Expand Down Expand Up @@ -29,9 +29,12 @@ export function configFactory (config, emit) {
* math.evaluate('0.4') // outputs Fraction 2/5
*
* @param {Object} [options] Available options:
* {number} epsilon
* {number} relTol
* Minimum relative difference between two
* compared values, used by all comparison functions.
* {number} absTol
* Minimum absolute difference between two
* compared values, used by all comparison functions.
* {string} matrix
* A string 'Matrix' (default) or 'Array'.
* {string} number
Expand All @@ -49,7 +52,16 @@ export function configFactory (config, emit) {
*/
function _config (options) {
if (options) {
const prev = mapObject(config, clone)
if (options.epsilon !== undefined) {
// this if is only for backwards compatibility, it can be removed in the future.
console.warn('Warning: The configuration option "epsilon" is deprecated. Use "relTol" and "absTol" instead.')
const optionsFix = clone(options)
optionsFix.relTol = options.epsilon
optionsFix.absTol = options.epsilon * 1e-3
delete optionsFix.epsilon
return _config(optionsFix)
}
const prev = clone(config)

// validate some of the options
validateOption(options, 'matrix', MATRIX_OPTIONS)
Expand All @@ -58,16 +70,16 @@ export function configFactory (config, emit) {
// merge options
deepExtend(config, options)

const curr = mapObject(config, clone)
const curr = clone(config)

const changes = mapObject(options, clone)
const changes = clone(options)

// emit 'config' event
emit('config', curr, prev, changes)

return curr
} else {
return mapObject(config, clone)
return clone(config)
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/function/arithmetic/ceil.js
Expand Up @@ -14,15 +14,15 @@ export const createCeilNumber = /* #__PURE__ */ factory(
name, ['typed', 'config', 'round'], ({ typed, config, round }) => {
return typed(name, {
number: function (x) {
if (nearlyEqual(x, round(x), config.epsilon)) {
if (nearlyEqual(x, round(x), config.relTol, config.absTol)) {
return round(x)
} else {
return Math.ceil(x)
}
},

'number, number': function (x, n) {
if (nearlyEqual(x, round(x, n), config.epsilon)) {
if (nearlyEqual(x, round(x, n), config.relTol, config.absTol)) {
return round(x, n)
} else {
let [number, exponent] = `${x}e`.split('e')
Expand Down Expand Up @@ -95,15 +95,15 @@ export const createCeil = /* #__PURE__ */ factory(name, dependencies, ({ typed,
},

BigNumber: function (x) {
if (bigNearlyEqual(x, round(x), config.epsilon)) {
if (bigNearlyEqual(x, round(x), config.relTol, config.absTol)) {
return round(x)
} else {
return x.ceil()
}
},

'BigNumber, BigNumber': function (x, n) {
if (bigNearlyEqual(x, round(x, n), config.epsilon)) {
if (bigNearlyEqual(x, round(x, n), config.relTol, config.absTol)) {
return round(x, n)
} else {
return x.toDecimalPlaces(n.toNumber(), Decimal.ROUND_CEIL)
Expand Down
8 changes: 4 additions & 4 deletions src/function/arithmetic/floor.js
Expand Up @@ -14,15 +14,15 @@ export const createFloorNumber = /* #__PURE__ */ factory(
name, ['typed', 'config', 'round'], ({ typed, config, round }) => {
return typed(name, {
number: function (x) {
if (nearlyEqual(x, round(x), config.epsilon)) {
if (nearlyEqual(x, round(x), config.relTol, config.absTol)) {
return round(x)
} else {
return Math.floor(x)
}
},

'number, number': function (x, n) {
if (nearlyEqual(x, round(x, n), config.epsilon)) {
if (nearlyEqual(x, round(x, n), config.relTol, config.absTol)) {
return round(x, n)
} else {
let [number, exponent] = `${x}e`.split('e')
Expand Down Expand Up @@ -98,15 +98,15 @@ export const createFloor = /* #__PURE__ */ factory(name, dependencies, ({ typed,
},

BigNumber: function (x) {
if (bigNearlyEqual(x, round(x), config.epsilon)) {
if (bigNearlyEqual(x, round(x), config.relTol, config.absTol)) {
return round(x)
} else {
return x.floor()
}
},

'BigNumber, BigNumber': function (x, n) {
if (bigNearlyEqual(x, round(x, n), config.epsilon)) {
if (bigNearlyEqual(x, round(x, n), config.relTol, config.absTol)) {
return round(x, n)
} else {
return x.toDecimalPlaces(n.toNumber(), Decimal.ROUND_FLOOR)
Expand Down
24 changes: 12 additions & 12 deletions src/function/arithmetic/round.js
Expand Up @@ -75,19 +75,19 @@ export const createRound = /* #__PURE__ */ factory(name, dependencies, ({ typed,
*/
return typed(name, {
number: function (x) {
// Handle round off errors by first rounding to epsilon precision
const xEpsilon = roundNumber(x, toExponent(config.epsilon))
const xSelected = nearlyEqual(x, xEpsilon, config.epsilon) ? xEpsilon : x
// Handle round off errors by first rounding to relTol precision
const xEpsilon = roundNumber(x, toExponent(config.relTol))
const xSelected = nearlyEqual(x, xEpsilon, config.relTol, config.absTol) ? xEpsilon : x
return roundNumber(xSelected)
},

'number, number': function (x, n) {
// Same as number: unless user specifies more decimals than epsilon
const epsilonExponent = toExponent(config.epsilon)
// Same as number: unless user specifies more decimals than relTol
const epsilonExponent = toExponent(config.relTol)
if (n >= epsilonExponent) { return roundNumber(x, n) }

const xEpsilon = roundNumber(x, epsilonExponent)
const xSelected = nearlyEqual(x, xEpsilon, config.epsilon) ? xEpsilon : x
const xSelected = nearlyEqual(x, xEpsilon, config.relTol, config.absTol) ? xEpsilon : x
return roundNumber(xSelected, n)
},

Expand Down Expand Up @@ -115,21 +115,21 @@ export const createRound = /* #__PURE__ */ factory(name, dependencies, ({ typed,
},

BigNumber: function (x) {
// Handle round off errors by first rounding to epsilon precision
const xEpsilon = new BigNumber(x).toDecimalPlaces(toExponent(config.epsilon))
const xSelected = bigNearlyEqual(x, xEpsilon, config.epsilon) ? xEpsilon : x
// Handle round off errors by first rounding to relTol precision
const xEpsilon = new BigNumber(x).toDecimalPlaces(toExponent(config.relTol))
const xSelected = bigNearlyEqual(x, xEpsilon, config.relTol, config.absTol) ? xEpsilon : x
return xSelected.toDecimalPlaces(0)
},

'BigNumber, BigNumber': function (x, n) {
if (!n.isInteger()) { throw new TypeError(NO_INT) }

// Same as BigNumber: unless user specifies more decimals than epsilon
const epsilonExponent = toExponent(config.epsilon)
// Same as BigNumber: unless user specifies more decimals than relTol
const epsilonExponent = toExponent(config.relTol)
if (n >= epsilonExponent) { return x.toDecimalPlaces(n.toNumber()) }

const xEpsilon = x.toDecimalPlaces(epsilonExponent)
const xSelected = bigNearlyEqual(x, xEpsilon, config.epsilon) ? xEpsilon : x
const xSelected = bigNearlyEqual(x, xEpsilon, config.relTol, config.absTol) ? xEpsilon : x
return xSelected.toDecimalPlaces(n.toNumber())
},

Expand Down
2 changes: 1 addition & 1 deletion src/function/geometry/intersect.js
Expand Up @@ -119,7 +119,7 @@ export const createIntersect = /* #__PURE__ */ factory(name, dependencies, ({ ty
const d2 = subtract(o2, p2b)
const det = subtract(multiplyScalar(d1[0], d2[1]), multiplyScalar(d2[0], d1[1]))
if (isZero(det)) return null
if (smaller(abs(det), config.epsilon)) {
if (smaller(abs(det), config.relTol)) {
return null
}
const d20o11 = multiplyScalar(d2[0], o1[1])
Expand Down
4 changes: 2 additions & 2 deletions src/function/matrix/eigs.js
Expand Up @@ -68,7 +68,7 @@ export const createEigs = /* #__PURE__ */ factory(name, dependencies, ({ config,
*
* @param {Array | Matrix} x Matrix to be diagonalized
*
* @param {number | BigNumber | OptsObject} [opts] Object with keys `precision`, defaulting to config.epsilon, and `eigenvectors`, defaulting to true and specifying whether to compute eigenvectors. If just a number, specifies precision.
* @param {number | BigNumber | OptsObject} [opts] Object with keys `precision`, defaulting to config.relTol, and `eigenvectors`, defaulting to true and specifying whether to compute eigenvectors. If just a number, specifies precision.
* @return {{values: Array|Matrix, eigenvectors?: Array<EVobj>}} Object containing an array of eigenvalues and an array of {value: number|BigNumber, vector: Array|Matrix} objects. The eigenvectors property is undefined if eigenvectors were not requested.
*
*/
Expand Down Expand Up @@ -100,7 +100,7 @@ export const createEigs = /* #__PURE__ */ factory(name, dependencies, ({ config,

function doEigs (mat, opts = {}) {
const computeVectors = 'eigenvectors' in opts ? opts.eigenvectors : true
const prec = opts.precision ?? config.epsilon
const prec = opts.precision ?? config.relTol
const result = computeValuesAndVectors(mat, prec, computeVectors)
if (opts.matricize) {
result.values = matrix(result.values)
Expand Down
6 changes: 3 additions & 3 deletions src/function/matrix/eigs/realSymmetric.js
Expand Up @@ -7,7 +7,7 @@ export function createRealSymmetric ({ config, addScalar, subtract, abs, atan, c
* @param {number} prec
* @param {'number' | 'BigNumber'} type
*/
function main (arr, N, prec = config.epsilon, type, computeVectors) {
function main (arr, N, prec = config.relTol, type, computeVectors) {
if (type === 'number') {
return diag(arr, prec, computeVectors)
}
Expand Down Expand Up @@ -85,7 +85,7 @@ export function createRealSymmetric ({ config, addScalar, subtract, abs, atan, c
// get angle
function getTheta (aii, ajj, aij) {
const denom = (ajj - aii)
if (Math.abs(denom) <= config.epsilon) {
if (Math.abs(denom) <= config.relTol) {
return Math.PI / 4.0
} else {
return 0.5 * Math.atan(2.0 * aij / (ajj - aii))
Expand All @@ -95,7 +95,7 @@ export function createRealSymmetric ({ config, addScalar, subtract, abs, atan, c
// get angle
function getThetaBig (aii, ajj, aij) {
const denom = subtract(ajj, aii)
if (abs(denom) <= config.epsilon) {
if (abs(denom) <= config.relTol) {
return bignumber(-1).acos().div(4)
} else {
return multiplyScalar(0.5, atan(multiply(2.0, aij, inv(denom))))
Expand Down
6 changes: 3 additions & 3 deletions src/function/relational/compare.js
Expand Up @@ -30,7 +30,7 @@ export const createCompare = /* #__PURE__ */ factory(name, dependencies, ({ type
* Compare two values. Returns 1 when x > y, -1 when x < y, and 0 when x == y.
*
* x and y are considered equal when the relative difference between x and y
* is smaller than the configured epsilon. The function cannot be used to
* is smaller than the configured absTol and relTol. The function cannot be used to
* compare values smaller than approximately 2.22e-16.
*
* For matrices, the function is evaluated element wise.
Expand Down Expand Up @@ -72,7 +72,7 @@ export const createCompare = /* #__PURE__ */ factory(name, dependencies, ({ type
},

'BigNumber, BigNumber': function (x, y) {
return bigNearlyEqual(x, y, config.epsilon)
return bigNearlyEqual(x, y, config.relTol, config.absTol)
? new BigNumber(0)
: new BigNumber(x.cmp(y))
},
Expand All @@ -97,7 +97,7 @@ export const createCompare = /* #__PURE__ */ factory(name, dependencies, ({ type
export const createCompareNumber = /* #__PURE__ */ factory(name, ['typed', 'config'], ({ typed, config }) => {
return typed(name, {
'number, number': function (x, y) {
return nearlyEqual(x, y, config.epsilon)
return nearlyEqual(x, y, config.relTol, config.absTol)
? 0
: (x > y ? 1 : -1)
}
Expand Down
2 changes: 1 addition & 1 deletion src/function/relational/compareNatural.js
Expand Up @@ -19,7 +19,7 @@ export const createCompareNatural = /* #__PURE__ */ factory(name, dependencies,
* the function compares in a natural way.
*
* For numeric values, x and y are considered equal when the relative
* difference between x and y is smaller than the configured epsilon.
* difference between x and y is smaller than the configured relTol and absTol.
* The function cannot be used to compare values smaller than
* approximately 2.22e-16.
*
Expand Down
2 changes: 1 addition & 1 deletion src/function/relational/equal.js
Expand Up @@ -23,7 +23,7 @@ export const createEqual = /* #__PURE__ */ factory(name, dependencies, ({ typed,
* Test whether two values are equal.
*
* The function tests whether the relative difference between x and y is
* smaller than the configured epsilon. The function cannot be used to
* smaller than the configured relTol and absTol. The function cannot be used to
* compare values smaller than approximately 2.22e-16.
*
* For matrices, the function is evaluated element wise.
Expand Down