feat: Implement complex arithmetic through sqrt

Together with any auxiliary functions needed for that goal. Also
  strives to ensure the same functions are being defined for
  number and for `Complex<T>`.
This commit is contained in:
Glen Whitney 2022-12-22 00:14:58 -05:00
parent d55776655f
commit fbec410c42
10 changed files with 320 additions and 18 deletions

139
src/Complex/arithmetic.ts Normal file
View File

@ -0,0 +1,139 @@
import {Complex, UnderlyingReal, complex_binary} from './type.js'
import {
BBinary, Dependency, ConservativeUnary, ConservativeBinary, ImpType
} from '../core/Dispatcher.js'
declare module "./type" {
interface ComplexReturn<Params> {
add: ConservativeBinary<Params, Complex<any>>
addReal: Params extends [infer Z, infer R]
? [R] extends [UnderlyingReal<Z>] ? Z : never
: never
unaryMinus: ConservativeUnary<Params, Complex<any>>
conj: ConservativeUnary<Params, Complex<any>>
subtract: ConservativeBinary<Params, Complex<any>>
multiply: ConservativeBinary<Params, Complex<any>>
absquare: Params extends [infer Z]
? Z extends Complex<any> ? UnderlyingReal<Z> : never
: never
reciprocal: ConservativeUnary<Params, Complex<any>>
divide: ConservativeBinary<Params, Complex<any>>
divideByReal: Params extends [infer Z, infer R]
? [R] extends [UnderlyingReal<Z>] ? Z : never
: never
// square root that remains the same type
conservativeSqrt: ConservativeUnary<Params, Complex<any>>
// Same as conservativeSqrt for complex numbers:
sqrt: ConservativeUnary<Params, Complex<any>>
// complex square root of the real type of a complex:
complexSqrt: Params extends [infer T] ? Complex<T> : never
}
}
export const add =
<T>(dep: Dependency<'add', [T,T]>):
ImpType<'add', [Complex<T>, Complex<T>]> =>
(w, z) => complex_binary(dep.add(w.re, z.re), dep.add(w.im, z.im))
export const addReal =
<T>(dep: Dependency<'addReal', [T, UnderlyingReal<T>]>):
ImpType<'addReal', [Complex<T>, UnderlyingReal<T>]> =>
(z, r) => complex_binary(dep.addReal(z.re, r), z.im)
export const unaryMinus =
<T>(dep: Dependency<'unaryMinus', [T]>):
ImpType<'unaryMinus', [Complex<T>]> =>
z => complex_binary(dep.unaryMinus(z.re), dep.unaryMinus(z.im))
export const conj =
<T>(dep: Dependency<'unaryMinus'|'conj', [T]>):
ImpType<'conj', [Complex<T>]> =>
z => complex_binary(dep.conj(z.re), dep.unaryMinus(z.im))
export const subtract =
<T>(dep: Dependency<'subtract', [T,T]>):
ImpType<'subtract', [Complex<T>, Complex<T>]> =>
(w, z) => complex_binary(dep.subtract(w.re, z.re), dep.subtract(w.im, z.im))
export const multiply =
<T>(dep: Dependency<'add', [T,T]>
& Dependency<'subtract', [T,T]>
& Dependency<'multiply', [T,T]>
& Dependency<'conj', [T]>):
ImpType<'multiply', [Complex<T>, Complex<T>]> =>
(w, z) => {
const mult = dep.multiply
const realpart = dep.subtract(
mult( w.re, z.re), mult(dep.conj(w.im), z.im))
const imagpart = dep.add(
mult(dep.conj(w.re), z.im), mult( w.im, z.re))
return complex_binary(realpart, imagpart)
}
export const absquare =
<T>(dep: Dependency<'absquare', [T]>
& Dependency<'add', BBinary<UnderlyingReal<T>>>):
ImpType<'absquare', [Complex<T>]> =>
z => dep.add(dep.absquare(z.re), dep.absquare(z.im))
export const divideByReal =
<T>(dep: Dependency<'divideByReal', [T, UnderlyingReal<T>]>):
ImpType<'divideByReal', [Complex<T>, UnderlyingReal<T>]> =>
(z, r) => complex_binary(
dep.divideByReal(z.re, r), dep.divideByReal(z.im, r))
export const reciprocal =
<T>(dep: Dependency<'conj', [Complex<T>]>
& Dependency<'absquare', [Complex<T>]>
& Dependency<'divideByReal', [Complex<T>, UnderlyingReal<T>]>):
ImpType<'reciprocal', [Complex<T>]> =>
z => dep.divideByReal(dep.conj(z), dep.absquare(z))
export const divide =
<T>(dep: Dependency<'multiply', [Complex<T>, Complex<T>]>
& Dependency<'reciprocal', [Complex<T>]>):
ImpType<'divide', [Complex<T>, Complex<T>]> =>
(w, z) => dep.multiply(w, dep.reciprocal(z))
export const complexSqrt =
<T>(dep: Dependency<'conservativeSqrt', [T]>
& Dependency<'isSquare', [T]>
& Dependency<'complex', [T]>
& Dependency<'unaryMinus', [T]>
& Dependency<'zero', [T]>
& Dependency<'nan', [Complex<T>]>): ImpType<'complexSqrt', [T]> =>
r => {
if (dep.isSquare(r)) return dep.complex(dep.conservativeSqrt(r))
const negative = dep.unaryMinus(r)
if (dep.isSquare(negative)) {
return complex_binary(
dep.zero(r), dep.conservativeSqrt(negative))
}
// neither the real number or its negative is a square; could happen
// for example with bigint. So there is no square root. So we have to
// return the NaN of the type.
return dep.nan(dep.complex(r))
}
export const sqrt =
<T>(dep: Dependency<'isReal', [Complex<T>]>
& Dependency<'complexSqrt', [T]>
& Dependency<'absquare', [Complex<T>]>
& Dependency<'conservativeSqrt', [UnderlyingReal<T>]>
& Dependency<'addReal', [Complex<T>,UnderlyingReal<T>]>
& Dependency<'re', [Complex<T>]>
& Dependency<'add', [UnderlyingReal<T>,UnderlyingReal<T>]>
& Dependency<'divideByReal', [Complex<T>,UnderlyingReal<T>]>
): ImpType<'sqrt', [Complex<T>]> =>
z => {
if (dep.isReal(z)) return dep.complexSqrt(z.re)
const myabs = dep.conservativeSqrt(dep.absquare(z))
const num = dep.addReal(z, myabs)
const r = dep.re(z)
const denomsq = dep.add(dep.add(myabs, myabs), dep.add(r, r))
const denom = dep.conservativeSqrt(denomsq)
return dep.divideByReal(num, denom)
}
export const conservativeSqrt = sqrt

