feat: Implement Vector type (#28)
All checks were successful
/ test (push) Successful in 18s

The Vector type is simply a generic type for built-in JavaScript Arrays. The parameter type is the type of all of the entries of the Array. The Vector type also supports inhomogeneous arrays by using the special type `Unknown` as the argument type, but note that when computing with inhomogeneous arrays, method dispatch must be performed separately for every entry in a calculation, making all operations considerably slower than on homogeneous Vector instances.

Note also that arithmetic operations on nested Vectors, e.g. `Vector(Vector(NumberT))` are defined so as to interpret such entities as ordinary matrices, represented in row-major format (i.e., the component `Vector(NumberT)` items of such an entity are the _rows_ of the matrix.

Reviewed-on: #28
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
Glen Whitney 2025-05-07 00:03:49 +00:00 committed by Glen Whitney
parent 0765ba7202
commit 95d81d0338
47 changed files with 1204 additions and 122 deletions

View file

@ -11,7 +11,7 @@ describe('BooleanT Type', () => {
})
it('autoconverts to number type', () => {
assert.strictEqual(math.abs(false), 0)
assert.strictEqual(math.absquare(true), 1)
assert.strictEqual(math.normsq(true), 1)
assert.strictEqual(math.add(true, true), 2)
assert.strictEqual(math.divide(false, true), 0)
assert.strictEqual(math.cbrt(true), 1)

View file

@ -1,3 +1,4 @@
export * as typeDefinition from './BooleanT.js'
export * as logical from './logical.js'
export * as type from './type.js'
export * as utilities from './utils.js'

9
src/boolean/logical.js Normal file
View file

@ -0,0 +1,9 @@
import {BooleanT} from './BooleanT.js'
import {Returns} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
export const not = match(BooleanT, Returns(BooleanT, p => !p))
export const and = match(
[BooleanT, BooleanT], Returns(BooleanT, (p, q) => p && q))
export const or = match(
[BooleanT, BooleanT], Returns(BooleanT, (p, q) => p || q))

View file

@ -9,10 +9,17 @@ const CplxNum = Complex(NumberT)
const {full} = ReturnTyping
describe('complex arithmetic operations', () => {
it('computes absquare of complex numbers', () => {
assert.strictEqual(math.absquare(cplx(3, 4)), 25)
assert.strictEqual(math.absquare(cplx(cplx(2, 3), cplx(4,5))), 54)
assert.strictEqual(math.absquare(cplx(true, true)), 2)
it('computes norm-square of complex numbers', () => {
assert.strictEqual(math.normsq(cplx(3, 4)), 25)
assert.strictEqual(math.normsq(cplx(cplx(2, 3), cplx(4, 5))), 54)
assert.strictEqual(math.normsq(cplx(true, true)), 2)
})
it('computes norm-magnitude of complex numbers', () => {
assert.strictEqual(math.norm(cplx(-3, 4)), 5)
assert.strictEqual(
math.norm(cplx(cplx(2, -3), cplx(-4, 5))), Math.sqrt(54))
const boolnorm = math.norm(cplx(true, true))
assert(math.equal(boolnorm * boolnorm, 2))
})
it('adds complex numbers', () => {
const z = cplx(3, 4)

View file

@ -1,7 +1,7 @@
import assert from 'assert'
import math from '#nanomath'
import {Complex} from '../Complex.js'
import {OneOf, ReturnTyping} from '#core/Type.js'
import {OneOf, ReturnType, ReturnTyping} from '#core/Type.js'
import {NumberT} from '#number/NumberT.js'
@ -38,13 +38,13 @@ describe('complex type operations', () => {
})
it('computes cis of an angle', () => {
const cis = math.cis.resolve(NumberT)
assert.strictEqual(cis.returns, OneOf(Complex(NumberT), NumberT))
assert.strictEqual(ReturnType(cis), OneOf(Complex(NumberT), NumberT))
assert.strictEqual(cis(0), 1)
assert.strictEqual(cis(Math.PI), -1)
assert(math.equal(cis(Math.PI/3), cplx(0.5, Math.sqrt(3)/2)))
math.config.returnTyping = ReturnTyping.full
const ccis = math.cis.resolve(NumberT)
assert.strictEqual(ccis.returns, Complex(NumberT))
assert.strictEqual(ReturnType(ccis), Complex(NumberT))
const one = ccis(0)
assert(one !== 1)
assert(math.equal(one, 1))

View file

@ -1,17 +1,17 @@
import {Complex} from './Complex.js'
import {maybeComplex, promoteBinary, promoteUnary} from './helpers.js'
import {ResolutionError} from '#core/helpers.js'
import {Returns, ReturnTyping} from '#core/Type.js'
import {Returns, ReturnType, ReturnTyping} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
import {ReturnsAs} from '#generic/helpers.js'
const {conservative, full, free} = ReturnTyping
export const absquare = match(Complex, (math, C, strategy) => {
const compAbsq = math.absquare.resolve(C.Component, full)
const R = compAbsq.returns
export const normsq = match(Complex, (math, C, strategy) => {
const compNormsq = math.normsq.resolve(C.Component, full)
const R = ReturnType(compNormsq)
const add = math.add.resolve([R,R], strategy)
return ReturnsAs(add, z => add(compAbsq(z.re), compAbsq(z.im)))
return ReturnsAs(add, z => add(compNormsq(z.re), compNormsq(z.im)))
})
export const add = promoteBinary('add')
@ -19,31 +19,34 @@ export const add = promoteBinary('add')
export const conj = match(Complex, (math, C, strategy) => {
const neg = math.negate.resolve(C.Component, full)
const compConj = math.conj.resolve(C.Component, full)
const cplx = maybeComplex(math, strategy, compConj.returns, neg.returns)
const cplx = maybeComplex(
math, strategy, ReturnType(compConj), ReturnType(neg))
return ReturnsAs(cplx, z => cplx(compConj(z.re), neg(z.im)))
})
export const divide = [
match([Complex, T => !T.complex], (math, [C, R], strategy) => {
const div = math.divide.resolve([C.Component, R], full)
const cplx = maybeComplex(math, strategy, div.returns, div.returns)
const NewComp = ReturnType(div)
const cplx = maybeComplex(math, strategy, NewComp, NewComp)
return ReturnsAs(cplx, (z, r) => cplx(div(z.re, r), div(z.im, r)))
}),
match([Complex, Complex], (math, [W, Z], strategy) => {
const inv = math.invert.resolve(Z, full)
const mult = math.multiply.resolve([W, inv.returns], strategy)
const mult = math.multiply.resolve([W, ReturnType(inv)], strategy)
return ReturnsAs(mult, (w, z) => mult(w, inv(z)))
})
]
export const invert = match(Complex, (math, C, strategy) => {
const conj = math.conj.resolve(C, full)
const norm = math.absquare.resolve(C, full)
const div = math.divide.resolve([C.Component, norm.returns], full)
const cplx = maybeComplex(math, strategy, div.returns, div.returns)
const normsq = math.normsq.resolve(C, full)
const div = math.divide.resolve([C.Component, ReturnType(normsq)], full)
const NewComp = ReturnType(div)
const cplx = maybeComplex(math, strategy, NewComp, NewComp)
return ReturnsAs(cplx, z => {
const c = conj(z)
const d = norm(z)
const d = normsq(z)
return cplx(div(c.re, d), div(c.im, d))
})
})
@ -53,7 +56,8 @@ export const invert = match(Complex, (math, C, strategy) => {
export const multiply = [
match([T => !T.complex, Complex], (math, [R, C], strategy) => {
const mult = math.multiply.resolve([R, C.Component], full)
const cplx = maybeComplex(math, strategy, mult.returns, mult.returns)
const NewComp = ReturnType(mult)
const cplx = maybeComplex(math, strategy, NewComp, NewComp)
return ReturnsAs(cplx, (r, z) => cplx(mult(r, z.re), mult(r, z.im)))
}),
match([Complex, T => !T.complex], (math, [C, R], strategy) => {
@ -62,15 +66,19 @@ export const multiply = [
}),
match([Complex, Complex], (math, [W, Z], strategy) => {
const conj = math.conj.resolve(W.Component, full)
if (conj.returns !== W.Component) {
if (ReturnType(conj) !== W.Component) {
throw new ResolutionError(
`conjugation on ${W.Component} returns type (${conj.returns})`)
`conjugation on ${W.Component} returns different type `
+ `(${ReturnType(conj)})`)
}
const mWZ = math.multiply.resolve([W.Component, Z.Component], full)
const mZW = math.multiply.resolve([Z.Component, W.Component], full)
const sub = math.subtract.resolve([mWZ.returns, mZW.returns], full)
const add = math.add.resolve([mWZ.returns, mZW.returns], full)
const cplx = maybeComplex(math, strategy, sub.returns, add.returns)
const TWZ = ReturnType(mWZ)
const TZW = ReturnType(mZW)
const sub = math.subtract.resolve([TWZ, TZW], full)
const add = math.add.resolve([TWZ, TZW], full)
const cplx = maybeComplex(
math, strategy, ReturnType(sub), ReturnType(add))
return ReturnsAs(cplx, (w, z) => {
const real = sub(mWZ( w.re, z.re), mZW(z.im, conj(w.im)))
const imag = add(mWZ(conj(w.re), z.im), mZW(z.re, w.im))
@ -85,7 +93,7 @@ export const negate = promoteUnary('negate')
// integer coordinates.
export const sqrt = match(Complex, (math, C, strategy) => {
const re = math.re.resolve(C)
const R = re.returns
const R = ReturnType(re)
const isReal = math.isReal.resolve(C)
// dependencies for the real case:
const zComp = math.zero(C.Component)
@ -98,8 +106,8 @@ export const sqrt = match(Complex, (math, C, strategy) => {
const cplx = math.complex.resolve([C.Component, C.Component], full)
// additional dependencies for the complex case
const abs = math.abs.resolve(C, full)
if (abs.returns !== R) {
throw new TypeError(`abs on ${C} returns ${abs.returns}, not ${R}`)
if (ReturnType(abs) !== R) {
throw new TypeError(`abs on ${C} returns ${ReturnType(abs)}, not ${R}`)
}
const addRR = math.add.resolve([R, R], conservative)
const twoR = addRR(oneR, oneR)

View file

@ -1,5 +1,5 @@
import {Complex} from './Complex.js'
import {ReturnTyping} from '#core/Type.js'
import {ReturnType, ReturnTyping} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
import {ReturnsAs} from '#generic/helpers.js'
@ -8,7 +8,7 @@ const {free, full} = ReturnTyping
export const maybeComplex = (math, strategy, Real, Imag) => {
if (strategy !== free) return math.complex.resolve([Real, Imag], strategy)
const cplx = math.complex.resolve([Real, Imag], full)
const prune = math.pruneImaginary.resolve(cplx.returns, full)
const prune = math.pruneImaginary.resolve(ReturnType(cplx), full)
return ReturnsAs(prune, (r, m) => prune(cplx(r, m)))
}
@ -17,7 +17,8 @@ export const promoteUnary = (name, overrideStrategy) => match(
(math, C, strategy) => {
const compOp = math.resolve(name, C.Component, full)
if (overrideStrategy) strategy = overrideStrategy
const cplx = maybeComplex(math, strategy, compOp.returns, compOp.returns)
const NewComp = ReturnType(compOp)
const cplx = maybeComplex(math, strategy, NewComp, NewComp)
return ReturnsAs(cplx, z => cplx(compOp(z.re), compOp(z.im)))
})
@ -32,7 +33,8 @@ export const promoteBinary = name => match(
[Complex, Complex],
(math, [W, Z], strategy) => {
const compOp = math.resolve(name, [W.Component, Z.Component], full)
const cplx = maybeComplex(math, strategy, compOp.returns, compOp.returns)
const NewComp = ReturnType(compOp)
const cplx = maybeComplex(math, strategy, NewComp, NewComp)
return ReturnsAs(
cplx, (w, z) => cplx(compOp(w.re, z.re), compOp(w.im, z.im)))
})

View file

@ -1,5 +1,7 @@
import {Complex} from './Complex.js'
import {OneOf, Returns, ReturnTyping, TypeOfTypes} from "#core/Type.js"
import {
OneOf, Returns, ReturnType, ReturnTyping, TypeOfTypes
} from "#core/Type.js"
import {Any, match} from "#core/TypePatterns.js"
import {BooleanT} from '#boolean/BooleanT.js'
import {NumberT} from '#number/NumberT.js'
@ -32,7 +34,7 @@ export const complex = [
export const arg = // [ // enable when we have atan2 in mathjs
// match(Complex, (math, C) => {
// const re = math.re.resolve(C)
// const R = re.returns
// const R = ReturnType(re)
// const im = math.im.resolve(C)
// const abs = math.abs.resolve(C)
// const atan2 = math.atan2.resolve([R, R], conservative)
@ -54,11 +56,11 @@ export const associate = match(
}
const eq = math.equal.resolve([W, Z], full)
const neg = math.negate.resolve(Z, full)
const eqN = math.equal.resolve([W, neg.returns], full)
const eqN = math.equal.resolve([W, ReturnType(neg)], full)
const mult = math.multiply.resolve([Z, Z], full)
const eqM = math.equal.resolve([W, mult.returns], full)
const negM = math.negate.resolve(mult.returns, full)
const eqNM = math.equal.resolve([W, negM.returns], full)
const eqM = math.equal.resolve([W, ReturnType(mult)], full)
const negM = math.negate.resolve(ReturnType(mult), full)
const eqNM = math.equal.resolve([W, ReturnType(negM)], full)
const iZ = math.complex(math.zero(Z.Component), math.one(Z.Component))
return Returns(BooleanT, (w, z) => {
if (eq(w, z) || eqN(w, neg(z))) return true

View file

@ -13,12 +13,16 @@ As of this writing, the only two types required to be in a TypeDispatcher are
Undefined (the type inhabited only by `undefined`) and TypeOfTypes (the type
inhabited exactly by Type objects).
There is also a constant NotAType which is the type-world analogue of NaN for
numbers. It is occasionally used for the rare behavior that truly does not
There is also a constant Unknown which is the type that is used when it is
not possible in advance to determine what the type of an entity is or will
be. It is, for example, used for the occasional behavior that truly does not
return any particular type, such as the method `zero` that takes a Type and
returns its zero element. However, it does not really work as a Type, and in
particular, do _not_ merge it into any TypeDispatcher -- it will disrupt the
type and method resolution process.
returns its zero element. It is also used for the component type of instances
of containers, like Vector, that have inhomogeneous contents -- and therefore,
as the return type of `sum` on a `Vector(Unknown)`. However, it lacks much of
the machinery of other Type entities. In particular, do _not_ attempt to merge
Unknown as a type into any TypeDispatcher -- it will disrupt the type and
method resolution process.
## Core methods

View file

@ -66,31 +66,43 @@ export const Undefined = new Type(
t => typeof t === 'undefined',
{zero: undefined, one: undefined, nan: undefined})
export const TypeOfTypes = new Type(t => t instanceof Type)
export const NotAType = new Type(() => true) // Danger, do not merge!
NotAType._doNotMerge = true
export const Unknown = new Type(() => true) // Danger, do not merge!
Unknown._doNotMerge = true
const unionDirectory = new ArrayKeyedMap() // make sure only one of each union
export const OneOf = (...types) => {
if (!types.length) {
throw new RangeError('cannot choose OneOf no types at all')
}
const nonType = types.findIndex(T => !(T instanceof Type))
if (nonType >= 0) {
throw new RangeError(
`OneOf can only take type arguments, not ${types[nonType]}`)
}
const typeSet = new Set(types) // remove duplicates
const typeSet = new Set() // remove duplicates:
for (const type of types) {
if (type.unifies) {
type.unifies.forEach(t => typeSet.add(t))
} else typeSet.add(type)
}
if (typeSet.size === 1) return types[0]
const typeList = Array.from(typeSet).sort() // canonical order
const generic = typeList.find(T => !T.concrete)
if (generic) {
throw new RangeError(`OneOf can only take concrete types, not ${generic}`)
}
if (!unionDirectory.has(typeList)) {
unionDirectory.set(typeList, new Type(
const unionType = new Type(
t => typeList.some(T => T.test(t)),
{typeName: typeList.join('|')}))
{typeName: typeList.join('|')})
unionType.unifies = typeList
unionDirectory.set(typeList, unionType)
}
return unionDirectory.get(typeList)
}
export const Returns = (type, f) => (f.returns = type, f)
export const Returns = (type, f) => (f.returns = type, f.isBehavior = true, f)
export const ReturnType = f => f.returns ?? Unknown
export const whichType = typs => Returns(TypeOfTypes, item => {
for (const type of Object.values(typs)) {

View file

@ -3,7 +3,9 @@ import ArrayKeyedMap from 'array-keyed-map'
import {ResolutionError, isPlainFunction, isPlainObject} from './helpers.js'
import {Implementations, ImplementationsGenerator} from './Implementations.js'
import {bootstrapTypes} from './type.js'
import {Returns, ReturnTyping, whichType, Type} from './Type.js'
import {
Returns, ReturnType, ReturnTyping, Unknown, Type, whichType
} from './Type.js'
import {
matched, needsCollection, Passthru, Matcher, match
} from './TypePatterns.js'
@ -253,7 +255,7 @@ export class TypeDispatcher {
if ('actual' in template) { // incorporate conversion
let convert = template.convertor
// Check if it's a factory:
if (!convert.returns) convert = convert(this, template.actual)
if (!convert.isBehavior) convert = convert(this, template.actual)
extractor = args => convert(args[from])
}
return state ? extractor : args => [extractor(args)]
@ -273,6 +275,14 @@ export class TypeDispatcher {
throw new ReferenceError(`no method or value for key '${key}'`)
}
if (!Array.isArray(types)) types = [types]
if (types.some(T => T === Unknown)) {
if (!strategy) return this[key]
const thisTypeOf = whichType(this.types)
return (...args) => {
const types = args.map(thisTypeOf)
return this.resolve(key, types, strategy)(...args)
}
}
if (!strategy) {
// Avoid recursing on obtaining config
if (key === 'config') strategy = ReturnTyping.free
@ -309,9 +319,9 @@ export class TypeDispatcher {
let needItem = true
let item
let template
let pattern
if (imps.length) {
for (const options of [{}, {convert: true}]) {
let pattern
for ([pattern, item] of imps) {
let finalIndex
;[finalIndex, template] = pattern.match(types, options)
@ -327,11 +337,18 @@ export class TypeDispatcher {
needItem = false
item = this._fallbacks[key]
template = types
pattern = Passthru
}
if (needItem) {
throw new ResolutionError(
`no matching definition of '${key}' on '${types}'`)
}
/* The following message is often helpful in debugging, so left it
in but commented out; likely at some point we should have some
sort of trace facility that this would be a part of:
*/
// console.log(`RESOLVING ${key} on ${types} matches ${pattern}`)
// If this key is producing a non-function value, we're done
if (!isPlainFunction(item)) {
behave.set(bhvix, item)
@ -373,12 +390,7 @@ export class TypeDispatcher {
finalBehavior = theBehavior
if (typeof theBehavior === 'function') {
const returning = theBehavior.returns
if (!returning) {
throw new TypeError(
`No return type specified for ${key} on ${types} with`
+ ` return typing ${ReturnTyping.name(strategy)}`)
}
const returning = ReturnType(theBehavior)
if (needsCollection(template)) {
// have to wrap the behavior to collect the actual arguments
// in the way corresponding to the template. Generating that

View file

@ -1,4 +1,4 @@
import {Type, Undefined} from './Type.js'
import {ReturnType, Type, Undefined, Unknown} from './Type.js'
import {isPlainFunction} from './helpers.js'
export class TypePattern {
@ -9,6 +9,7 @@ export class TypePattern {
throw new Error('Specific TypePatterns must implement sampleTypes')
}
equal(other) {return other.constructor === this.constructor}
toString() {return 'Abstract Pattern (?!)'}
}
class MatchTypePattern extends TypePattern {
@ -34,6 +35,7 @@ class MatchTypePattern extends TypePattern {
}
sampleTypes() {return [this.type]}
equal(other) {return super.equal(other) && this.type === other.type}
toString() {return `Match(${this.type})`}
}
class SequencePattern extends TypePattern {
@ -51,7 +53,8 @@ class SequencePattern extends TypePattern {
const [newPos, newMatch] = pat.match(typeSequence, options)
if (newPos < 0) return [-1, Undefined]
options.position = newPos
matches.push(newMatch)
if (Array.isArray(newMatch)) matches.push(...newMatch)
else matches.push(newMatch)
}
return [options.position, matches]
}
@ -63,6 +66,7 @@ class SequencePattern extends TypePattern {
&& this.patterns.length === other.patterns.length
&& this.patterns.every((elt, ix) => elt.equal(other.patterns[ix]))
}
toString() {return `[${this.patterns}]`}
}
class PredicatePattern extends TypePattern {
@ -83,6 +87,7 @@ class PredicatePattern extends TypePattern {
equal(other) {
return super.equal(other) && this.predicate === other.predicate
}
toString() {return `Test(${this.predicate})`}
}
export const pattern = patternOrSpec => {
@ -105,6 +110,7 @@ class AnyPattern extends TypePattern {
: [-1, Undefined]
}
sampleTypes() {return [Undefined]}
toString() {return 'Any'}
}
export const Any = new AnyPattern()
@ -125,12 +131,13 @@ class OptionalPattern extends TypePattern {
options.position = newPos
matches.push(newMatch)
}
return [options.position, matches]
return [options.position, [matches]]
}
sampleTypes() {return []}
equal(other) {
return super.equal(other) && this.pattern.equal(other.pattern)
}
toString() {return `?${this.pattern}`}
}
export const Optional = item => new OptionalPattern(item)
@ -148,7 +155,7 @@ class MultiPattern extends TypePattern {
const matches = []
while (true) {
const [newPos, newMatch] = this.pattern.match(typeSequence, options)
if (newPos < 0) return [options.position, matches]
if (newPos < 0) return [options.position, [matches]]
options.position = newPos
matches.push(newMatch)
}
@ -157,6 +164,7 @@ class MultiPattern extends TypePattern {
equal(other) {
return super.equal(other) && this.pattern.equal(other.pattern)
}
toString() {return `${this.pattern}*`}
}
export const Multiple = item => new MultiPattern(item)
@ -169,6 +177,7 @@ class PassthruPattern extends TypePattern {
return [typeSequence.length, typeSequence.slice(position)]
}
sampleTypes() {return []}
toString() {return 'Passthru'}
}
export const Passthru = new PassthruPattern()
@ -180,8 +189,10 @@ export const matched = (template, math) => {
}
if (template.matched) {
let convert = template.convertor
if (!convert.returns) convert = convert(math, template.actual)
return convert.returns || template.matched
if (!convert.isBehavior) convert = convert(math, template.actual)
const ConvertsTo = ReturnType(convert)
if (ConvertsTo !== Unknown) return ConvertsTo
return template.matched
}
return template
}

View file

@ -2,7 +2,7 @@ import assert from 'assert'
import math from '#nanomath'
import {NumberT} from '#number/NumberT.js'
import {Returns, ReturnTyping} from '../Type.js'
import {Returns, ReturnType, ReturnTyping} from '../Type.js'
import {isPlainFunction} from '../helpers.js'
describe('Core types', () => {
@ -36,7 +36,7 @@ describe('Core types', () => {
it('provides a return-value labeling', () => {
const labeledF = Returns(math.types.Undefined, () => undefined)
assert.strictEqual(typeof labeledF, 'function')
assert.strictEqual(labeledF.returns, math.types.Undefined)
assert.strictEqual(ReturnType(labeledF), math.types.Undefined)
assert(isPlainFunction(labeledF))
})

View file

@ -6,7 +6,7 @@ import * as numbers from '#number/all.js'
import {NumberT} from '#number/NumberT.js'
import {ResolutionError} from "#core/helpers.js"
import {match, Any} from "#core/TypePatterns.js"
import {NotAType, Returns, ReturnTyping} from "#core/Type.js"
import {Returns, ReturnType, ReturnTyping, Unknown} from "#core/Type.js"
import {plain} from "#number/helpers.js"
describe('TypeDispatcher', () => {
@ -18,7 +18,7 @@ describe('TypeDispatcher', () => {
const {NumberT, TypeOfTypes, Undefined} = incremental.types
assert(NumberT.test(7))
assert.strictEqual(incremental.add(-1.5, 0.5), -1)
assert.throws(() => incremental.add(7, undefined), ResolutionError)
assert.throws(() => incremental.add(7, NumberT), ResolutionError)
// Make Undefined act like zero:
incremental.merge({add: [
match([Undefined, Any], (_m, [_U, T]) => Returns(T, (_a, b) => b)),
@ -26,11 +26,11 @@ describe('TypeDispatcher', () => {
]})
assert.strictEqual(incremental.add(7, undefined), 7)
assert.strictEqual(
incremental.resolve('add', [TypeOfTypes, Undefined]).returns,
ReturnType(incremental.resolve('add', [TypeOfTypes, Undefined])),
TypeOfTypes)
assert.strictEqual(incremental.add(undefined, -3.25), -3.25)
assert.strictEqual(
incremental.add.resolve([Undefined, NumberT]).returns,
ReturnType(incremental.add.resolve([Undefined, NumberT])),
NumberT)
// Oops, changed my mind ;-), make it work like NaN with numbers:
const alwaysNaN = Returns(NumberT, () => NaN)
@ -40,7 +40,7 @@ describe('TypeDispatcher', () => {
]})
assert(isNaN(incremental.add(undefined, -3.25)))
assert.strictEqual(
incremental.add.resolve([Undefined, NumberT]).returns,
ReturnType(incremental.add.resolve([Undefined, NumberT])),
NumberT)
assert.strictEqual(incremental.isnan(NaN), 1)
incremental.merge(booleans)
@ -68,9 +68,9 @@ describe('TypeDispatcher', () => {
assert(!bgn._behaviors.negate.has([ReturnTyping.free, BooleanT]))
assert.strictEqual(bgn.negate(true), -2)
})
it('disallows merging NotAType', () => {
it('disallows merging Unknown', () => {
const doomed = new TypeDispatcher()
assert.throws(() => doomed.merge({NaT: NotAType}), TypeError)
assert.throws(() => doomed.merge({Unknown}), TypeError)
})
it('supports generic types', () => {
assert.throws(() => NumberT(0), TypeError)

View file

@ -53,6 +53,10 @@ describe('Type patterns', () => {
midOpt.match([Undefined, TypeOfTypes, TypeOfTypes]),
[-1, Undefined])
assert.deepStrictEqual(midOpt.sampleTypes(), [Undefined, Undefined])
const justMulti = pattern(Multiple(Undefined))
assert.deepStrictEqual(
justMulti.match([Undefined, Undefined]),
[2, [[Undefined, Undefined]]])
const midMulti = pattern([Undefined, Multiple(TypeOfTypes), Undefined])
assert.deepStrictEqual(
midMulti.match([Undefined, Undefined]),

View file

@ -1,2 +1,3 @@
export * from './arithmetic.js'
export * from './relational.js'
export * from './utils.js'

View file

@ -0,0 +1,6 @@
import {Returns, Undefined} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
import {boolnum} from '#number/helpers.js'
export const isnan = match(Undefined, boolnum(() => true))
export const negate = match(Undefined, Returns(Undefined, () => undefined))

View file

@ -1,8 +1,14 @@
import {TypeOfTypes, Undefined} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
import {Any, Multiple, match} from '#core/TypePatterns.js'
import {boolnum} from '#number/helpers.js'
export const indistinguishable = [
match([Undefined, Undefined], boolnum(() => true)),
match([TypeOfTypes, TypeOfTypes], boolnum((t, u) => t === u))
// I don't think there's any other type that should be indistinguishable
// from undefined:
match([Undefined, Any, Multiple(Any)], boolnum(() => false)),
match([Any, Undefined, Multiple(Any)], boolnum(() => false)),
match([Undefined, Undefined, Multiple(Any)], boolnum(() => true)),
match(
[TypeOfTypes, TypeOfTypes, Multiple(Any)],
boolnum((t, u) => t === u))
]

View file

@ -1,15 +1,23 @@
import {NotAType, Returns, TypeOfTypes} from '#core/Type.js'
import {Returns, TypeOfTypes, Unknown} from '#core/Type.js'
import {match,Any} from "#core/TypePatterns.js"
import {boolnum} from "#number/helpers.js"
export const haszero = [
match(Any, (math, T) => {
const answer = math.haszero(T)
return Returns(math.typeOf(answer), () => answer)
}),
match(TypeOfTypes, boolnum(T => 'zero' in T))
]
export const zero = [
match(Any, (math, T) => {
const z = math.zero(T)
return Returns(T, () => z)
}),
match(TypeOfTypes, Returns(NotAType, t => {
if ('zero' in t) return t.zero
throw new RangeError(`type '${t}' has no zero element`)
match(TypeOfTypes, Returns(Unknown, T => {
if ('zero' in T) return T.zero
throw new RangeError(`type '${T}' has no zero element`)
}))
]
@ -18,10 +26,10 @@ export const one = [
const unit = math.one(T)
return Returns(T, () => unit)
}),
match(TypeOfTypes, Returns(NotAType, t => {
if ('one' in t) return t.one
match(TypeOfTypes, Returns(Unknown, T => {
if ('one' in T) return T.one
throw new RangeError(
`type '${t}' has no unit element designated as "one"`)
`type '${T}' has no unit element designated as "one"`)
}))
]
@ -30,7 +38,7 @@ export const hasnan = [
const answer = math.hasnan(T)
return Returns(math.typeOf(answer), () => answer)
}),
match(TypeOfTypes, boolnum(t => 'nan' in t))
match(TypeOfTypes, boolnum(T => 'nan' in T))
]
export const nan = [
@ -38,9 +46,9 @@ export const nan = [
const notanum = math.nan(T)
return Returns(T, () => notanum)
}),
match(TypeOfTypes, Returns(NotAType, t => {
if ('nan' in t) return t.nan
match(TypeOfTypes, Returns(Unknown, T => {
if ('nan' in T) return T.nan
throw new RangeError(
`type '${t}' has no "not a number" element`)
`type '${T}' has no "not a number" element`)
}))
]

View file

@ -1,6 +1,6 @@
import assert from 'assert'
import math from '#nanomath'
import {ReturnTyping} from '#core/Type.js'
import {ReturnType, ReturnTyping} from '#core/Type.js'
const {Complex, NumberT} = math.types
@ -8,7 +8,7 @@ describe('generic arithmetic', () => {
it('squares anything', () => {
const sq = math.square
assert.strictEqual(sq(7), 49)
assert.strictEqual(math.square.resolve([NumberT]).returns, NumberT)
assert.strictEqual(ReturnType(math.square.resolve([NumberT])), NumberT)
assert.deepStrictEqual(sq(math.complex(3, 4)), math.complex(-7, 24))
const eyes = math.complex(0, 2)
assert.strictEqual(sq(eyes), -4)

View file

@ -48,7 +48,7 @@ describe('generic relational functions', () => {
assert.throws(() => compare(false, NaN), TypeError)
assert(isNaN(compare(NaN, NaN)))
assert.throws(() => compare(true, false), TypeError)
assert.throws(() => compare(undefined, -1), ResolutionError)
assert.throws(() => compare(math.types.BooleanT, -1), ResolutionError)
})
it('determines the sign of numeric values', () => {
const {sign} = math

View file

@ -1,3 +1,4 @@
export * as arithmetic from './arithmetic.js'
export * as logical from './logical.js'
export * as relational from './relational.js'
export * as utilities from './utils.js'

View file

@ -1,15 +1,20 @@
import {ReturnsAs} from './helpers.js'
import {Returns, ReturnTyping} from '#core/Type.js'
import {Returns, ReturnType, ReturnTyping} from '#core/Type.js'
import {match, Any} from '#core/TypePatterns.js'
export const abs = match(Any, (math, T) => {
const absq = math.absquare.resolve(T)
const sqrt = math.sqrt.resolve(absq.returns, ReturnTyping.conservative)
return ReturnsAs(sqrt, t => sqrt(absq(t)))
export const norm = match(Any, (math, T) => {
const normsq = math.normsq.resolve(T)
const sqrt = math.sqrt.resolve(
ReturnType(normsq), ReturnTyping.conservative)
return ReturnsAs(sqrt, t => sqrt(normsq(t)))
})
export const abs = norm // coincide for most types (scalars)
export const conj = match(Any, (_math, T) => Returns(T, t => t))
export const square = match(Any, (math, T, strategy) => {
const mult = math.multiply.resolve([T, T], strategy)
return Returns(mult.returns, t => mult(t, t))
return ReturnsAs(mult, t => mult(t, t))
})

View file

@ -1 +1,3 @@
export const ReturnsAs = (g, f) => (f.returns = g.returns, f)
import {Returns, ReturnType} from '#core/Type.js'
export const ReturnsAs = (g, f) => Returns(ReturnType(g), f)

40
src/generic/logical.js Normal file
View file

@ -0,0 +1,40 @@
import {ReturnsAs} from './helpers.js'
import {OneOf, Returns, ReturnType} from '#core/Type.js'
import {Any, Multiple, match} from '#core/TypePatterns.js'
import {boolnum} from '#number/helpers.js'
export const not = match(Any, (math, T) => {
const bool = math.boolean.resolve(T)
return ReturnsAs(bool, t => !bool(t))
})
export const and = [
match([], boolnum(() => true)),
match(Any, (_math, T) => Returns(T, t => t)),
match([Any, Any], (math, [T, U]) => {
const bool = math.boolean.resolve(T)
return Returns(OneOf(T, U), (t, u) => bool(t) ? u : t)
}),
match([Any, Any, Any, Multiple(Any)], (math, [T, U, V, Rest]) => {
const andRest = math.and.resolve([U, V, ...Rest])
const andFirst = math.and.resolve([T, ReturnType(andRest)])
return ReturnsAs(
andFirst, (t, u, v, rest) => andFirst(t, andRest(u, v, ...rest)))
})
]
export const or = [
match([], boolnum(() => false)),
match(Any, (_math, T) => Returns(T, t => t)),
match([Any, Any], (math, [T, U]) => {
const bool = math.boolean.resolve(T)
return Returns(OneOf(T, U), (t, u) => bool(t) ? t : u)
}),
match([Any, Any, Any, Multiple(Any)], (math, [T, U, V, Rest]) => {
const orRest = math.or.resolve([U, V, ...Rest])
const orFirst = math.and.resolve([T, ReturnType(orRest)])
return ReturnsAs(
orFirst, (t, u, v, rest) => orFirst(t, orRest(u, v, ...rest)))
})
]

View file

@ -1,5 +1,5 @@
import {ReturnsAs} from './helpers.js'
import {Returns, ReturnTyping} from '#core/Type.js'
import {Returns, ReturnType, ReturnTyping} from '#core/Type.js'
import {Any, Passthru, match, matched} from '#core/TypePatterns.js'
import {boolnum} from '#number/helpers.js'
@ -39,6 +39,9 @@ export const equal = match([Any, Any], (math, [T, U]) => {
// now that we have `equal` and `exceeds`, pretty much everything else should
// be easy:
export const deepEqual = match(
Passthru, (math, types, strategy) => math.equal.resolve(types, strategy))
export const compare = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const gt = math.exceeds.resolve([T, U])
@ -79,35 +82,42 @@ export const sign = match(Any, (math, T) => {
export const unequal = match(Passthru, (math, types) => {
const eq = math.equal.resolve(types)
return ReturnsAs(eq, (...args) => !eq(...args))
const not = math.not.resolve(ReturnType(eq))
return ReturnsAs(not, (...args) => not(eq(...args)))
})
export const larger = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const bigger = math.exceeds.resolve([T, U])
return boolnum((t, u) => !eq(t, u) && bigger(t, u))(math)
const not = math.not.resolve(ReturnType(eq))
const and = math.and.resolve([ReturnType(not), ReturnType(bigger)])
return ReturnsAs(and, (t, u) => and(not(eq(t, u)), bigger(t, u)))
})
export const isPositive = match(Any, (math, T) => {
const zero = math.zero(T)
const larger = math.larger.resolve([T, T])
return boolnum(t => larger(t, zero))
return ReturnsAs(larger, t => larger(t, zero))
})
export const largerEq = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const bigger = math.exceeds.resolve([T, U])
return ReturnsAs(bigger, (t, u) => eq(t, u) || bigger(t, u))
const or = math.or.resolve([ReturnType(eq), ReturnType(bigger)])
return ReturnsAs(or, (t, u) => or(eq(t, u), bigger(t, u)))
})
export const smaller = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const bigger = math.exceeds.resolve([U, T])
return boolnum((t, u) => !eq(t, u) && bigger(u, t))(math)
const not = math.not.resolve(ReturnType(eq))
const and = math.and.resolve([ReturnType(not), ReturnType(bigger)])
return ReturnsAs(and, (t, u) => and(not(eq(t, u)), bigger(u, t)))
})
export const smallerEq = match([Any, Any], (math, [T, U]) => {
const eq = math.equal.resolve([T, U])
const bigger = math.exceeds.resolve([U, T])
return ReturnsAs(bigger, (t, u) => eq(t, u) || bigger(u, t))
const or = math.or.resolve([ReturnType(eq), ReturnType(bigger)])
return ReturnsAs(or, (t, u) => or(eq(t, u), bigger(u, t)))
})

View file

@ -18,7 +18,7 @@ export const isZero = match(Passthru, (math, [T]) => {
const eq = math.equal.resolve([T, T])
return ReturnsAs(eq, x => eq(z, x))
})
export const nonImaginary = match(Passthru, boolnum(() => true))
export const nonImaginary = match(Any, boolnum(() => true))
export const re = match(Any, (_math, T) => Returns(T, t => t))
export const im = match(Any, (math, T) => {
const z = math.zero(T)

View file

@ -1,8 +1,9 @@
import * as booleans from './boolean/all.js'
import * as coretypes from './coretypes/all.js'
import * as booleans from './boolean/all.js'
import * as complex from './complex/all.js'
import * as generics from './generic/all.js'
import * as numbers from './number/all.js'
import * as complex from './complex/all.js'
import * as vectors from './vector/all.js'
import {TypeDispatcher} from '#core/TypeDispatcher.js'
// At the moment, since we are not sorting patterns in any way,
@ -12,9 +13,10 @@ import {TypeDispatcher} from '#core/TypeDispatcher.js'
// whatever has been merged before.)
// Hence, in building the math instance, we put generics first because
// they should only kick in when there are not specific implementations,
// and complex next becausewe want its conversion (which converts _any_
// and complex next because we want its conversion (which converts _any_
// non-complex type to complex, potentially making a poor overload choice)
// to be tried last.
const math = new TypeDispatcher(generics, complex, booleans, coretypes, numbers)
const math = new TypeDispatcher(
generics, complex, vectors, booleans, coretypes, numbers)
export default math

View file

@ -5,7 +5,8 @@ import {ReturnTyping} from '#core/Type.js'
describe('number arithmetic', () => {
it('supports basic operations', () => {
assert.strictEqual(math.abs(-Infinity), Infinity)
assert.strictEqual(math.absquare(-2), 4)
assert(isNaN(math.norm(NaN)))
assert.strictEqual(math.normsq(-2), 4)
assert.strictEqual(math.add(2, 2), 4)
assert.strictEqual(math.divide(6, 4), 1.5)
assert.strictEqual(math.cbrt(-8), -2)

View file

@ -1,5 +1,6 @@
export * as typeDefinition from './NumberT.js'
export * as arithmetic from './arithmetic.js'
export * as logical from './logical.js'
export * as relational from './relational.js'
export * as type from './type.js'
export * as utils from './utils.js'

View file

@ -1,14 +1,19 @@
import {plain} from './helpers.js'
import {NumberT} from './NumberT.js'
import {OneOf, Returns, ReturnTyping} from '#core/Type.js'
import {OneOf, Returns, ReturnTyping, Undefined} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
import {Complex} from '#complex/Complex.js'
const {conservative, full} = ReturnTyping
export const abs = plain(Math.abs)
export const absquare = plain(a => a*a)
export const add = plain((a, b) => a + b)
export const norm = abs
export const normsq = plain(a => a*a)
export const add = [
plain((a, b) => a + b),
match([Undefined, NumberT], Returns(NumberT, () => NaN)),
match([NumberT, Undefined], Returns(NumberT, () => NaN))
]
export const divide = plain((a, b) => a / b)
export const cbrt = plain(a => {
if (a === 0) return a
@ -22,7 +27,11 @@ export const cbrt = plain(a => {
return negate ? -result : result
})
export const invert = plain(a => 1/a)
export const multiply = plain((a, b) => a * b)
export const multiply = [
plain((a, b) => a * b),
match([Undefined, NumberT], Returns(NumberT, () => NaN)),
match([NumberT, Undefined], Returns(NumberT, () => NaN))
]
export const negate = plain(a => -a)
export const sqrt = match(NumberT, (math, _N, strategy) => {
@ -44,5 +53,10 @@ export const sqrt = match(NumberT, (math, _N, strategy) => {
})
})
export const subtract = plain((a, b) => a - b)
export const subtract = [
plain((a, b) => a - b),
match([Undefined, NumberT], Returns(NumberT, () => NaN)),
match([NumberT, Undefined], Returns(NumberT, () => NaN))
]
export const quotient = plain((a,b) => Math.floor(a/b))

6
src/number/logical.js Normal file
View file

@ -0,0 +1,6 @@
import {boolnum} from './helpers.js'
import {NumberT} from './NumberT.js'
import {match} from '#core/TypePatterns.js'
export const not = match(NumberT, boolnum(a => !a))

View file

@ -1,7 +1,9 @@
import {match, Optional} from '#core/TypePatterns.js'
import {boolnum} from './helpers.js'
import {NumberT} from './NumberT.js'
import {Undefined} from '#core/Type.js'
import {match, Optional} from '#core/TypePatterns.js'
// In nanomath, we take the point of view that two comparators are primitive:
// indistinguishable(a, b, relTol, absTol), and exceeds(a, b). All others
// are defined generically in terms of these. They typically return BooleanT,
@ -32,4 +34,9 @@ export const indistinguishable = match(
// Returns truthy if a (interpreted as completely precise) represents a
// greater value than b (interpreted as completely precise). Note that even if
// so, a and b might be indistinguishable() to some tolerances.
export const exceeds = match([NumberT, NumberT], boolnum((a, b) => a > b))
export const exceeds = [
match([NumberT, NumberT], boolnum((a, b) => a > b)),
// Needed to allow comparison of vectors of different lengths:
match([Undefined, NumberT], boolnum(() => false)),
match([NumberT, Undefined], boolnum(() => false))
]

View file

@ -1,8 +1,10 @@
import {plain} from './helpers.js'
import {BooleanT} from '#boolean/BooleanT.js'
import {Returns} from '#core/Type.js'
import {NumberT} from './NumberT.js'
import {Returns, ReturnType} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
import {NumberT} from '#number/NumberT.js'
import {BooleanT} from '#boolean/BooleanT.js'
import {Complex} from '#complex/Complex.js'
const num = f => Returns(NumberT, f)
@ -10,5 +12,17 @@ export const number = [
plain(a => a),
// conversions from Boolean should be consistent with one and zero:
match(BooleanT, num(p => p ? NumberT.one : NumberT.zero)),
match([], num(() => 0))
match([], num(() => 0)),
match(Complex, (math, C) => {
const im = math.im.resolve(C)
const re = math.re.resolve(C)
const compNum = math.number.resolve(ReturnType(re))
const isZ = math.isZero.resolve(ReturnType(im))
return num(z => {
if (!isZ(im(z))) {
throw new RangeError(`can't convert Complex ${z} to number`)
}
return compNum(re(z))
})
})
]

48
src/vector/Vector.js Normal file
View file

@ -0,0 +1,48 @@
import {Type, Unknown} from '#core/Type.js'
const isVector = v => Array.isArray(v)
export const Vector = new Type(isVector, {
specialize(CompType) {
const compTest = CompType.test
const specTest = v => isVector(v) && v.every(compTest)
const typeName = `Vector(${CompType})`
if (CompType.concrete) {
const typeOptions = {typeName}
if ('zero' in CompType) typeOptions.zero = [CompType.zero]
const vectorCompType = new Type(specTest, typeOptions)
vectorCompType.Component = CompType
vectorCompType.vectorDepth = (CompType.vectorDepth ?? 0) + 1
return vectorCompType
}
// Wrapping a generic type in Vector
return new Type(specTest, {
typeName,
specialize: (...args) => this.specialize(CompType.specialize(...args)),
specializesTo: VT => this.specializesTo(VT)
&& CompType.specializesTo(VT.Component),
refine: (v, typer) => {
if (!v.length) {
throw new RangeError(
`no way to refine ${typeName} on an empty vector`)
}
const eltTypes = v.map(elt => CompType.refine(elt, typer))
const newCompType = eltTypes[0]
if (eltTypes.some(T => T !== newCompType)) {
throw new TypeError(
`can't refine ${typeName} to ${v}; `
+ `not all entries have type ${newCompType}`)
}
return this.specialize(newCompType)
}
})
},
specializesTo: VT => 'vectorDepth' in VT && VT.vectorDepth > 0,
refine(v, typer) {
if (!v.length) return this.specialize(Unknown) // what else could we do?
const eltTypes = v.map(elt => typer(elt))
let compType = eltTypes[0]
if (eltTypes.some(T => T !== compType)) compType = Unknown
return this.specialize(compType)
}
})

View file

@ -0,0 +1,35 @@
import assert from 'assert'
import {Vector} from '../Vector.js'
import {Unknown} from '#core/Type.js'
import math from '#nanomath'
describe('Vector Type', () => {
it('correctly recognizes vectors', () => {
assert(Vector.test([3, 4]))
assert(!Vector.test({re: 3, im: 4}))
})
it('can fully type vectors', () => {
const {BooleanT, Complex, NumberT} = math.types
const typ = math.typeOf
const cplx = math.complex
assert.strictEqual(typ([3, 4]), Vector(NumberT))
assert.strictEqual(typ([true, false]), Vector(BooleanT))
assert.strictEqual(typ([3, false]), Vector(Unknown))
assert.strictEqual(typ([cplx(3, 4), cplx(5)]), Vector(Complex(NumberT)))
assert.strictEqual(typ([[3, 4], [5]]), Vector(Vector(NumberT)))
})
it('can refine when nested', () => {
const {Complex, NumberT} = math.types
const cplx = math.complex
const VecComplex = Vector(Complex)
const vcEx = [cplx(3, 4), cplx(5)]
assert(VecComplex.test(vcEx))
const vcType = VecComplex.refine(vcEx, math.typeOf)
assert.strictEqual(vcType, Vector(Complex(NumberT)))
const CplxVec = Complex(Vector)
const cvEx = cplx([3,4], [5, 0])
assert(CplxVec.test(cvEx))
const cvType = CplxVec.refine(cvEx, math.typeOf)
assert.strictEqual(cvType, Complex(Vector(NumberT)))
})
})

View file

@ -0,0 +1,82 @@
import assert from 'assert'
import math from '#nanomath'
describe('Vector arithmetic functions', () => {
const cplx = math.complex
const cV = [cplx(3, 4), cplx(4, -3), cplx(-3, -4)]
it('distributes abs elementwise', () => {
const abs = math.abs
assert.deepStrictEqual(abs([-3, 4, -5]), [3, 4, 5])
assert.deepStrictEqual(abs(cV), [5, 5, 5])
assert.deepStrictEqual(abs([true, -4, cplx(4, 3)]), [1, 4, 5])
})
it('computes the norm of a vector or matrix', () => {
const norm = math.norm
assert.strictEqual(norm([-3, 4, -5]), Math.sqrt(50))
assert.strictEqual(norm([[1, 2], [4, 2]]), 5)
assert.strictEqual(norm(cV), Math.sqrt(75))
})
it('adds vectors and matrices', () => {
const add = math.add
assert.deepStrictEqual(add([-3, 4, -5], [8, 0, 2]), [5, 4, -3])
assert.deepStrictEqual(
add([[1, 2], [4, 2]], [[0, -1], [3, -4]]), [[1, 1], [7, -2]])
assert.deepStrictEqual(
add([[1, 2], [4, 2]], [0, -1]), [[1, 1], [4, 1]])
})
it('(pseudo)inverts matrices', () => {
const inv = math.invert
// inverses
assert.deepStrictEqual(inv([3, 4, 5]), [3/50, 2/25, 1/10])
assert.deepStrictEqual(inv([[4]]), [[0.25]])
assert.deepStrictEqual(inv([[5, 2], [-7, -3]]), [[3, 2], [-7, -5]])
assert(math.equal(
inv([[3, 0, 2], [2, 1, 0], [1, 4, 2]]),
[[1/10, 2/5, -1/10], [-1/5, 1/5, 1/5], [7/20, -3/5, 3/20]]))
// pseudoinverses
assert.deepStrictEqual(inv([[1, 0], [1, 0]]), [[1/2, 1/2], [0, 0]])
assert.deepStrictEqual(
inv([[1, 0], [0, 1], [0, 1]]), [[1, 0, 0], [0, 1/2, 1/2]])
assert.deepStrictEqual(
inv([[1, 0, 0, 0, 2],
[0, 0, 3, 0, 0],
[0, 0, 0, 0, 0],
[0, 4, 0, 0, 0]]),
[[1/5, 0, 0, 0],
[ 0, 0, 0, 1/4],
[ 0, 1/3, 0, 0],
[ 0, 0, 0, 0],
[2/5, 0, 0, 0]])
})
it('multiplies vectors and matrices', () => {
const mult = math.multiply
const pyth = [3, 4, 5]
assert.deepStrictEqual(mult(pyth, 2), [6, 8, 10])
assert.deepStrictEqual(mult(-3, pyth), [-9, -12, -15])
assert.strictEqual(mult(pyth, pyth), 50)
const mat23 = [[1, 2, 3], [-3, -2, -1]]
assert.deepStrictEqual(mult(mat23, pyth), [26, -22])
const mat32 = math.transpose(mat23)
assert.deepStrictEqual(mult(pyth, mat32), [26, -22])
assert.deepStrictEqual(mult(mat23, mat32), [[14, -10], [-10, 14]])
assert.deepStrictEqual(
mult(mat32, [[1, 2], [3, 4]]),
[[-8, -10], [-4, -4], [0, 2]])
assert(math.equal(
mult([[3, 0, 2], [2, 1, 0], [1, 4, 2]],
[[1/10, 2/5, -1/10], [-1/5, 1/5, 1/5], [7/20, -3/5, 3/20]]),
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]))
})
it('negates a vector', () => {
assert.deepStrictEqual(math.negate([-3, 4, -5]), [3, -4, 5])
})
it('subtracts vectors', () => {
assert.deepStrictEqual(math.subtract([-3, 4, -5], [8, 0, 2]), [-11, 4, -7])
})
it('computes the sum of a vector', () => {
const sum = math.sum
assert.strictEqual(sum([-3, 4, -5]), -4)
assert.deepStrictEqual(sum(cV), cplx(4, -3))
assert.strictEqual(sum([4, true, -2]), 3)
})
})

View file

@ -0,0 +1,47 @@
import assert from 'assert'
import math from '#nanomath'
const t = true
const f = false
describe('Vector relational functions', () => {
it('can test two vectors for deep equality', () => {
const dEq = math.deepEqual
const pyth = [3, 4, 5]
assert.strictEqual(dEq(pyth, [3, 4, 5]), t)
assert.strictEqual(dEq(pyth, [3, 4, 6]), f)
assert.strictEqual(dEq(pyth, [3, 4, [5]]), f)
assert.strictEqual(dEq(3, 3 + 1e-13), t)
assert.strictEqual(dEq(pyth, [3+1e-13, 4-1e-13, 5]), t)
assert.strictEqual(dEq([pyth, pyth], [pyth, [3, 4, 5]]), t)
assert.strictEqual(dEq([3], 3), f)
})
it('performs equal elementwise', () => {
const eq = math.equal
const pyth = [3, 4, 5]
assert.deepStrictEqual(eq(pyth, 4), [f, t, f])
assert.deepStrictEqual(eq(5, pyth), [f, f, t])
assert.deepStrictEqual(eq(pyth, pyth), [t, t, t])
assert.deepStrictEqual(eq(pyth, [3]), [t, f, f])
assert.deepStrictEqual(eq([4 + 1e-14], pyth), [f, t, f])
assert.deepStrictEqual(eq(pyth, [3, 4]), [t, t, f])
assert.deepStrictEqual(eq([3, 2], pyth), [t, f, f])
assert.deepStrictEqual(eq([pyth, pyth], [3, 4]), [[t, f, f], [f, t, f]])
assert.deepStrictEqual(
eq([pyth, pyth], [[5], pyth]), [[f, f, t], [t, t, t]])
assert.deepStrictEqual(
eq([[1, 2], [3, 4]], [[1, 3], [2, 4]]), [[t, f], [f, t]])
})
it('performs order comparisons elementwise', () => {
const pyth = [3, 4, 5]
const ans = [-1, 0, 1]
const powers = [2, 4, 8]
assert.deepStrictEqual(math.compare(pyth, [5, 4, 3]), ans)
assert.deepStrictEqual(math.sign([-Infinity, 0, 3]), ans)
assert.deepStrictEqual(math.unequal(pyth, [5, 4 + 1e-14, 5]), [t, f, f])
assert.deepStrictEqual(math.larger(pyth, powers), [t, f, f])
assert.deepStrictEqual(math.isPositive(ans), [f, f, t])
assert.deepStrictEqual(math.largerEq(pyth, powers), [t, t, f])
assert.deepStrictEqual(math.smaller(pyth, powers), [f, f, t])
assert.deepStrictEqual(math.smallerEq(pyth, powers), [f, t, t])
})
})

View file

@ -0,0 +1,45 @@
import assert from 'assert'
import {ReturnType, Unknown} from '#core/Type.js'
import math from '#nanomath'
describe('Vector type functions', () => {
it('can construct a vector', () => {
const vec = math.vector
const {BooleanT, NumberT, Vector} = math.types
assert.deepStrictEqual(vec(3, 4, 5), [3, 4, 5])
assert.strictEqual(
ReturnType(vec.resolve([NumberT, NumberT, NumberT])),
Vector(NumberT))
assert.deepStrictEqual(vec(3, true), [3, true])
assert.strictEqual(
ReturnType(vec.resolve([NumberT, BooleanT])),
Vector(Unknown))
})
it('can transpose vectors and matrices', () => {
const tsp = math.transpose
assert.deepStrictEqual(tsp([3, 4, 5]), [[3], [4], [5]])
assert.deepStrictEqual(tsp([[1, 2], [3, 4]]), [[1, 3], [2, 4]])
assert.deepStrictEqual(
tsp([[1, 2, 3], [4, 5, 6]]), [[1, 4], [2, 5], [3, 6]])
})
it('can take adjoint (conjugate transpose) of a matrix', () => {
const cx = math.complex
assert.deepStrictEqual(
math.adjoint([[cx(1, 1), cx(2, 2)], [cx(3, 3), cx(4, 4)]]),
[[cx(1, -1), cx(3, -3)], [cx(2, -2), cx(4, -4)]])
})
it('generates identity from an example matrix or a number of rows', () => {
const id = math.identity
const cx = math.complex
assert.deepStrictEqual(id(2), [[1, 0], [0, 1]])
assert.deepStrictEqual(id(cx(2)), [[cx(1), cx(0)], [cx(0), cx(1)]])
assert.deepStrictEqual(
id([[1, 2, 3]]),
[[1, 0 , 0], [0, 1, 0], [0, 0, 1]])
})
it('takes the determinant of a matrix', () => {
assert.strictEqual(
math.determinant([[6, 1, 1], [4, -2, 5], [2, 8, 7]]),
-306)
})
})

View file

@ -0,0 +1,41 @@
import assert from 'assert'
import math from '#nanomath'
describe('Vector utility functions', () => {
it('can clone a vector', () => {
const subj1 = [3, 4]
const clone1 = math.clone(subj1)
assert.deepStrictEqual(clone1, subj1)
assert(clone1 !== subj1)
const subj2 = [3, false]
const clone2 = math.clone(subj2)
assert.deepStrictEqual(clone2, subj2)
assert(clone2 !== subj2)
const subj3 = [[3, 4], [2, 5]]
const clone3 = math.clone(subj3)
assert.deepStrictEqual(clone3, subj3)
assert(clone3 !== subj3)
assert(clone3[0] !== subj3[0])
assert(clone3[1] !== subj3[1])
})
it('performs utilities elementwise', () => {
const cplx = math.complex
const sink = math.vector(
NaN, -Infinity, 7,
cplx(3, 4), cplx(3.5, -2.5), cplx(2.2), cplx(cplx(3, 4))
)
const t = true, f = false
assert.deepStrictEqual(math.isnan(sink), [t, f, f, f, f, f, f])
assert.deepStrictEqual(math.isfinite(sink), [f, f, t, t, t, t, t])
assert.deepStrictEqual(math.isInteger(sink), [f, f, t, t, f, f, t])
assert.deepStrictEqual(math.isReal(sink), [t, t, t, f, f, t, f])
assert.deepStrictEqual(math.nonImaginary(sink), [t, t, t, f, f, t, t])
assert.deepStrictEqual(math.re(sink), [NaN, -Infinity, 7, 3, 3.5, 2.2, 3])
assert.deepStrictEqual(
math.im(sink),
[0, 0, 0, cplx(0, 4), cplx(0, -2.5), cplx(0, 0), cplx(cplx(0, 4))])
})
})

7
src/vector/all.js Normal file
View file

@ -0,0 +1,7 @@
export * as typeDefinition from './Vector.js'
export * as arithmetic from './arithmetic.js'
export * as logical from './logical.js'
export * as relational from './relational.js'
export * as type from './type.js'
export * as utilities from './utils.js'

266
src/vector/arithmetic.js Normal file
View file

@ -0,0 +1,266 @@
import {
distributeFirst, distributeSecond, promoteBinary, promoteUnary
} from './helpers.js'
import {Vector} from './Vector.js'
import {Returns, ReturnType} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
import {ReturnsAs} from '#generic/helpers.js'
export const normsq = match(Vector, (math, V) => {
const compNormsq = math.normsq.resolve(V.Component)
const sum = math.sum.resolve(Vector(ReturnType(compNormsq)))
return ReturnsAs(sum, v => sum(v.map(compNormsq)))
})
// abs and norm differ only on Vector (and perhaps other collections) --
// norm computes overall by the generic formula, whereas abs distributes
// elementwise:
export const abs = promoteUnary('abs')
export const add = promoteBinary('add')
export const dotMultiply = promoteBinary('multiply')
export const invert = match(Vector, (math, V, strategy) => {
if (V.vectorDepth > 2) {
throw new TypeError(
'invert not implemented for arrays of dimension > 2')
}
const normsq = math.normsq.resolve(V, strategy)
const NormT = ReturnType(normsq)
const zNorm = math.zero(NormT)
const isRealZ = math.isZero.resolve(NormT, strategy)
if (V.vectorDepth === 1) {
const invNorm = math.invert.resolve(NormT, strategy)
const scalarMult = math.multiply.resolve(
[V, ReturnType(invNorm)], strategy)
return ReturnsAs(scalarMult, v => {
const nsq = normsq(v)
if (isRealZ(nsq)) return Array(v.length).fill(zNorm)
return scalarMult(v, invNorm(nsq))
})
}
// usual matrix situation, want to find a matrix whose product with v
// is the identity, or as close as we can get to that if the rank is
// deficient. We use the Moore-Penrose pseudoinverse.
const clone = math.clone.resolve(V, strategy)
const VComp = V.Component
const Elt = VComp.Component
const invElt = math.invert.resolve(Elt, strategy)
const det = math.determinant.resolve(V, strategy)
const neg = math.negate.resolve(Elt, strategy)
const multMM = math.multiply.resolve([V, V], strategy)
const multMS = math.multiply.resolve([V, ReturnType(invElt)], strategy)
const multVS = math.multiply.resolve([VComp, ReturnType(invElt)], strategy)
const multSS = math.multiply.resolve([Elt, Elt], strategy)
const sub = math.subtract.resolve([Elt, Elt], strategy)
const id = math.identity.resolve(V, strategy)
const abs = math.abs.resolve(Elt, strategy)
const gt = math.larger.resolve([NormT, NormT], strategy)
const isEltZ = math.isZero.resolve(Elt, strategy)
const zElt = math.zero(Elt)
const adj = math.adjoint.resolve(V, strategy)
const methods = {
invElt, det, isRealZ, neg, multMM, multMS, multVS, multSS,
id, abs, isEltZ, zElt, gt, sub, adj, clone
}
if (ReturnType(abs) !== NormT) {
throw new TypeError('type inconsistency in matrix invert')
}
return Returns(V, m => {
const rows = m.length
const cols = m[0].length
const nsq = normsq(m)
if (isRealZ(nsq)) { // all-zero matrix
const retval = []
for (let ix = 0; ix < cols; ++ix) {
retval.push(Array(rows).fill(zNorm))
}
return retval
}
if (rows == cols) {
// the inv helper will return falsy if not invertible
const retval = inv(m, rows, methods)
if (retval) return retval
}
return pinv(m, rows, cols, methods)
})
})
// Returns the inverse of a rows×rows matrix or false if not invertible
// Note: destroys m in the inversion process.
function inv(
origm,
rows,
{
invElt, det, isRealZ, neg, multMS, multVS, multSS,
id, abs, isEltZ, zElt, gt, sub, clone
}
) {
switch (rows) {
case 1: return [[invElt(origm[0][0])]]
case 2: {
const dt = det(origm)
if (isRealZ(dt)) return false
const divisor = invElt(dt)
const [[a, b], [c, d]] = origm
return multMS([[d, neg(b)], [neg(c), a]], divisor)
}
default: { // Gauss-Jordan elimination
const m = clone(origm)
const B = id(m)
const cols = rows
// Loop over columns, performing row reductions:
for (let c = 0; c < cols; ++c) {
// Pivot: Find row r that has the largest entry in column c, and
// swap row c and r:
let colMax = abs(m[c][c])
let rMax = c
for (let r = c + 1; r < rows; ++r) {
const mag = abs(m[r][c])
if (gt(mag, colMax)) {
colMax = mag
rMax = r
}
}
if (isRealZ(colMax)) return false
if (rMax !== c) {
[m[c], m[rMax], B[c], B[rMax]]
= [m[rMax], m[c], B[rMax], B[c]]
}
// Normalize the cth row:
const normalizer = invElt(m[c][c])
const mc = multVS(m[c], normalizer)
m[c] = mc
const Bc = multVS(B[c], normalizer)
B[c] = Bc
// Eliminate nonzero values on other rows at column c
for (let r = 0; r < rows; ++r) {
if (r === c) continue
const mr = m[r]
const Br = B[r]
const mrc = mr[c]
if (!isEltZ(mr[c])) {
// Subtract Arc times row c from row r to eliminate A[r][c]
mr[c] = zElt
for (let s = c + 1; s < cols; ++s) {
mr[s] = sub(mr[s], multSS(mrc, mc[s]))
}
for (let s = 0; s < cols; ++s) {
Br[s] = sub(Br[s], multSS(mrc, Bc[s]))
}
}
}
}
return B
}}
}
// Calculates Moore-Penrose pseudoinverse
// uses rank factorization per mathjs; SVD appears to be considered better
// but not worth the effort to implement for this prototype
function pinv(m, rows, cols, methods) {
const {C, F} = rankFactorization(m, rows, cols, methods)
const {multMM, adj} = methods
const Cstar = adj(C)
const Cpinv = multMM(inv(multMM(Cstar, C), Cstar.length, methods), Cstar)
const Fstar = adj(F)
const Fpinv = multMM(Fstar, inv(multMM(F, Fstar), F.length, methods))
return multMM(Fpinv, Cpinv)
}
// warning: destroys m in computing the row-reduced echelon form
// TODO: this code should be merged with inv to the extent possible. It's
// a very similar process.
function rref(origm, rows, cols, methods) {
const {isEltZ, invElt, multVS, zElt, sub, multSS, clone} = methods
const m = clone(origm)
let lead = -1
for (let r = 0; r < rows && ++lead < cols; ++r) {
if (cols <= lead) return m
let i = r
while (isEltZ(m[i][lead])) {
if (++i === rows) {
i = r
if (++lead === cols) return m
}
}
if (i !== r) [m[i], m[r]] = [m[r], m[i]]
let normalizer = invElt(m[r][lead])
const mr = multVS(m[r], normalizer)
m[r] = mr
for (let i = 0; i < rows; ++i) {
if (i === r) continue
const mi = m[i]
const toRemove = mi[lead]
mi[lead] = zElt
for (let j = lead + 1; j < cols; ++j) {
mi[j] = sub(mi[j], multSS(toRemove, mr[j]))
}
}
}
return m
}
function rankFactorization(m, rows, cols, methods) {
const RREF = rref(m, rows, cols, methods)
const {isEltZ} = methods
const rankRows = RREF.map(row => row.some(elt => !isEltZ(elt)))
const C = m.map(row => row.filter((_, j) => j < rows && rankRows[j]))
const F = RREF.filter((_, i) => rankRows[i])
return {C, F}
}
export const multiply = [
distributeFirst('multiply'),
distributeSecond('multiply'),
match([Vector, Vector], (math, [V, W], strategy) => {
const VComp = V.Component
if (W.vectorDepth === 1) {
if (V.vectorDepth === 1) {
const eltWise = math.dotMultiply.resolve([V, W], strategy)
const sum = math.sum.resolve(ReturnType(eltWise))
return ReturnsAs(sum, (v, w) => sum(eltWise(v, w)))
}
const compMult = math.multiply.resolve([VComp, W], strategy)
return ReturnsAs(
Vector(ReturnType(compMult)),
(v, w) => v.map(f => compMult(f, w)))
}
const transpose = math.transpose.resolve(W, strategy)
const wrapV = V.vectorDepth === 1
const RowV = wrapV ? V : VComp
const rowMult = math.multiply.resolve([RowV, W.Component], strategy)
let RetType = Vector(ReturnType(rowMult))
if (!wrapV) RetType = Vector(RetType)
return ReturnsAs(RetType, (v, w) => {
if (wrapV) v = [v]
w = transpose(w)
let retval = v.map(vrow => w.map(wcol => rowMult(vrow, wcol)))
if (wrapV) retval = retval[0]
return retval
})
})
]
export const negate = promoteUnary('negate')
export const subtract = promoteBinary('subtract')
export const sum = match(Vector, (math, V) => {
const add = math.add.resolve([V.Component, V.Component])
const haszero = math.haszero(V.Component)
const zero = haszero ? math.zero(V.Component) : undefined
return ReturnsAs(add, v => {
if (v.length === 0) {
if (haszero) return zero
throw new TypeError(`Can't sum empty ${V}: no zero element`)
}
let ix = 0
let retval = v[ix]
while (++ix < v.length) retval = add(retval, v[ix])
return retval
})
})

70
src/vector/helpers.js Normal file
View file

@ -0,0 +1,70 @@
import {Vector} from './Vector.js'
import {Returns, ReturnType, Undefined} from '#core/Type.js'
import {Any, match} from '#core/TypePatterns.js'
export const promoteUnary = name => match(Vector, (math, V, strategy) => {
const compOp = math.resolve(name, V.Component, strategy)
return Returns(Vector(ReturnType(compOp)), v => v.map(elt => compOp(elt)))
})
export const distributeFirst = name => match(
[Vector, Any],
(math, [V, E], strategy) => {
const compOp = math.resolve(name, [V.Component, E], strategy)
return Returns(
Vector(ReturnType(compOp)), (v, e) => v.map(f => compOp(f, e)))
})
export const distributeSecond = name => match(
[Any, Vector],
(math, [E, V], strategy) => {
const compOp = math.resolve(name, [E, V.Component], strategy)
return Returns(
Vector(ReturnType(compOp)), (e, v) => v.map(f => compOp(e, f)))
})
export const promoteBinary = name => [
distributeFirst(name),
distributeSecond(name),
match([Vector, Vector], (math, [V, W], strategy) => {
const VComp = V.Component
const WComp = W.Component
// special case: if the vector nesting depths do not match,
// we operate between the elements of the deeper one and the entire
// more shallow one:
if (V.vectorDepth > W.vectorDepth) {
const compOp = math.resolve(name, [VComp, W], strategy)
return Returns(
Vector(ReturnType(compOp)), (v, w) => v.map(f => compOp(f, w)))
}
if (V.vectorDepth < W.vectorDepth) {
const compOp = math.resolve(name, [V, WComp], strategy)
return Returns(
Vector(ReturnType(compOp)), (v, w) => w.map(f => compOp(v, f)))
}
const compOp = math.resolve(name, [VComp, WComp], strategy)
const opNoV = math.resolve(name, [Undefined, WComp], strategy)
const opNoW = math.resolve(name, [VComp, Undefined], strategy)
return Returns(
Vector(ReturnType(compOp)),
(v, w) => {
const vInc = Number(v.length > 1)
const wInc = Number(w.length >= v.length || w.length > 1)
const retval = []
let vIx = 0
let wIx = 0
while ((vInc && vIx < v.length)
|| (wInc && wIx < w.length)
) {
if (vIx >= v.length) {
retval.push(opNoV(undefined, w[wIx]))
} else if (wIx >= w.length) {
retval.push(opNoW(v[vIx], undefined))
} else retval.push(compOp(v[vIx], w[wIx]))
vIx += vInc
wIx += wInc
}
return retval
})
})
]

7
src/vector/logical.js Normal file
View file

@ -0,0 +1,7 @@
import {promoteBinary, promoteUnary} from './helpers.js'
export const not = promoteUnary('not')
export const and = promoteBinary('and')
export const or = promoteBinary('or')

95
src/vector/relational.js Normal file
View file

@ -0,0 +1,95 @@
import {Vector} from './Vector.js'
import {promoteBinary} from './helpers.js'
import {Unknown, Returns, ReturnType, Undefined} from '#core/Type.js'
import {Any, Optional, match} from '#core/TypePatterns.js'
import {BooleanT} from '#boolean/BooleanT.js'
export const deepEqual = [
match([Vector, Any], Returns(BooleanT, () => false)),
match([Any, Vector], Returns(BooleanT, () => false)),
match([Vector, Vector], (math, [V, W]) => {
const compDeep = math.deepEqual.resolve([V.Component, W.Component])
return Returns(BooleanT, (v,w) => v === w
|| (v.length === w.length
&& v.every((e, i) => compDeep(e, w[i]))))
})
]
export const indistinguishable = [
match([Vector, Any, Optional([Any, Any])], (math, [V, E, T]) => {
const VComp = V.Component
if (T.length === 0) { // no tolerances
const same = math.indistinguishable.resolve([VComp, E])
return Returns(
Vector(ReturnType(same)), (v, e) => v.map(f => same(f, e)))
}
const [[RT, AT]] = T
const same = math.indistinguishable.resolve([VComp, E, RT, AT])
return Returns(
Vector(ReturnType(same)),
(v, e, [[rT, aT]]) => v.map(f => same(f, e, rT, aT)))
}),
match([Any, Vector, Optional([Any, Any])], (math, [E, V, T]) => {
// reimplement to get other order in same so as not to assume
// same is symmetric, even though it probably is
const VComp = V.Component
if (T.length === 0) { // no tolerances
const same = math.indistinguishable.resolve([E, VComp])
return Returns(
Vector(ReturnType(same)), (e, v) => v.map(f => same(e, f)))
}
const [[RT, AT]] = T
const same = math.indistinguishable.resolve([E, VComp, RT, AT])
return Returns(
Vector(ReturnType(same)),
(e, v, [[rT, aT]]) => v.map(f => same(e, f, rT, aT)))
}),
match([Vector, Vector, Optional([Any, Any])], (math, [V, W, T]) => {
const VComp = V.Component
const WComp = W.Component
const inhomogeneous = VComp === Unknown || WComp === Unknown
let same
let sameNoV
let sameNoW
if (inhomogeneous) {
same = math.indistinguishable
sameNoV = same
sameNoW = same
} else if (T.length === 0) { // no tolerances
same = math.indistinguishable.resolve([VComp, WComp])
sameNoV = math.indistinguishable.resolve([Undefined, WComp])
sameNoW = math.indistinguishable.resolve([VComp, Undefined])
} else {
const [[RT, AT]] = T
same = math.indistinguishable.resolve([VComp, WComp, RT, AT])
sameNoV = math.indistinguishable.resolve([Undefined, WComp, RT, AT])
sameNoW = math.indistinguishable.resolve([VComp, Undefined, RT, AT])
}
return Returns(
inhomogeneous ? Vector(Unknown) : Vector(ReturnType(same)),
(v, w, [tol = [0, 0]]) => {
const [rT, aT] = tol
const vInc = Number(v.length > 1)
const wInc = Number(w.length >= v.length || w.length > 1)
const retval = []
let vIx = 0
let wIx = 0
while ((vInc && vIx < v.length)
|| (wInc && wIx < w.length)
) {
if (vIx >= v.length) {
retval.push(sameNoV(undefined, w[wIx], rT, aT))
} else if (wIx >= w.length) {
retval.push(sameNoW(v[vIx], undefined, rT, aT))
} else retval.push(same(v[vIx], w[wIx], rT, aT))
vIx += vInc
wIx += wInc
}
return retval
})
})
]
export const compare = promoteBinary('compare')
export const exceeds = promoteBinary('exceeds')

133
src/vector/type.js Normal file
View file

@ -0,0 +1,133 @@
import {Vector} from './Vector.js'
import {OneOf, Returns, ReturnType, Unknown} from '#core/Type.js'
import {Any, Multiple, match} from '#core/TypePatterns.js'
export const vector = match(Multiple(Any), (math, [TV]) => {
if (!TV.length) return Returns(Vector(Unknown), () => [])
let CompType = TV[0]
if (TV.some(T => T !== CompType)) CompType = Unknown
return Returns(Vector(CompType), v => v)
})
export const determinant = match(Vector(Vector), (math, M, strategy) => {
const Elt = M.Component.Component
const cloneElt = math.clone.resolve(Elt, strategy)
const mult = math.multiply.resolve([Elt, Elt], strategy)
const sub = math.subtract.resolve(
[ReturnType(mult), ReturnType(mult)], strategy)
const isZ = math.isZero.resolve(Elt, strategy)
const zElt = math.zero(Elt)
const clone = math.clone.resolve(M, strategy)
const div = math.divide.resolve([ReturnType(sub), Elt], strategy)
const neg = math.negate.resolve(Elt, strategy)
return Returns(OneOf(ReturnType(clone), ReturnType(sub), Elt), origm => {
const rows = origm.length
switch (rows) {
case 1: return cloneElt(origm[0][0])
case 2: {
const [[a, b], [c, d]] = origm
return sub(mult(a, d), mult(b, c))
}
default: { // Bareiss algorithm
const m = clone(origm)
let negated = false
const rowIndices = [...Array(rows).keys()] // track row indices
// because the algorithm may swap rows
for (let k = 0; k < rows; ++k) {
let k_ = rowIndices[k]
if (isZ(m[k_][k])) {
let _k = k
while (++_k < rows) {
if (!isZ(m[rowIndices[_k]][k])) {
k_ = rowIndices[_k]
rowIndices[_k] = rowIndices[k]
rowIndices[k] = k_
negated = !negated
break
}
}
if (_k === rows) return zElt
}
const piv = m[k_][k] // we now know nonzero
const piv_ = k === 0 ? 1 : m[rowIndices[k-1]][k-1]
for (let i = k + 1; i < rows; ++i) {
const i_ = rowIndices[i]
for (let j = k + 1; j < rows; ++j) {
m[i_][j] = div(
sub(mult(m[i_][j], piv), mult(m[i_][k], m[k_][j])),
piv_)
}
}
}
const det = m[rowIndices[rows - 1]][rows - 1]
return negated ? neg(det) : det
}}
})
})
function identitizer(cols, zero, one) {
const retval = []
for (let ix = 0; ix < cols; ++ix) {
const row = Array(cols).fill(zero)
row[ix] = one
retval.push(row)
}
return retval
}
export const identity = [
match(Any, (math, V) => {
const toNum = math.number.resolve(V)
const zero = math.zero(V)
const one = math.one(V)
return Returns(Vector(Vector(V)), n => identitizer(toNum(n), zero, one))
}),
match(Vector, (math, V) => {
switch (V.vectorDepth) {
case 1: {
const Elt = V.Component
const one = math.one(Elt)
return Returns(math.typeOf(one), () => one)
}
case 2: {
const Elt = V.Component.Component
const zero = math.zero(Elt)
const one = math.one(Elt)
return Returns(V, m => identitizer(
m.length ? m[0].length : 0, zero, one))
}
default:
throw new RangeError(
`'identity' not implemented on ${V.vectorDepth} dimensional arrays`)
}
})
]
// transposes a 2D matrix
function transposer(wrap, eltFun) {
return v => {
if (wrap) v = [v]
const cols = v.length ? v[0].length : 0
const retval = []
for (let ix = 0; ix < cols; ++ix) {
retval.push(v.map(row => eltFun(row[ix])))
}
return retval
}
}
export const transpose = match(Vector, (_math, V) => {
const wrapV = V.vectorDepth === 1
const Mat = wrapV ? Vector(V) : V
return Returns(Mat, transposer(wrapV, elt => elt))
})
// or with conjugation:
export const adjoint = match(Vector, (math, V, strategy) => {
const wrapV = V.vectorDepth === 1
const VComp = V.Component
const Elt = wrapV ? VComp : VComp.Component
const conj = math.conj.resolve(Elt, strategy)
const Mat = Vector(Vector(ReturnType(conj)))
return Returns(Mat, transposer(wrapV, conj))
})

10
src/vector/utils.js Normal file
View file

@ -0,0 +1,10 @@
import {promoteUnary} from './helpers.js'
export const clone = promoteUnary('clone')
export const isnan = promoteUnary('isnan')
export const isfinite = promoteUnary('isfinite')
export const isInteger = promoteUnary('isInteger')
export const isReal = promoteUnary('isReal')
export const nonImaginary = promoteUnary('nonImaginary')
export const re = promoteUnary('re')
export const im = promoteUnary('im')