feat: Make cis(theta) respect return typing strategy

To accomplish this, also
  * implements `OneOf` union types
  * passes desired return typing strategy to behavior factory functions
  * adds formerly missing BooleanT utils
  * adds isInteger utility function, along with its dependency isfinite
  * adds several new and formerly missing Complex utils

  In addition, it adds several Complex tests, including careful checking
  of the behavior and return type of `math.cis` under different
  `math.config.returnTyping` settings.
This commit is contained in:
Glen Whitney 2025-04-26 18:40:09 -07:00
parent 47370cec9e
commit 722a2724a0
11 changed files with 156 additions and 27 deletions

View file

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

7
src/boolean/utils.js Normal file
View file

@ -0,0 +1,7 @@
import {BooleanT} from './BooleanT.js'
import {Returns} from '#core/Type.js'
import {match} from '#core/TypePatterns.js'
export const clone = match(BooleanT, Returns(BooleanT, p => p))
export const isnan = match(BooleanT, Returns(BooleanT, () => false))
export const isfinite = match(BooleanT, Returns(BooleanT, () => true))

View file

@ -1,5 +1,9 @@
import assert from 'assert'
import math from '#nanomath'
import {Complex} from '../Complex.js'
import {OneOf, ReturnTyping} from '#core/Type.js'
import {NumberT} from '#number/NumberT.js'
const cplx = math.complex
@ -33,7 +37,20 @@ describe('complex type operations', () => {
assert(!assoc(cplx(0, 1), b))
})
it('computes cis of an angle', () => {
assert(math.equal(math.cis(0), 1))
assert(math.equal(math.cis(Math.PI/3), cplx(0.5, Math.sqrt(3)/2)))
const cis = math.cis.resolve(NumberT)
assert.strictEqual(cis.returns, 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))
const one = ccis(0)
assert(one !== 1)
assert(math.equal(one, 1))
assert(math.equal(ccis(Math.PI), cplx(-1)))
math.config.returnTyping = ReturnTyping.free
assert.strictEqual(math.cis.resolve(NumberT), cis)
assert.strictEqual(math.cis(2*Math.PI), 1)
})
})

View file

@ -0,0 +1,40 @@
import assert from 'assert'
import math from '#nanomath'
const cplx = math.complex
describe('complex utilities', () => {
it('clones a complex', () => {
const z = cplx(3, 4)
const cz = math.clone(z)
assert(cz !== z)
assert.deepStrictEqual(z, cz)
const q = cplx(z, math.add(z, 1))
const cq = math.clone(q)
assert(cq !== q)
assert.deepStrictEqual(q, cq)
assert(q.re !== cq.re && q.im !== cq.im)
})
it('checks for nan', () => {
assert(math.isnan(cplx(NaN, NaN)))
assert(!math.isnan(cplx(NaN, 6.28)))
})
it('tests for finiteness', () => {
const fin = math.isfinite
assert(fin(cplx(3, 4)))
assert(!fin(cplx(2, Infinity)))
assert(!fin(cplx(NaN, 0)))
})
it('identifies Gaussian integers', () => {
const isInt = math.isInteger
assert(isInt(cplx(3, 4)))
assert(isInt(cplx(-37,0)))
assert(!isInt(cplx(99, -1.000001)))
})
it('identifies real numbers', () => {
const {isReal} = math
assert(isReal(cplx(5, 0)))
assert(isReal(cplx(5, -1e-17)))
assert(!isReal(cplx(5, 0.000001)))
})
})

View file

@ -10,6 +10,13 @@ export const promoteUnary = name => match(
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)
return ReturnsAs(compPred, z => compPred(z.re) && compPred(z.im))
})
export const promoteBinary = name => match(
[Complex, Complex],
(math, [W, Z]) => {

View file

@ -1,5 +1,5 @@
import {Complex} from './Complex.js'
import {Returns} from "#core/Type.js"
import {OneOf, Returns, ReturnTyping} from "#core/Type.js"
import {Any, match} from "#core/TypePatterns.js"
import {BooleanT} from '#boolean/BooleanT.js'
import {NumberT} from '#number/NumberT.js'
@ -28,28 +28,42 @@ export const complex = [
]
export const arg = match(
Complex(NumberT), Returns(NumberT, z => Math.atan2(z.im, z.re)))
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))
})
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))
})
})
export const cis = match(NumberT, Returns(Complex(NumberT), t => ({
re: Math.cos(t), im: Math.sin(t)})))
const _cis = t => ({re: Math.cos(t), im: Math.sin(t)})
export const cis = match(NumberT, (math, _type, strategy) => {
if (strategy === ReturnTyping.free) {
const intTest = math.isInteger.resolve(NumberT)
return Returns(OneOf(NumberT, Complex(NumberT)), t => {
let halfCycles = t / Math.PI
if (intTest(halfCycles)) {
halfCycles = Math.round(halfCycles)
return halfCycles % 2 ? -1 : 1
}
return _cis(t)
})
}
return Returns(Complex(NumberT), _cis)
})

View file

@ -1,9 +1,17 @@
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')
export const isnan = promotePredicateAnd('isnan')
export const isfinite = promotePredicateAnd('isfinite')
// Note: the followig predicate returns true for all Gaussian integers, not
// just so-called rational integers.
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])
return ReturnsAs(eq, z => eq(z.re, add(z.re, z.im)))
const eq = math.equal.resolve([C.Component, C.Component])
const add = math.add.resolve([C.Component, C.Component])
return ReturnsAs(eq, z => eq(z.re, add(z.re, z.im)))
})

View file

@ -69,6 +69,27 @@ export const TypeOfTypes = new Type(t => t instanceof Type)
export const NotAType = new Type(() => true) // Danger, do not merge!
NotAType._doNotMerge = true
const unionDirectory = new ArrayKeyedMap() // make sure only one of each union
export const OneOf = (...types) => {
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 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(
t => typeList.some(T => T.test(t)),
{typeName: typeList.join('|')}))
}
return unionDirectory.get(typeList)
}
export const Returns = (type, f) => (f.returns = type, f)
export const whichType = typs => Returns(TypeOfTypes, item => {

View file

@ -360,7 +360,8 @@ export class TypeDispatcher {
try {
theBehavior = item(
DependencyRecorder(this, '', this, []),
matched(template, this))
matched(template, this),
strategy)
} catch (e) {
e.message = `Error in factory for ${key} on ${types} `
+ `with return typing ${ReturnTyping.name(strategy)} `

View file

@ -10,4 +10,11 @@ describe('number utilities', () => {
assert.strictEqual(math.isnan(Infinity), false)
assert.strictEqual(math.isnan(43), false)
})
it('tests if a number is an integer', () => {
assert(math.isInteger(7))
assert(math.isInteger(7+5e-16))
assert(!math.isInteger(7.000001))
assert(!math.isInteger(-Infinity))
assert(!math.isInteger(NaN))
})
})

View file

@ -4,4 +4,10 @@ import {NumberT} from './NumberT.js'
import {match} from '#core/TypePatterns.js'
export const clone = plain(a => a)
export const isfinite = match(NumberT, boolnum(isFinite))
export const isInteger = match(NumberT, math => {
const finite = math.isfinite.resolve(NumberT)
const eq = math.equal.resolve([NumberT, NumberT])
return boolnum(a => finite(a) && eq(a, Math.round(a)))(math)
})
export const isnan = match(NumberT, boolnum(isNaN))