19
src/Complex/predicate.ts Normal file
View File

@ -0,0 +1,19 @@
import {Complex} from './type.js'
import {Signature, Dependency, ImpType} from '../core/Dispatcher.js'
declare module "./type" {
interface ComplexReturn<Params> {
isReal: Signature<Params, [Complex<any>], boolean>
isSquare: Signature<Params, [Complex<any>], boolean>
}
}
export const isReal =
<T>(dep: Dependency<'equal', [T,T]>
& Dependency<'add', [T,T]>
& Dependency<'isReal', [T]>
): ImpType<'isReal', [Complex<T>]> =>
z => dep.isReal(z.re) && dep.equal(z.re, dep.add(z.re, z.im))
export const isSquare: ImpType<'isSquare', [Complex<any>]> =
z => true // FIXME: not correct for Complex<bigint> once we get there

15
src/Complex/relational.ts Normal file
View File

@ -0,0 +1,15 @@
import {Complex} from './type.js'
import {BBinary, ImpType, Dependency} from '../core/Dispatcher.js'
declare module "./type" {
interface ComplexReturn<Params> {
equal: Params extends BBinary<infer B>
? B extends Complex<any> ? boolean : never
: never
}
}
export const equal =
<T>(dep: Dependency<'equal', [T,T]>):
ImpType<'equal', [Complex<T>, Complex<T>]> =>
(w, z) => dep.equal(w.re, z.re) && dep.equal(w.im, z.im)

