feat: more Complex methods
All checks were successful
/ test (pull_request) Successful in 16s

* Adds associate, conj, multiply, negate, subtract, indistinguishable
  * As a result equal is now supported
  * Adds a check for recursive loops in resolve (a key/signature method
    depending on itself
This commit is contained in:
Glen Whitney 2025-04-24 20:13:35 -07:00
parent 8da23a84be
commit 7daa621571
12 changed files with 182 additions and 21 deletions

View file

@ -1,6 +1,7 @@
import {Type} from '#core/Type.js'
export const BooleanT = new Type(n => typeof n === 'boolean', {
typeName: 'BooleanT',
zero: false,
one: true
})

View file

@ -1,5 +1,7 @@
import assert from 'assert'
import math from '#nanomath'
import {Complex} from '../Complex.js'
import {NumberT} from '#number/NumberT.js'
const cplx = math.complex
@ -11,16 +13,51 @@ describe('complex arithmetic operations', () => {
})
it('adds complex numbers', () => {
const z = cplx(3, 4)
assert.deepStrictEqual(math.add(z, cplx(-1, 1)), cplx(2, 5))
assert.deepStrictEqual(math.add(z, cplx(true, false)), cplx(4, 4))
assert.deepStrictEqual(math.add(cplx(false, true), z), cplx(3, 5))
const add = math.add
assert.deepStrictEqual(add(z, cplx(-1, 1)), cplx(2, 5))
assert.deepStrictEqual(add(z, cplx(true, false)), cplx(4, 4))
assert.deepStrictEqual(add(cplx(false, true), z), cplx(3, 5))
assert.deepStrictEqual(
math.add(cplx(z, z), cplx(cplx(0.5, 0.5), z)),
add(cplx(z, z), cplx(cplx(0.5, 0.5), z)),
cplx(cplx(3.5, 4.5), cplx(6, 8)))
assert.deepStrictEqual(math.add(z, 5), cplx(8, 4))
assert.deepStrictEqual(math.add(true, z), cplx(4, 4))
assert.deepStrictEqual(math.add(cplx(z,z), 10), cplx(cplx(13, 4), z))
assert.deepStrictEqual(add(z, 5), cplx(8, 4))
assert.deepStrictEqual(add(true, z), cplx(4, 4))
assert.deepStrictEqual(add(cplx(z,z), 10), cplx(cplx(13, 4), z))
assert.deepStrictEqual(
math.add(cplx(z,z), cplx(10,20)), cplx(cplx(13, 4), cplx(23, 4)))
add(cplx(z,z), cplx(10,20)), cplx(cplx(13, 4), cplx(23, 4)))
})
it('conjugates complex numbers', () => {
const conj = math.conj
const z = cplx(3, 4)
assert.deepStrictEqual(conj(z), cplx(3, -4))
assert.deepStrictEqual(conj(cplx(z,z)), cplx(cplx(3, -4), cplx(-3, -4)))
})
it('multiplies complex numbers', () => {
const mult = math.multiply
const z = cplx(3, 4)
assert.deepStrictEqual(mult(z, z), cplx(-7, 24))
assert(math.equal(mult(z, math.conj(z)), 25))
const q0 = cplx(cplx(1, 1), math.zero(Complex(NumberT)))
const q1 = cplx(cplx(1, 0.5), cplx(0.5, 0.75))
assert.deepStrictEqual(
mult(q0, q1), cplx(cplx(0.5, 1.5), cplx(1.25, 0.25)))
assert.deepStrictEqual(
mult(q0, cplx(cplx(2, 0.1), cplx(1, 0.1))),
cplx(cplx(1.9, 2.1), cplx(1.1, -0.9)))
})
it('subtracts complex numbers', () => {
const z = cplx(3, 4)
const sub = math.subtract
assert.deepStrictEqual(sub(z, cplx(-1, 1)), cplx(4, 3))
assert.deepStrictEqual(sub(z, cplx(true, false)), cplx(2, 4))
assert.deepStrictEqual(sub(cplx(false, true), z), cplx(-3, -3))
assert.deepStrictEqual(
sub(cplx(z, z), cplx(cplx(0.5, 0.5), z)),
cplx(cplx(2.5, 3.5), cplx(0, 0)))
assert.deepStrictEqual(sub(z, 5), cplx(-2, 4))
assert.deepStrictEqual(sub(true, z), cplx(-2, -4))
assert.deepStrictEqual(sub(cplx(z,z), 10), cplx(cplx(-7, 4), z))
assert.deepStrictEqual(
sub(cplx(z,z), cplx(10,20)), cplx(cplx(-7, 4), cplx(-17, 4)))
})
})

