herta
Version:
Advanced mathematics framework for scientific, engineering, and financial applications
589 lines (512 loc) • 16.2 kB
JavaScript
/**
* Complex Analysis Module for herta.js
* Provides tools for working with complex functions, contour integrals, and conformal mappings
*/
/**
* Represents a complex number with real and imaginary parts
* @param {number} real - Real part
* @param {number} imag - Imaginary part
* @returns {Object} - Complex number object
*/
function complex(real, imag = 0) {
return {
re: real,
im: imag,
/**
* Get the modulus (absolute value) of the complex number
* @returns {number} - Modulus
*/
modulus() {
return Math.sqrt(this.re * this.re + this.im * this.im);
},
/**
* Get the argument (phase) of the complex number
* @returns {number} - Argument in radians
*/
argument() {
return Math.atan2(this.im, this.re);
},
/**
* Get the complex conjugate
* @returns {Object} - Complex conjugate
*/
conjugate() {
return complex(this.re, -this.im);
},
/**
* Add another complex number
* @param {Object} other - Complex number to add
* @returns {Object} - Result of addition
*/
add(other) {
return complex(this.re + other.re, this.im + other.im);
},
/**
* Subtract another complex number
* @param {Object} other - Complex number to subtract
* @returns {Object} - Result of subtraction
*/
subtract(other) {
return complex(this.re - other.re, this.im - other.im);
},
/**
* Multiply by another complex number
* @param {Object} other - Complex number to multiply by
* @returns {Object} - Result of multiplication
*/
multiply(other) {
return complex(
this.re * other.re - this.im * other.im,
this.re * other.im + this.im * other.re
);
},
/**
* Divide by another complex number
* @param {Object} other - Complex number to divide by
* @returns {Object} - Result of division
*/
divide(other) {
const denominator = other.re * other.re + other.im * other.im;
return complex(
(this.re * other.re + this.im * other.im) / denominator,
(this.im * other.re - this.re * other.im) / denominator
);
},
/**
* Raise to a power
* @param {number} n - Power to raise to
* @returns {Object} - Result of exponentiation
*/
pow(n) {
if (n === 0) return complex(1, 0);
const r = this.modulus();
const theta = this.argument();
const newR = r ** n;
const newTheta = theta * n;
return complex(
newR * Math.cos(newTheta),
newR * Math.sin(newTheta)
);
},
/**
* Get the complex exponential
* @returns {Object} - Result of e^z
*/
exp() {
const expReal = Math.exp(this.re);
return complex(
expReal * Math.cos(this.im),
expReal * Math.sin(this.im)
);
},
/**
* Get the complex logarithm (principal value)
* @returns {Object} - Result of log(z)
*/
log() {
return complex(
Math.log(this.modulus()),
this.argument()
);
},
/**
* Get the complex sine
* @returns {Object} - Result of sin(z)
*/
sin() {
return complex(
Math.sin(this.re) * Math.cosh(this.im),
Math.cos(this.re) * Math.sinh(this.im)
);
},
/**
* Get the complex cosine
* @returns {Object} - Result of cos(z)
*/
cos() {
return complex(
Math.cos(this.re) * Math.cosh(this.im),
-Math.sin(this.re) * Math.sinh(this.im)
);
},
/**
* Get the complex tangent
* @returns {Object} - Result of tan(z)
*/
tan() {
return this.sin().divide(this.cos());
},
/**
* String representation
* @returns {string} - String form of complex number
*/
toString() {
if (this.im === 0) return `${this.re}`;
if (this.re === 0) return `${this.im}i`;
if (this.im < 0) return `${this.re} - ${-this.im}i`;
return `${this.re} + ${this.im}i`;
}
};
}
/**
* Creates a complex function from real and imaginary component functions
* @param {Function} realPart - Function computing the real part
* @param {Function} imagPart - Function computing the imaginary part
* @returns {Function} - Complex function
*/
function complexFunction(realPart, imagPart) {
return function (z) {
if (typeof z === 'number') {
z = complex(z, 0);
}
return complex(realPart(z.re, z.im), imagPart(z.re, z.im));
};
}
/**
* Check if a complex function is analytic (using Cauchy-Riemann equations)
* @param {Function} f - Complex function to check
* @param {Object} z - Complex point to check at
* @param {number} epsilon - Tolerance for derivative approximation
* @returns {boolean} - True if function appears to be analytic at the point
*/
function isAnalytic(f, z, epsilon = 1e-8) {
// Compute partial derivatives at point
const h = epsilon;
// f(z) = u(x,y) + iv(x,y)
const f_z = f(z);
// f(z+h)
const f_z_plus_h = f(complex(z.re + h, z.im));
// f(z+ih)
const f_z_plus_ih = f(complex(z.re, z.im + h));
// Approximate partial derivatives
const du_dx = (f_z_plus_h.re - f_z.re) / h;
const du_dy = (f_z_plus_ih.re - f_z.re) / h;
const dv_dx = (f_z_plus_h.im - f_z.im) / h;
const dv_dy = (f_z_plus_ih.im - f_z.im) / h;
// Check Cauchy-Riemann equations
return Math.abs(du_dx - dv_dy) < epsilon && Math.abs(du_dy + dv_dx) < epsilon;
}
/**
* Compute the complex derivative of a function at a point
* @param {Function} f - Complex function
* @param {Object} z - Complex point
* @param {number} h - Step size for approximation
* @returns {Object} - Complex derivative f'(z)
*/
function derivative(f, z, h = 1e-6) {
const f_z = f(z);
const f_z_plus_h = f(complex(z.re + h, z.im));
return complex(
(f_z_plus_h.re - f_z.re) / h,
(f_z_plus_h.im - f_z.im) / h
);
}
/**
* Compute the zeros of a complex polynomial
* @param {Array<Object>} coefficients - Array of complex coefficients [a_n, a_{n-1}, ..., a_1, a_0]
* @returns {Array<Object>} - Array of complex zeros
*/
function findPolynomialZeros(coefficients) {
// For now, we'll implement a simple case for linear and quadratic polynomials
if (coefficients.length === 2) {
// Linear case: a*z + b = 0
const a = coefficients[0];
const b = coefficients[1];
return [complex(-b.re / a.re, -b.im / a.re)];
}
if (coefficients.length === 3) {
// Quadratic case: a*z^2 + b*z + c = 0
const a = coefficients[0];
const b = coefficients[1];
const c = coefficients[2];
// Calculate discriminant
const discriminant = b.multiply(b).subtract(a.multiply(c).multiply(complex(4, 0)));
const sqrtDisc = complex(Math.sqrt(discriminant.modulus()), 0);
// Apply quadratic formula
const negB = complex(-b.re, -b.im);
const twoA = complex(2 * a.re, 2 * a.im);
const root1 = negB.add(sqrtDisc).divide(twoA);
const root2 = negB.subtract(sqrtDisc).divide(twoA);
return [root1, root2];
}
// For higher-degree polynomials, we'd need to implement numerical methods
// like Durand-Kerner, but this is beyond the scope of this simple implementation
throw new Error('Finding zeros of polynomials higher than quadratic is not implemented yet');
}
/**
* Compute a contour integral of a complex function
* @param {Function} f - Complex function to integrate
* @param {Array<Object>} contour - Array of complex points defining the contour
* @returns {Object} - Result of the contour integral
*/
function contourIntegral(f, contour) {
let result = complex(0, 0);
for (let i = 0; i < contour.length - 1; i++) {
const z1 = contour[i];
const z2 = contour[i + 1];
// Line segment from z1 to z2
const dz = z2.subtract(z1);
// Midpoint rule for each segment
const midpoint = z1.add(z2).multiply(complex(0.5, 0));
const f_mid = f(midpoint);
// Accumulate contribution from this segment
result = result.add(f_mid.multiply(dz));
}
return result;
}
/**
* Compute the residue of a complex function at a pole
* @param {Function} f - Complex function
* @param {Object} pole - Complex pole location
* @param {number} order - Order of the pole
* @returns {Object} - Complex residue
*/
function residue(f, pole, order = 1) {
if (order === 1) {
// Simple pole: evaluate lim_{z→pole} (z-pole)*f(z)
const h = 1e-6;
const z = complex(pole.re + h, pole.im);
const z_minus_pole = z.subtract(pole);
return z_minus_pole.multiply(f(z));
}
// For higher-order poles, we'd need to implement more advanced methods
// but this is a simplification for educational purposes
throw new Error('Finding residues of poles with order > 1 is not implemented yet');
}
/**
* Compute Laurent series coefficients for a complex function
* @param {Function} f - Complex function
* @param {Object} center - Center of Laurent series
* @param {number} radius - Radius for contour integration
* @param {number} nTerms - Number of terms to compute
* @returns {Object} - Object with positive and negative index coefficients
*/
function laurentSeries(f, center, radius, nTerms = 5) {
const coefficients = {
// Positive indices (analytic part)
positive: [],
// Negative indices (principal part)
negative: []
};
// Generate points on a circle
const numPoints = 100;
const circle = [];
for (let i = 0; i < numPoints; i++) {
const theta = 2 * Math.PI * i / numPoints;
circle.push(complex(
center.re + radius * Math.cos(theta),
center.im + radius * Math.sin(theta)
));
}
circle.push(circle[0]); // Close the contour
// Compute coefficients using contour integration
for (let n = -nTerms; n <= nTerms; n++) {
// Integrand for nth coefficient
const integrand = function (z) {
const z_minus_center = z.subtract(center);
const denominator = z_minus_center.pow(n + 1);
return f(z).divide(denominator);
};
const result = contourIntegral(integrand, circle);
const coefficient = result.divide(complex(2 * Math.PI * 1, 0)); // 2πi
if (n < 0) {
coefficients.negative[-n] = coefficient;
} else {
coefficients.positive[n] = coefficient;
}
}
return coefficients;
}
/**
* Create a conformal mapping between two domains
* @param {Function} mapping - Complex function representing the mapping
* @returns {Object} - Conformal mapping object with direct and inverse functions
*/
function conformalMapping(mapping, inverse = null) {
// Validate that mapping is analytic at a sample point
const testPoint = complex(1, 1);
if (!isAnalytic(mapping, testPoint)) {
console.warn('Provided mapping may not be conformal (not analytic at test point)');
}
return {
map: mapping,
inverse,
/**
* Map a contour from domain to range
* @param {Array<Object>} contour - Array of complex points
* @returns {Array<Object>} - Mapped contour
*/
mapContour(contour) {
return contour.map((z) => this.map(z));
},
/**
* Map a contour from range back to domain (if inverse is provided)
* @param {Array<Object>} contour - Array of complex points
* @returns {Array<Object>} - Mapped contour
*/
inverseMapContour(contour) {
if (!this.inverse) {
throw new Error('Inverse mapping is not provided');
}
return contour.map((z) => this.inverse(z));
}
};
}
/**
* Common conformal mappings
*/
const conformalMappings = {
/**
* Möbius transformation with parameters a, b, c, d (where ad-bc ≠ 0)
* Maps z → (az+b)/(cz+d)
*/
mobius(a, b, c, d) {
// Check that ad-bc ≠ 0
const determinant = (a.re * d.re - a.im * d.im) - (b.re * c.re - b.im * c.im);
if (Math.abs(determinant) < 1e-10) {
throw new Error('Invalid Möbius transformation: ad-bc must not be zero');
}
const mapping = function (z) {
const numerator = a.multiply(z).add(b);
const denominator = c.multiply(z).add(d);
return numerator.divide(denominator);
};
const inverse = function (w) {
// Inverse Möbius transformation: (dw-b)/(-cw+a)
const numerator = d.multiply(w).subtract(b);
const denominator = a.subtract(c.multiply(w));
return numerator.divide(denominator);
};
return conformalMapping(mapping, inverse);
},
/**
* Exponential mapping: z → e^z
*/
exponential() {
const mapping = function (z) {
return z.exp();
};
const inverse = function (w) {
return w.log();
};
return conformalMapping(mapping, inverse);
},
/**
* Logarithmic mapping: z → log(z)
*/
logarithmic() {
const mapping = function (z) {
return z.log();
};
const inverse = function (w) {
return w.exp();
};
return conformalMapping(mapping, inverse);
},
/**
* Power mapping: z → z^n
*/
power(n) {
const mapping = function (z) {
return z.pow(n);
};
let inverse = null;
// Only provide inverse for integer powers
if (Number.isInteger(n) && n !== 0) {
inverse = function (w) {
return w.pow(1 / n);
};
}
return conformalMapping(mapping, inverse);
},
/**
* Joukowski mapping: z → z + 1/z
* Maps unit circle to line segment [-2, 2]
*/
joukowski() {
const mapping = function (z) {
return z.add(complex(1, 0).divide(z));
};
// No simple inverse for Joukowski mapping
return conformalMapping(mapping);
}
};
/**
* Compute the complex potential flow around a cylinder
* @param {number} velocity - Uniform flow velocity
* @param {number} radius - Cylinder radius
* @returns {Function} - Complex potential function
*/
function potentialFlowAroundCylinder(velocity, radius) {
return function (z) {
const r2 = radius * radius;
const term1 = complex(velocity * z.re, velocity * z.im);
const term2 = complex(
velocity * r2 * z.re / (z.re * z.re + z.im * z.im),
-velocity * r2 * z.im / (z.re * z.re + z.im * z.im)
);
return term1.subtract(term2);
};
}
/**
* Compute the image of a rectangular grid under a conformal mapping
* @param {Function} mapping - Complex function
* @param {number} xMin - Minimum x value
* @param {number} xMax - Maximum x value
* @param {number} yMin - Minimum y value
* @param {number} yMax - Maximum y value
* @param {number} xSteps - Number of x steps
* @param {number} ySteps - Number of y steps
* @returns {Object} - Grid lines in original and mapped domains
*/
function conformalGrid(mapping, xMin, xMax, yMin, yMax, xSteps = 10, ySteps = 10) {
const result = {
originalHorizontal: [],
originalVertical: [],
mappedHorizontal: [],
mappedVertical: []
};
// Create horizontal grid lines
for (let j = 0; j <= ySteps; j++) {
const y = yMin + (yMax - yMin) * j / ySteps;
const line = [];
const mappedLine = [];
for (let i = 0; i <= xSteps; i++) {
const x = xMin + (xMax - xMin) * i / xSteps;
const z = complex(x, y);
line.push(z);
mappedLine.push(mapping(z));
}
result.originalHorizontal.push(line);
result.mappedHorizontal.push(mappedLine);
}
// Create vertical grid lines
for (let i = 0; i <= xSteps; i++) {
const x = xMin + (xMax - xMin) * i / xSteps;
const line = [];
const mappedLine = [];
for (let j = 0; j <= ySteps; j++) {
const y = yMin + (yMax - yMin) * j / ySteps;
const z = complex(x, y);
line.push(z);
mappedLine.push(mapping(z));
}
result.originalVertical.push(line);
result.mappedVertical.push(mappedLine);
}
return result;
}
module.exports = {
complex,
complexFunction,
isAnalytic,
derivative,
findPolynomialZeros,
contourIntegral,
residue,
laurentSeries,
conformalMapping,
conformalMappings,
potentialFlowAroundCylinder,
conformalGrid
};