View File

@ -4,6 +4,9 @@ import {
export type Complex<T> = {re: T; im: T;} export type Complex<T> = {re: T; im: T;}
export type UnderlyingReal<T> =
T extends Complex<infer U> ? UnderlyingReal<U> : T
export const Complex_type = { export const Complex_type = {
test: <T>(dep: {testT: (z: unknown) => z is T}) => test: <T>(dep: {testT: (z: unknown) => z is T}) =>
(z: unknown): z is Complex<T> => (z: unknown): z is Complex<T> =>
@ -26,6 +29,25 @@ export interface ComplexReturn<Params> {
complex: Params extends [infer U] ? Complex<U> // unary case complex: Params extends [infer U] ? Complex<U> // unary case
: Params extends BBinary<infer B> ? Complex<B> // binary case : Params extends BBinary<infer B> ? Complex<B> // binary case
: never : never
zero: Params extends [infer Z] // unary
? Z extends Complex<infer T> // of a Complex parameter
? ImpReturns<'zero', T> extends T ? Z : never // that has its real 0
: never
: never
one: Params extends [infer Z] // unary
? Z extends Complex<infer T> // of a Complex parameter
? ImpReturns<'one'|'zero', T> extends T ? Z : never // has real 1, 0
: never
: never
nan: Params extends [infer Z] // unary
? Z extends Complex<infer T> // of a Complex parameter
? ImpReturns<'nan', T> extends T ? Z : never // has real NaN
: never
: never
re: Params extends [infer Z]
? Z extends Complex<infer T> ? UnderlyingReal<T> : never
: never
} }
export const complex_unary = export const complex_unary =
@ -33,3 +55,20 @@ export const complex_unary =
t => ({re: t, im: dep.zero(t)}) t => ({re: t, im: dep.zero(t)})
export const complex_binary = <T>(t: T, u: T): ImpReturns<'complex', [T,T]> => export const complex_binary = <T>(t: T, u: T): ImpReturns<'complex', [T,T]> =>
({re: t, im: u}) ({re: t, im: u})
export const zero =
<T>(dep: Dependency<'zero', [T]>): ImpType<'zero', [Complex<T>]> =>
z => complex_binary(dep.zero(z.re), dep.zero(z.im))
export const one =
<T>(dep: Dependency<'zero' | 'one', [T]>): ImpType<'one', [Complex<T>]> =>
z => // Must provide parameter T, else TS narrows to return type of dep.one
complex_binary<T>(dep.one(z.re), dep.zero(z.im))
export const nan =
<T>(dep: Dependency<'nan', [T]>): ImpType<'nan', [Complex<T>]> =>
z => complex_binary(dep.nan(z.re), dep.nan(z.im))
export const re =
<T>(dep: Dependency<'re', [T]>): ImpType<'re', [Complex<T>]> =>
z => dep.re(z.re)

View File

@ -1,4 +1,5 @@
export type Config = { export type Config = {
epsilon: number
predictable: boolean predictable: boolean
} }

View File

@ -66,12 +66,24 @@ export interface ReturnTypes<Params> {}
export type Signature<CandidateParams, ActualParams, Returns> = export type Signature<CandidateParams, ActualParams, Returns> =
CandidateParams extends ActualParams ? Returns : never CandidateParams extends ActualParams ? Returns : never
// A homogenous binary operation (comes up a lot, needs a better name?) // A homogeneous binary parameter tuple (comes up a lot, needs a better name?)
// Typical usage: `foo_impl: Params extends BBinary<infer B> ? B : never` // Typical usage: `foo_impl: Params extends BBinary<infer B> ? B : never`
// says that this implementation takes two arguments, both of type B, and // says that this implementation takes two arguments, both of type B, and
// returns the same type. // returns the same type.
export type BBinary<B> = [B, B] export type BBinary<B> = [B, B]
// A unary signature that preserves the type of its argument, which must
// extend the given Bound:
export type ConservativeUnary<CandidateParams, Bound> =
CandidateParams extends [infer T] ? T extends Bound ? T : never : never
// A homogeneous binary signature that preserves the common type of its
// arguments, which must extend the given Bound:
export type ConservativeBinary<CandidateParams, Bound> =
CandidateParams extends BBinary<infer B>
? B extends Bound ? B : never
: never
// Helper for collecting return types // Helper for collecting return types
// (Really just adds the literal string Suffix onto the keys of interface IFace) // (Really just adds the literal string Suffix onto the keys of interface IFace)
export type ForType<Suffix extends string, IFace> = keyof IFace extends string export type ForType<Suffix extends string, IFace> = keyof IFace extends string

View File

@ -1,25 +1,43 @@
import {configDependency} from '../core/Config.js' import {configDependency} from '../core/Config.js'
import {Signature, Dependency, ImpType} from '../core/Dispatcher.js' import {
import type {Complex} from '../Complex/type.js' Signature, ConservativeBinary, ConservativeUnary, Dependency, ImpType
} from '../core/Dispatcher.js'
import type {Complex, UnderlyingReal} from '../Complex/type.js'
declare module "./type" { declare module "./type" {
interface NumbersReturn<Params> { interface NumbersReturn<Params> {
// This description loses information: some subtypes like NumInt or // This description loses information: some subtypes like NumInt or
// Positive are closed under addition, but this says that the result // Positive are closed under addition, but this says that the result
// of add is just a number, not still of the reduced type // of add is just a number, not still of the reduced type
add: Signature<Params, [number, number], number> // add: Signature<Params, [number, number], number>
// Whereas this one would preserve information, but would lie
// Whereas this one preserves information, but lies
// because it claims all subtypes of number are closed under addition, // because it claims all subtypes of number are closed under addition,
// which is not true for `1 | 2 | 3`, for example. // which is not true for `1 | 2 | 3`, for example. But because in
// add: Params extends BBinary<infer B> // generics that use add we often need to assign the result of add
// ? B extends number ? B : never // to something of the exact generic type, generics using add won't
// : never // compile unless we lie in this way and assert that add returns
// // the subtype.
add: ConservativeBinary<Params, number>
// Not sure how this will need to go when we introduce NumInt. // Not sure how this will need to go when we introduce NumInt.
unaryMinus: Signature<Params, [number], number>
subtract: Signature<Params, [number, number], number> addReal: Params extends [infer R, infer S]
multiply: Signature<Params, [number, number], number> ? R extends number ? S extends R ? R : never : never
divide: Signature<Params, [number, number], number> : never
unaryMinus: ConservativeUnary<Params, number>
conj: ConservativeUnary<Params, number>
subtract: ConservativeBinary<Params, number>
multiply: ConservativeBinary<Params, number>
absquare: Params extends [infer R]
? R extends number ? UnderlyingReal<R> : never
: never
reciprocal: ConservativeUnary<Params, number>
divide: ConservativeBinary<Params, number>
divideByReal: Params extends [infer R, infer S]
? R extends number ? S extends R ? R : never : never
: never
// best square root that remains the same type
conservativeSqrt: ConservativeUnary<Params, number>
// Best we can do for sqrt at compile time, since actual return // Best we can do for sqrt at compile time, since actual return
// type depends on config. Not sure how this will play out // type depends on config. Not sure how this will play out
// when we make a number-only bundle, but at least the import type // when we make a number-only bundle, but at least the import type
@ -29,16 +47,23 @@ declare module "./type" {
} }
export const add: ImpType<'add', [number, number]> = (a, b) => a + b export const add: ImpType<'add', [number, number]> = (a, b) => a + b
export const addReal = add
export const unaryMinus: ImpType<'unaryMinus', [number]> = a => -a export const unaryMinus: ImpType<'unaryMinus', [number]> = a => -a
export const conj: ImpType<'conj', [number]> = a => a
export const subtract: ImpType<'subtract', [number, number]> = (a, b) => a - b export const subtract: ImpType<'subtract', [number, number]> = (a, b) => a - b
export const multiply: ImpType<'multiply', [number, number]> = (a, b) => a * b export const multiply: ImpType<'multiply', [number, number]> = (a, b) => a * b
export const absquare: ImpType<'absquare', [number]> = a => a*a
export const reciprocal: ImpType<'reciprocal', [number]> = a => 1/a
export const divide: ImpType<'divide', [number, number]> = (a, b) => a / b export const divide: ImpType<'divide', [number, number]> = (a, b) => a / b
export const divideByReal: ImpType<'divideByReal', [number, number]> = divide
export const conservativeSqrt: ImpType<'conservativeSqrt', [number]> =
a => isNaN(a) ? NaN : Math.sqrt(a)
export const sqrt = export const sqrt =
(dep: configDependency (dep: configDependency
& Dependency<'complex', [number, number]>): ImpType<'sqrt', [number]> => { & Dependency<'complex', [number, number]>): ImpType<'sqrt', [number]> => {
if (dep.config.predictable || !dep.complex) { if (dep.config.predictable || !dep.complex) return conservativeSqrt
return a => isNaN(a) ? NaN : Math.sqrt(a)
}
return a => { return a => {
if (isNaN(a)) return NaN if (isNaN(a)) return NaN
if (a >= 0) return Math.sqrt(a) if (a >= 0) return Math.sqrt(a)

11
src/numbers/predicate.ts Normal file
View File

@ -0,0 +1,11 @@
import {Signature, ImpType} from '../core/Dispatcher.js'
declare module "./type" {
interface NumbersReturn<Params> {
isReal: Signature<Params, [number], true>
isSquare: Signature<Params, [number], boolean>
}
}
export const isReal: ImpType<'isReal', [number]> = a => true
export const isSquare: ImpType<'isSquare', [number]> = a => a >= 0

27
src/numbers/relational.ts Normal file
View File

@ -0,0 +1,27 @@
import {configDependency} from '../core/Config.js'
import {Signature, ImpType} from '../core/Dispatcher.js'
const DBL_EPSILON = Number.EPSILON || 2.2204460492503130808472633361816E-16
declare module "./type" {
interface NumbersReturn<Params> {
equal: Signature<Params, [number, number], boolean>
}
}
export const equal =
(dep: configDependency): ImpType<'equal', [number, number]> =>
(x, y) => {
const eps = dep.config.epsilon
if (eps === null || eps === undefined) return x === y
if (x === y) return true
if (isNaN(x) || isNaN(y)) return false
if (isFinite(x) && isFinite(y)) {
const diff = Math.abs(x - y)
if (diff < DBL_EPSILON) return true
return diff <= Math.max(Math.abs(x), Math.abs(y)) * eps
}
return false
}

View File

@ -1,4 +1,5 @@
import {ImpType} from '../core/Dispatcher.js' import {ImpType} from '../core/Dispatcher.js'
import type {UnderlyingReal} from '../Complex/type.js'
export const number_type = { export const number_type = {
before: ['Complex'], before: ['Complex'],
@ -25,6 +26,19 @@ export interface NumbersReturn<Params> {
// zero: Signature<Params, [number], 0> // zero: Signature<Params, [number], 0>
// makes complex fail to compile, because it worries that you might be // makes complex fail to compile, because it worries that you might be
// making `Complex<Small>` where zero would not return the right type. // making `Complex<Small>` where zero would not return the right type.
one: Params extends [infer T]
? T extends number ? 1 extends T ? 1 : never : never
: never
nan: Params extends [infer T]
? T extends number ? typeof NaN extends T ? typeof NaN : never : never
: never
re: Params extends [infer T]
? T extends number ? UnderlyingReal<T> : never
: never
} }
export const zero: ImpType<'zero', [number]> = a => 0 export const zero: ImpType<'zero', [number]> = a => 0
export const one: ImpType<'one', [number]> = a => 1
export const nan: ImpType<'nan', [number]> = a => NaN
export const re: ImpType<'re', [number]> = a => a