feat: respect return typing strategy for all complex functions

This commit is contained in:
Glen Whitney 2025-04-26 22:47:07 -07:00
parent 87f5a0ed2a
commit c42249e561
5 changed files with 124 additions and 62 deletions

View file

@ -1,9 +1,12 @@
import assert from 'assert'
import math from '#nanomath'
import {Complex} from '../Complex.js'
import {ReturnTyping} from '#core/Type.js'
import {NumberT} from '#number/NumberT.js'
const cplx = math.complex
const CplxNum = Complex(NumberT)
const {full} = ReturnTyping
describe('complex arithmetic operations', () => {
it('computes absquare of complex numbers', () => {
@ -25,23 +28,33 @@ describe('complex arithmetic operations', () => {
assert.deepStrictEqual(add(cplx(z,z), 10), cplx(cplx(13, 4), z))
assert.deepStrictEqual(
add(cplx(z,z), cplx(10,20)), cplx(cplx(13, 4), cplx(23, 4)))
assert.strictEqual(add(z, math.conj(z)), 6)
const addFull = math.add.resolve([CplxNum, CplxNum], full)
assert.deepStrictEqual(addFull(z, math.conj(z)), cplx(6, 0))
})
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)))
const r = cplx(3, 0)
assert.strictEqual(conj(r), 3)
const conjFull = math.conj.resolve(CplxNum, full)
assert.deepStrictEqual(conjFull(r), cplx(3, -0))
})
it('divides complex numbers', () => {
const div = math.divide
const z = cplx(3, 4)
assert.deepStrictEqual(div(z, cplx(0, 1)), cplx(4, -3))
assert(math.equal(div(z, z), 1))
assert.strictEqual(div(z, z), 1)
const divFull = math.divide.resolve([CplxNum, CplxNum], full)
assert.deepStrictEqual(divFull(z, z), math.one(CplxNum))
// should probably have a quaternion example, but it's intricate
})
it('inverts complex numbers', () => {
const inv = math.invert
assert.deepStrictEqual(inv(cplx(0, 1)), cplx(0, -1))
assert.strictEqual(inv(cplx(2, 0)), 0.5)
assert.deepStrictEqual(inv(cplx(3, 4)), cplx(3/25, -4/25))
assert.deepStrictEqual(
inv(cplx(cplx(1, 2), cplx(4, 2))),
@ -51,8 +64,10 @@ describe('complex arithmetic operations', () => {
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)))
assert.strictEqual(mult(z, math.conj(z)), 25)
const multFull = math.multiply.resolve([CplxNum, CplxNum], full)
assert.deepStrictEqual(multFull(z, math.conj(z)), cplx(25, 0))
const q0 = cplx(cplx(1, 1), math.zero(CplxNum))
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)))
@ -67,8 +82,15 @@ describe('complex arithmetic operations', () => {
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)))
sub(cplx(z, z), cplx(cplx(0.5, 0.5), z)), cplx(2.5, 3.5))
assert.strictEqual(
sub(cplx(z, z), cplx(cplx(0, 4), z)), 3)
const subFull = math.subtract.resolve([
Complex(CplxNum), Complex(CplxNum)
], full)
assert.deepStrictEqual(
subFull(cplx(z, z), cplx(cplx(0.5, 0.5), z)),
cplx(cplx(2.5, 3.5), math.zero(CplxNum)))
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))

View file