View file

@ -15,4 +15,21 @@ describe('complex type operations', () => {
assert.strictEqual(math.arg(cplx(1, Math.sqrt(3))), Math.PI/3)
assert.strictEqual(math.arg(cplx(true, true)), Math.PI/4)
})
it('detects associates of a complex number', () => {
const z = cplx(3, 4)
const assoc = math.associate
assert(assoc(z, z))
assert(assoc(z, cplx(-3, -4)))
assert(assoc(z, cplx(-4, 3)))
assert(assoc(z, cplx(4, -3)))
assert(!assoc(z, math.conj(z)))
const b = cplx(true, true)
assert(assoc(b, cplx(-1, 1)))
assert(assoc(cplx(1, 1), b))
assert(assoc(cplx(-1, -1), b))
assert(assoc(cplx(1, -1), b))
assert(assoc(cplx(-1, 1), b))
assert(!assoc(b, cplx(false, true)))
assert(!assoc(cplx(0, 1), b))
})
})

View file

@ -1,3 +1,4 @@
export * as typeDefinition from './Complex.js'
export * as arithmetic from './arithmetic.js'
export * as relational from './relational.js'
export * as type from './type.js'

View file

@ -1,17 +1,44 @@
import {Complex} from './Complex.js'
import {promoteBinary, promoteUnary} from './helpers.js'
import {ResolutionError} from '#core/helpers.js'
import {match} from '#core/TypePatterns.js'
import {ReturnsAs} from '#generic/helpers.js'
export const absquare = match(Complex, (math, C) => {
const compAbsq = math.absquare.resolve([C.Component])
const R = compAbsq.returns
const add = math.add.resolve([R,R])
return ReturnsAs(add, z => add(compAbsq(z.re), compAbsq(z.im)))
const compAbsq = math.absquare.resolve([C.Component])
const R = compAbsq.returns
const add = math.add.resolve([R,R])
return ReturnsAs(add, z => add(compAbsq(z.re), compAbsq(z.im)))
})
export const add = match([Complex, Complex], (math, [C, D]) => {
const addComps = math.add.resolve([C.Component, D.Component])
const cplx = math.complex.resolve([addComps.returns, addComps.returns])
return ReturnsAs(
cplx, (w,z) => cplx(addComps(w.re, z.re), addComps(w.im, z.im)))
export const add = promoteBinary('add')
export const conj = match(Complex, (math, C) => {
const neg = math.negate.resolve(C.Component)
const compConj = math.conj.resolve(C.Component)
const cplx = math.complex.resolve([compConj.returns, neg.returns])
return ReturnsAs(cplx, z => cplx(compConj(z.re), neg(z.im)))
})
// We want this to work for complex numbers, quaternions, octonions, etc
// See https://math.ucr.edu/home/baez/octonions/node5.html
export const multiply = match([Complex, Complex], (math, [W, Z]) => {
const conj = math.conj.resolve(W.Component)
if (conj.returns !== W.Component) {
throw new ResolutionError(
`conjugation on ${W.Component} returns other type (${conj.returns})`)
}
const mWZ = math.multiply.resolve([W.Component, Z.Component])
const mZW = math.multiply.resolve([Z.Component, W.Component])
const sub = math.subtract.resolve([mWZ.returns, mZW.returns])
const add = math.add.resolve([mWZ.returns, mZW.returns])
const cplx = math.complex.resolve([sub.returns, add.returns])
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))
return cplx(real, imag)
})
})
export const negate = promoteUnary('negate')
export const subtract = promoteBinary('subtract')

20
src/complex/helpers.js Normal file
View file

@ -0,0 +1,20 @@
import {Complex} from './Complex.js'
import {match} from '#core/TypePatterns.js'
import {ReturnsAs} from '#generic/helpers.js'
export const promoteUnary = name => match(
Complex,
(math, C) => {
const compOp = math.resolve(name, C.Component)
const cplx = math.complex.resolve([compOp.returns, compOp.returns])
return ReturnsAs(cplx, z => cplx(compOp(z.re), compOp(z.im)))
})
export const promoteBinary = name => match(
[Complex, Complex],
(math, [W, Z]) => {
const compOp = math.resolve(name, [W.Component, Z.Component])
const cplx = math.complex.resolve([compOp.returns, compOp.returns])
return ReturnsAs(
cplx, (w, z) => cplx(compOp(w.re, z.re), compOp(w.im, z.im)))
})

24
src/complex/relational.js Normal file
View file

