From c42249e5619574d7916eac0c77efb6d3ee1599fd Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 26 Apr 2025 22:47:07 -0700 Subject: [PATCH] feat: respect return typing strategy for all complex functions --- src/complex/__test__/arithmetic.spec.js | 32 +++++++++++--- src/complex/arithmetic.js | 55 ++++++++++++----------- src/complex/helpers.js | 29 +++++++++---- src/complex/type.js | 58 +++++++++++++++++-------- src/complex/utils.js | 12 +++-- 5 files changed, 124 insertions(+), 62 deletions(-) diff --git a/src/complex/__test__/arithmetic.spec.js b/src/complex/__test__/arithmetic.spec.js index 966843b..536277a 100644 --- a/src/complex/__test__/arithmetic.spec.js +++ b/src/complex/__test__/arithmetic.spec.js @@ -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)) diff --git a/src/complex/arithmetic.js b/src/complex/arithmetic.js index d50b993..9b72697 100644 --- a/src/complex/arithmetic.js +++ b/src/complex/arithmetic.js @@ -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)) diff --git a/src/complex/helpers.js b/src/complex/helpers.js index 5397188..2f3a6ec 100644 --- a/src/complex/helpers.js +++ b/src/complex/helpers.js @@ -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))) }) diff --git a/src/complex/type.js b/src/complex/type.js index e626907..6df2bec 100644 --- a/src/complex/type.js +++ b/src/complex/type.js @@ -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) +}) diff --git a/src/complex/utils.js b/src/complex/utils.js index 77d1acc..0aae100 100644 --- a/src/complex/utils.js +++ b/src/complex/utils.js @@ -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))) })