@ -1,43 +1,46 @@
import {Complex} from './Complex.js'
import {promoteBinary, promoteUnary} from './helpers.js'
import {maybeComplex, promoteBinary, promoteUnary} from './helpers.js'
import {ResolutionError} from '#core/helpers.js'
import {ReturnTyping} from '#core/Type.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 {full} = ReturnTyping
export const absquare = match(Complex, (math, C, strategy) => {
const compAbsq = math.absquare.resolve(C.Component, full)
const R = compAbsq.returns
const add = math.add.resolve([R,R])
const add = math.add.resolve([R,R], strategy)
return ReturnsAs(add, z => add(compAbsq(z.re), compAbsq(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])
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)
return ReturnsAs(cplx, z => cplx(compConj(z.re), neg(z.im)))
})
export const divide = [
match([Complex, T => !T.complex], (math, [C, R]) => {
const div = math.divide.resolve([C.Component, R])
const cplx = math.complex.resolve([div.returns, div.returns])
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)
return ReturnsAs(cplx, (z, r) => cplx(div(z.re, r), div(z.im, r)))
}),
match([Complex, Complex], (math, [W, Z]) => {
const inv = math.invert.resolve(Z)
const mult = math.multiply.resolve([W, inv.returns])
match([Complex, Complex], (math, [W, Z], strategy) => {
const inv = math.invert.resolve(Z, full)
const mult = math.multiply.resolve([W, inv.returns], strategy)
return ReturnsAs(mult, (w, z) => mult(w, inv(z)))
})
]
export const invert = match(Complex, (math, C) => {
const conj = math.conj.resolve(C)
const norm = math.absquare.resolve(C)
const div = math.divide.resolve([C.Component, norm.returns])
const cplx = math.complex.resolve([div.returns, div.returns])
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)
return ReturnsAs(cplx, z => {
const c = conj(z)
const d = norm(z)
@ -47,17 +50,17 @@ export const invert = match(Complex, (math, C) => {
// 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)
export const multiply = match([Complex, Complex], (math, [W, Z], strategy) => {
const conj = math.conj.resolve(W.Component, full)
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])
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)
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))

View file

@ -1,27 +1,38 @@
import {Complex} from './Complex.js'
import {OneOf, Returns, ReturnTyping} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
import {ReturnsAs} from '#generic/helpers.js'
export const promoteUnary = name => match(
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)
return ReturnsAs(prune, (r, m) => prune(cplx(r, m)))
}
export const promoteUnary = (name, overrideStrategy) => match(
Complex,
(math, C) => {
const compOp = math.resolve(name, C.Component)
const cplx = math.complex.resolve([compOp.returns, compOp.returns])
(math, C, strategy) => {
const compOp = math.resolve(name, C.Component, full)
if (overrideStrategy) strategy = overrideStrategy
const cplx = maybeComplex(math, strategy, compOp.returns, compOp.returns)
return ReturnsAs(cplx, z => cplx(compOp(z.re), compOp(z.im)))
})
export const promotePredicateAnd = name => match(
Complex,
(math, C) => {
const compPred = math.resolve(name, C.Component)
(math, C, strategy) => {
const compPred = math.resolve(name, C.Component, strategy)
return ReturnsAs(compPred, z => compPred(z.re) && compPred(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])
(math, [W, Z], strategy) => {
const compOp = math.resolve(name, [W.Component, Z.Component], full)
const cplx = maybeComplex(math, strategy, compOp.returns, compOp.returns)
return ReturnsAs(
cplx, (w, z) => cplx(compOp(w.re, z.re), compOp(w.im, z.im)))
})

View file

@ -4,6 +4,8 @@ import {Any, match} from "#core/TypePatterns.js"
import {BooleanT} from '#boolean/BooleanT.js'
import {NumberT} from '#number/NumberT.js'
const {free, full} = ReturnTyping
export const complex = [
match(Any, (math, T) => {
const z = math.zero(T)
@ -31,30 +33,32 @@ 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(
export const associate = match(
[Complex, Complex],
(math, [W, Z], strategy) => {
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))
}
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 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 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))
})
})
})
const _cis = t => ({re: Math.cos(t), im: Math.sin(t)})
export const cis = match(NumberT, (math, _type, strategy) => {
if (strategy === ReturnTyping.free) {
if (strategy === free) {
const intTest = math.isInteger.resolve(NumberT)
return Returns(OneOf(NumberT, Complex(NumberT)), t => {
let halfCycles = t / Math.PI
@ -67,3 +71,21 @@ export const cis = match(NumberT, (math, _type, strategy) => {
}
return Returns(Complex(NumberT), _cis)
})
// Returns the real part if the complex number supplied has no imaginary
// part, otherwise leaves that complex number alone.
// Since it is explicitly asking for varying type, it ignores strategy.
export const pruneImaginary = match(Complex, (math, C) => {
const outcomes = [C]
let T = C
while (T.complex) {
T = T.Component
outcomes.push(T)
}
const real = math.isReal.resolve(C, full)
if (C.Component.complex) {
const compPrune = math.pruneImaginary.resolve(C.Component)
return Returns(OneOf(...outcomes), z => real(z) ? compPrune(z.re) : z)
}
return Returns(OneOf(...outcomes), z => real(z) ? z.re : z)
})

View file

@ -1,9 +1,13 @@
import {Complex} from './Complex.js'
import {match} from '#core/TypePatterns.js'
import {ReturnsAs} from '#generic/helpers.js'
import {promotePredicateAnd, promoteUnary} from './helpers.js'
export const clone = promoteUnary('clone')
import {ReturnTyping} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
import {ReturnsAs} from '#generic/helpers.js'
const {full} = ReturnTyping
export const clone = promoteUnary('clone', full)
export const isnan = promotePredicateAnd('isnan')
export const isfinite = promotePredicateAnd('isfinite')
// Note: the followig predicate returns true for all Gaussian integers, not
@ -12,6 +16,6 @@ export const isInteger = promotePredicateAnd('isInteger')
export const isReal = match(Complex, (math, C) => {
const eq = math.equal.resolve([C.Component, C.Component])
const add = math.add.resolve([C.Component, C.Component])
const add = math.add.resolve([C.Component, C.Component], full)
return ReturnsAs(eq, z => eq(z.re, add(z.re, z.im)))
})