bali-component-framework
Version:
This library provides a JavaScript based implementation of the Bali Nebula™ Component Framework.
636 lines (552 loc) • 25.1 kB
JavaScript
/************************************************************************
* Copyright (c) Crater Dog Technologies(TM). All Rights Reserved. *
************************************************************************
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. *
* *
* This code is free software; you can redistribute it and/or modify it *
* under the terms of The MIT License (MIT), as published by the Open *
* Source Initiative. (See http://opensource.org/licenses/MIT) *
************************************************************************/
'use strict';
/*
* This element class captures the state and methods associated with a
* complex number element.
*/
const moduleName = '/bali/elements/Number';
const utilities = require('../utilities');
const abstractions = require('../abstractions');
const agents = require('../agents');
const Angle = require('./Angle').Angle;
// PUBLIC FUNCTIONS
/**
* This function creates an immutable instance of a complex number using the specified
* real and imaginary values. If the imaginary value is an angle then the complex number
* is in polar form, otherwise it is in rectangular form.
*
* An optional debug argument may be specified that controls the level of debugging that
* should be applied during execution. The allowed levels are as follows:
* <pre>
* 0: no debugging is applied (this is the default value and has the best performance)
* 1: log any exceptions to console.error before throwing them
* 2: perform argument validation checks on each call (poor performance)
* 3: log interesting arguments, states and results to console.log
* </pre>
*
* @param {Array} value The real and imaginary values of the complex number.
* @param {Object} parameters Optional parameters used to parameterize this element.
* @returns {Complex} The new complex number.
*/
const Complex = function(value, parameters, debug) {
abstractions.Element.call(
this,
[ moduleName ],
[
'/bali/libraries/Scalable',
'/bali/libraries/Numerical',
'/bali/interfaces/Polarized',
'/bali/interfaces/Discrete',
'/bali/interfaces/Continuous'
],
parameters,
debug
);
if (this.debug > 1) {
this.validateArgument('$Number', '$value', value, [
'/javascript/Undefined',
'/javascript/Array'
]);
}
// normalize the values
var real = value[0]; // real part or magnitude
var imaginary = value[1]; // imaginary part or phase (angle)
var magnitude, phase;
this.calculator = new utilities.Calculator(this.debug);
if (real === real) real = real || 0; // default value if not NaN and not defined or null
real = this.calculator.lockOnExtreme(real);
if (imaginary === imaginary) imaginary = imaginary || 0; // default value if not NaN and not defined or null
if (imaginary.isComponent && imaginary.isType('/bali/elements/Angle')) {
// convert polar to rectangular
magnitude = real;
phase = imaginary;
if (magnitude < 0) {
// normalize the magnitude
magnitude = -magnitude;
phase = Angle.inverse(phase);
}
real = magnitude * Angle.cosine(phase);
imaginary = magnitude * Angle.sine(phase);
if (real && imaginary) {
// it is a true complex number in polar form
this.isPolar = true;
}
}
imaginary = this.calculator.lockOnExtreme(imaginary);
if (real.toString() === 'NaN' || imaginary.toString() === 'NaN') {
real = NaN;
imaginary = NaN;
} else if (real === Infinity || real === -Infinity || imaginary === Infinity || imaginary === -Infinity) {
real = Infinity;
imaginary = Infinity;
}
this.isInteger = Number.isInteger(real) && imaginary === 0;
this.getReal = function() { return real; };
this.getImaginary = function() { return imaginary; };
this.getMagnitude = function() {
if (magnitude === undefined) {
// need to preserve full precision on this except for the sum part
magnitude = Math.sqrt(this.calculator.sum(Math.pow(real, 2), Math.pow(imaginary, 2)));
magnitude = this.calculator.lockOnExtreme(magnitude);
}
return magnitude;
};
this.getPhase = function() {
if (this.isInfinite()) return new Angle(0);
if (this.isUndefined()) return undefined;
if (phase === undefined) {
phase = Angle.arctangent(imaginary, real);
}
return phase;
};
this.getValue = function() {
if (this.isPolar) {
return [this.getMagnitude(), this.getPhase()];
}
return [this.getReal(), this.getImaginary()];
};
return this;
};
Complex.prototype = Object.create(abstractions.Element.prototype);
Complex.prototype.constructor = Complex;
exports.Number = Complex;
// PUBLIC METHODS
/**
* This method returns whether or not this complex number has a meaningful value. If the magnitude
* is undefined, zero or infinity it returns <code>false</code>, otherwise it returns <code>true</code>.
*
* @returns {Boolean} Whether or not this complex number has a meaningful value.
*/
Complex.prototype.isSignificant = function() {
return !(this.isZero() || this.isUndefined());
};
/**
* This method returns the real part of this complex number rounded to an integer.
*
* @returns {Number} The real part of this complex number rounded to an integer.
*/
Complex.prototype.toInteger = function() {
return Math.round(this.getReal());
};
/**
* This method returns the real part of this complex number.
*
* @returns {Number} The real part of this complex number.
*/
Complex.prototype.toReal = function() {
return this.getReal();
};
/**
* This method determines whether this complex number is undefined.
*
* @returns {boolean} Whether or not this complex number is undefined.
*/
Complex.prototype.isUndefined = function() {
return this.getReal().toString() === 'NaN'; // must use strings since NaN !== NaN
};
/**
* This method determines whether this complex number is zero.
*
* @returns {boolean} Whether or not this complex number is zero.
*/
Complex.prototype.isZero = function() {
return this.getReal() === 0 && this.getImaginary() === 0;
};
/**
* This method determines whether this complex number is infinite.
*
* @returns {boolean} Whether or not this complex number is infinite.
*/
Complex.prototype.isInfinite = function() {
return this.getReal() === Infinity;
};
/**
* This method determines whether or not this number has a negative value.
*
* @returns {Boolean} Whether or not this number is negative.
*/
Complex.prototype.isNegative = function() {
return this.getReal() < 0;
};
// SCALABLE LIBRARY FUNCTIONS
/**
* This function returns the arithmetic inverse of the specified complex number.
* <pre>
* inverse(z): (-z.real, -z.imaginary i)
* </pre>
*
* @param {Complex} number The complex number to be inverted.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.inverse = function(number, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$inverse', '$number', number, [
'/bali/elements/Number'
]);
}
// handle the special cases
if (number.isUndefined()) return new Complex([NaN, undefined], number.getParameters(), debug);
if (number.isInfinite()) return new Complex([Infinity, undefined], number.getParameters(), debug);
if (number.isZero()) return new Complex([0, undefined], number.getParameters(), debug);
const real = -number.getReal();
const imaginary = -number.getImaginary();
const result = new Complex([real, imaginary], number.getParameters(), debug);
return result;
};
/**
* This function returns the sum of two complex numbers.
* <pre>
* sum(x, y): (x.real + y.real, (x.imaginary + y.imaginary) i)
* </pre>
*
* @param {Complex} first The first complex number to be added.
* @param {Complex} second The second complex number to be added.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.sum = function(first, second, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$sum', '$first', first, [
'/bali/elements/Number'
]);
abstractions.Component.validateArgument(moduleName, '$sum', '$second', second, [
'/bali/elements/Number'
]);
}
// handle the special cases
const comparator = new agents.CanonicalComparator(this.debug);
if (first.isUndefined() || second.isUndefined()) return new Complex([NaN, undefined], first.getParameters(), debug);
if (first.isInfinite() || second.isInfinite()) return new Complex([Infinity, undefined], first.getParameters(), debug);
if (comparator.areEqual(first, Complex.inverse(second))) return new Complex([0, undefined], first.getParameters(), debug);
const calculator = new utilities.Calculator(debug);
const real = calculator.sum(first.getReal(), second.getReal());
const imaginary = calculator.sum(first.getImaginary(), second.getImaginary());
const result = new Complex([real, imaginary], first.getParameters(), debug);
return result;
};
/**
* This function returns the difference of two complex numbers.
* <pre>
* difference(x, y): (x.real - y.real, (x.imaginary - y.imaginary) i)
* </pre>
*
* @param {Complex} first The first complex number to be subtracted from.
* @param {Complex} second The second complex number to be subtracted.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.difference = function(first, second, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$difference', '$first', first, [
'/bali/elements/Number'
]);
abstractions.Component.validateArgument(moduleName, '$difference', '$second', second, [
'/bali/elements/Number'
]);
}
return Complex.sum(first, Complex.inverse(second, debug), debug);
};
/**
* This function returns a scaled version of a complex number.
* <pre>
* scaled(z, factor): (factor * z.real, factor * z.imaginary i)
* or
* scaled(z, factor): (factor * z.magnitude, z.phase i)
* </pre>
*
* @param {Complex} number The complex number to be scaled.
* @param {Number} factor The numeric scale factor.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.scaled = function(number, factor, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$scaled', '$number', number, [
'/bali/elements/Number'
]);
abstractions.Component.validateArgument(moduleName, '$scaled', '$factor', factor, [
'/javascript/Number'
]);
}
// handle the special cases
if (number.isUndefined() || Number.isNaN(factor)) return new Complex([NaN, undefined], number.getParameters(), debug);
if (number.isZero() && !Number.isFinite(factor)) return new Complex([NaN, undefined], number.getParameters(), debug);
if (number.isInfinite() && factor === 0) return new Complex([NaN, undefined], number.getParameters(), debug);
if (number.isInfinite() || !Number.isFinite(factor)) return new Complex([Infinity, undefined], number.getParameters(), debug);
if (number.isZero() || factor === 0) return new Complex([0, undefined], number.getParameters(), debug);
const calculator = new utilities.Calculator(debug);
const real = calculator.product(number.getReal(), factor);
const imaginary = calculator.product(number.getImaginary(), factor);
const result = new Complex([real, imaginary], number.getParameters(), debug);
return result;
};
// NUMERICAL LIBRARY FUNCTIONS
/**
* This function returns the multiplicative inverse of the specified complex number.
* <pre>
* reciprocal(z): (z.real, -z.imaginary i)/z.magnitude^2
* or
* reciprocal(z): (1/z.magnitude e^~-z.phase i)
* </pre>
*
* @param {Complex} number The complex number to be inverted.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.reciprocal = function(number, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$reciprocal', '$number', number, [
'/bali/elements/Number'
]);
}
// handle the special cases
if (number.isUndefined()) return new Complex([NaN, undefined], number.getParameters(), debug);
if (number.isInfinite()) return new Complex([0, undefined], number.getParameters(), debug);
if (number.isZero()) return new Complex([Infinity, undefined], number.getParameters(), debug);
const calculator = new utilities.Calculator(debug);
const squared = calculator.sum(calculator.product(number.getReal(), number.getReal()), calculator.product(number.getImaginary(), number.getImaginary()));
const real = calculator.quotient(number.getReal(), squared);
const imaginary = -calculator.quotient(number.getImaginary(), squared);
const result = new Complex([real, imaginary], number.getParameters(), debug);
return result;
};
/**
* This function returns the complex conjugate of the specified complex number.
* <pre>
* conjugate(z): (z.real, -z.imaginary i)
* </pre>
*
* @param {Complex} number The complex number to be conjugated.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.conjugate = function(number, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$conjugate', '$number', number, [
'/bali/elements/Number'
]);
}
// handle the special cases
if (number.isUndefined()) return new Complex([NaN, undefined], number.getParameters(), debug);
if (number.isInfinite()) return new Complex([Infinity, undefined], number.getParameters(), debug);
if (number.isZero()) return new Complex([0, undefined], number.getParameters(), debug);
const real = number.getReal();
const imaginary = -number.getImaginary();
const result = new Complex([real, imaginary], number.getParameters(), debug);
return result;
};
/**
* This function returns the complex factorial of the specified complex number.
*
* @param {Complex} number The complex number.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.factorial = function(number, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$factorial', '$number', number, [
'/bali/elements/Number'
]);
}
// handle the special cases
if (number.isUndefined()) return new Complex([NaN, undefined], number.getParameters(), debug);
if (number.isInfinite()) return new Complex([Infinity, undefined], number.getParameters(), debug);
if (number.isZero()) return new Complex([1, undefined], number.getParameters(), debug);
// just implement real factorials for now...
// TODO: what should a complex factorial be?
const factorial = gamma(number.getReal() + 1);
const result = new Complex([factorial, undefined], number.getParameters(), debug);
return result;
};
/**
* This function returns the product of two complex numbers.
* <pre>
* product(x, y): (x.magnitude * y.magnitude e^~(x.phase + y.phase) i)
* </pre>
*
* @param {Complex} first The first complex number to be multiplied.
* @param {Complex} second The second complex number to be multiplied.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.product = function(first, second, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$product', '$first', first, [
'/bali/elements/Number'
]);
abstractions.Component.validateArgument(moduleName, '$product', '$second', second, [
'/bali/elements/Number'
]);
}
// handle the special cases
if (first.isUndefined() || second.isUndefined()) return new Complex([NaN, undefined], first.getParameters(), debug);
if (first.isZero() && second.isInfinite()) return new Complex([NaN, undefined], first.getParameters(), debug);
if (first.isInfinite() && second.isZero()) return new Complex([NaN, undefined], first.getParameters(), debug);
if (first.isInfinite() || second.isInfinite()) return new Complex([Infinity, undefined], first.getParameters(), debug);
if (first.isZero() || second.isZero()) return new Complex([0, undefined], first.getParameters(), debug);
const calculator = new utilities.Calculator(debug);
const real = calculator.difference(calculator.product(first.getReal(), second.getReal()), calculator.product(first.getImaginary(), second.getImaginary()));
const imaginary = calculator.sum(calculator.product(first.getReal(), second.getImaginary()), calculator.product(first.getImaginary() * second.getReal()));
const result = new Complex([real, imaginary], first.getParameters(), debug);
return result;
};
/**
* This function returns the quotient of two complex numbers.
* <pre>
* quotient(x, y): (x.magnitude / y.magnitude e^~(x.phase - y.phase) i)
* </pre>
*
* @param {Complex} first The first complex number to be divided.
* @param {Complex} second The second complex number to be divided by.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.quotient = function(first, second, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$quotient', '$first', first, [
'/bali/elements/Number'
]);
abstractions.Component.validateArgument(moduleName, '$quotient', '$second', second, [
'/bali/elements/Number'
]);
}
return Complex.product(first, Complex.reciprocal(second, debug), debug);
};
/**
* This function returns the remainder of the quotient of two real numbers.
*
* @param {Complex} first The first real number to be divided.
* @param {Complex} second The second real number to be divided by.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting real number.
*/
Complex.remainder = function(first, second, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$remainder', '$first', first, [
'/bali/elements/Number'
]);
abstractions.Component.validateArgument(moduleName, '$remainder', '$second', second, [
'/bali/elements/Number'
]);
}
// handle the special cases
if (first.isUndefined() || second.isUndefined()) return new Complex([NaN, undefined], first.getParameters(), debug);
if (first.isInfinite() && second.isInfinite()) return new Complex([NaN, undefined], first.getParameters(), debug);
if (first.isZero() && second.isZero()) return new Complex([NaN, undefined], first.getParameters(), debug);
if (second.isInfinite()) return new Complex([0, undefined], first.getParameters(), debug);
if (second.isZero()) return new Complex([Infinity, undefined], first.getParameters(), debug);
// just implement for integer values
// TODO: what does remainder mean for complex numbers?
const firstInteger = Math.round(first.getReal());
const secondInteger = Math.round(second.getReal());
const calculator = new utilities.Calculator(debug);
return new Complex([calculator.remainder(firstInteger, secondInteger), undefined], first.getParameters(), debug);
};
/**
* This function returns the complex exponential of the specified complex number.
* <pre>
* exponential(base, exponent): exp(exponent * ln(base))
* </pre>
*
* @param {Complex} base The complex base.
* @param {Complex} exponent The complex exponent.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.exponential = function(base, exponent, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$exponential', '$base', base, [
'/bali/elements/Number'
]);
abstractions.Component.validateArgument(moduleName, '$exponential', '$exponent', exponent, [
'/bali/elements/Number'
]);
}
// handle the special cases
if (base.isUndefined() || exponent.isUndefined()) return new Complex([NaN, undefined], base.getParameters(), debug);
if (base.isZero() && (exponent.isZero() || exponent.isInfinite())) return new Complex([NaN, undefined], base.getParameters(), debug);
if (base.isInfinite() && exponent.isZero()) return new Complex([NaN, undefined], base.getParameters(), debug);
if (exponent.isInfinite()) return new Complex([Infinity, undefined], base.getParameters(), debug);
if (exponent.isZero()) return new Complex([1, undefined], base.getParameters(), debug);
const result = exp(Complex.product(exponent, ln(base, debug), debug), debug);
return result;
};
/**
* This function returns the complex logarithm with the specified base of the
* specified complex number.
* <pre>
* logarithm(base, value): ln(value)/ln(base)
* </pre>
*
* @param {Complex} base The base of the resulting exponent.
* @param {Complex} value The complex number.
* @param {Number} debug A number in the range 0..3.
* @returns {Complex} The resulting complex number.
*/
Complex.logarithm = function(base, value, debug) {
if (debug > 1) {
abstractions.Component.validateArgument(moduleName, '$logarithm', '$base', base, [
'/bali/elements/Number'
]);
abstractions.Component.validateArgument(moduleName, '$logarithm', '$value', value, [
'/bali/elements/Number'
]);
}
// handle the special cases
if (base.isUndefined() || value.isUndefined()) return new Complex([NaN, undefined], base.getParameters(), debug);
if (base.isZero() && (value.isZero() || value.isInfinite())) return new Complex([NaN, undefined], base.getParameters(), debug);
if (base.isInfinite() && (value.isZero() || value.isInfinite())) return new Complex([NaN, undefined], base.getParameters(), debug);
if (value.isInfinite()) return new Complex([Infinity, undefined], base.getParameters(), debug);
if (value.isZero()) return new Complex([Infinity, undefined], base.getParameters(), debug);
const result = Complex.quotient(ln(value, debug), ln(base, debug), debug);
return result;
};
// PRIVATE FUNCTIONS
// TODO: should the math in the gamma function use the precision module?
const gamma = function(number) {
const p = [0.99999999999980993, 676.5203681218851, -1259.1392167224028,
771.32342877765313, -176.61502916214059, 12.507343278686905,
-0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7
];
const g = 7;
if (number < 0.5) {
return Math.PI / (Math.sin(Math.PI * number) * gamma(1 - number));
}
number -= 1;
var a = p[0];
const t = number + g + 0.5;
for (var i = 1; i < p.length; i++) {
a += p[i] / (number + i);
}
return Math.sqrt(2 * Math.PI) * Math.pow(t, number + 0.5) * Math.exp(-t) * a;
};
const exp = function(number, debug) {
if (number.isUndefined()) return new Complex([NaN, undefined], number.getParameters(), debug);
if (number.isInfinite()) return new Complex([Infinity, undefined], number.getParameters(), debug);
if (number.isZero()) return new Complex([1, undefined], number.getParameters(), debug);
const calculator = new utilities.Calculator(debug);
const scale = calculator.exponential(number.getReal());
const real = calculator.product(scale, calculator.cosine(number.getImaginary()));
const imaginary = calculator.product(scale, calculator.sine(number.getImaginary()));
const result = new Complex([real, imaginary], number.getParameters(), debug);
return result;
};
const ln = function(number, debug) {
if (number.isUndefined()) return new Complex([NaN, undefined], number.getParameters(), debug);
if (number.isInfinite()) return new Complex([Infinity, undefined], number.getParameters(), debug);
if (number.isZero()) return new Complex([Infinity, undefined], number.getParameters(), debug);
const calculator = new utilities.Calculator(debug);
const real = calculator.logarithm(number.getMagnitude());
const imaginary = number.getPhase().getValue();
const result = new Complex([real, imaginary], number.getParameters(), debug);
return result;
};