@ -0,0 +1,24 @@
import {Complex} from './Complex.js'
import {BooleanT} from '#boolean/BooleanT.js'
import {Returns} from '#core/Type.js'
import {match, Any, Optional} from '#core/TypePatterns.js'
export const indistinguishable = match(
[Complex, Complex, Optional([Any, Any])],
(math, [W, Z, T]) => {
let WComp = W.Component
let ZComp = Z.Component
if (T.length === 0) { // no tolerances
const same = math.indistinguishable.resolve([WComp, ZComp])
return Returns(
BooleanT, (w, z) => same(w.re, z.re) && same(w.im, z.im))
}
const [RT, AT] = T
const same = math.indistinguishable.resolve([WComp, ZComp, RT, AT])
return Returns(
BooleanT,
(w, z, [rT, aT]) => {
return same(w.re, z.re, rT, aT) && same(w.im, z.im, rT, aT)
})
})

View file

@ -1,6 +1,7 @@
import {Complex} from './Complex.js'
import {Returns} from "#core/Type.js"
import {Any, match} from "#core/TypePatterns.js"
import {BooleanT} from '#boolean/BooleanT.js'
import {NumberT} from '#number/NumberT.js'
export const complex = [
@ -28,3 +29,24 @@ export const complex = [
export const arg = match(
Complex(NumberT), Returns(NumberT, z => Math.atan2(z.im, z.re)))
/* Returns true if w is z multiplied by a complex unit */
export const associate = match([Complex, Complex], (math, [W, Z]) => {
if (Z.Component.complex) {
throw new Error(
`The group of units of type ${Z} is not yet implemented`)
}
const eq = math.equal.resolve([W, Z])
const neg = math.negate.resolve(Z)
const eqN = math.equal.resolve([W, neg.returns])
const mult = math.multiply.resolve([Z, Z])
const eqM = math.equal.resolve([W, mult.returns])
const negM = math.negate.resolve(mult.returns)
const eqNM = math.equal.resolve([W, negM.returns])
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
const iz = mult(iZ, z)
return eqM(w, iz) || eqNM(w, negM(iz))
})
})

View file

@ -8,6 +8,8 @@ import {
matched, needsCollection, Passthru, Matcher, match
} from './TypePatterns.js'
const underResolution = Symbol('underResolution')
export class TypeDispatcher {
constructor(...specs) {
this._implementations = {} // maps key to list of [pattern, result] pairs
@ -261,6 +263,7 @@ export class TypeDispatcher {
if (!(key in this)) {
throw new ReferenceError(`no method or value for key '${key}'`)
}
if (!Array.isArray(types)) types = [types]
const generatingDeps = this.resolve._genDepsOf?.length
if (generatingDeps) this._addToDeps(key, types)
@ -269,6 +272,10 @@ export class TypeDispatcher {
// Return the cached resolution if it's there
if (behave.has(types)) {
const result = behave.get(types)
if (result === underResolution) {
throw new ResolutionError(
`recursive resolution of ${key} on ${types}`)
}
if (generatingDeps
&& typeof result === 'object'
&& !(result instanceof Type)
@ -328,6 +335,7 @@ export class TypeDispatcher {
let theBehavior = () => undefined
let finalBehavior
this.resolve._genDepsOf.push([key, types])
behave.set(types, underResolution)
try { // Used to make sure not to return without popping _genDepsOf
if (!('returns' in item)) {
// looks like a factory

View file

@ -1,6 +1,7 @@
import {Returns} from '#core/Type.js'
import {match, Any} from '#core/TypePatterns.js'
export const conj = match(Any, (_math, T) => Returns(T, a => a))
export const square = match(Any, (math, T) => {
const mult = math.multiply.resolve([T, T])
return Returns(mult.returns, a => mult(a, a))

View file

@ -10,9 +10,11 @@ import {TypeDispatcher} from '#core/TypeDispatcher.js'
// the following list will be tried earlier. (The rationale for that
// ordering is that any time you merge something, it should supersede
// whatever has been merged before.)
// Hence, in building the math instance, we put complex first 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(complex, generics, booleans, coretypes, numbers)
// 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_
// non-complex type to complex, potentially making a poor overload choice)
// to be tried last.
const math = new TypeDispatcher(generics, complex, booleans, coretypes, numbers)
export default math

View file

@ -4,6 +4,7 @@ import {BooleanT} from '#boolean/BooleanT.js'
export const NumberT = new Type(n => typeof n === 'number', {
from: match(BooleanT, math => math.number.resolve([BooleanT])),
typeName: 'NumberT', // since used before ever put in a TypeDispatcher
one: 1,
zero: 0,
nan: NaN