Files

556 lines
19 KiB
JavaScript

const FLOAT64_BYTES = Float64Array.BYTES_PER_ELEMENT;
export const DEFAULT_HERMITIAN_JACOBI_MAX_SWEEPS = 32;
export const DEFAULT_MAX_HERMITIAN_EIGEN_WORK_BYTES = 16 * 1024 * 1024;
export const DEFAULT_MAX_HERMITIAN_JACOBI_PAIR_UPDATES = 1_000_000_000;
function assertPositiveInteger(value, label) {
if (!Number.isSafeInteger(value) || value < 1) {
throw new TypeError(`${label} must be a positive safe integer.`);
}
}
function assertMaximumSweeps(value) {
if (
!Number.isSafeInteger(value) ||
value < 0 ||
value > DEFAULT_HERMITIAN_JACOBI_MAX_SWEEPS
) {
throw new RangeError(
`maximumSweeps must be an integer from 0 through ${DEFAULT_HERMITIAN_JACOBI_MAX_SWEEPS}.`,
);
}
}
function checkedProduct(factors, label) {
let product = 1;
for (const factor of factors) {
if (factor !== 0 && product > Number.MAX_SAFE_INTEGER / factor) {
throw new RangeError(`${label} exceeds the safe integer range.`);
}
product *= factor;
}
return product;
}
function roundoffGamma(operationCount) {
const product = operationCount * (Number.EPSILON / 2);
if (!(product < 1)) {
throw new RangeError("roundoff bound is undefined for this workload.");
}
return product / (1 - product);
}
/**
* Estimate the bounded typed-array allocation and cyclic-Jacobi work before
* any decomposition workspace is allocated.
*/
export function estimateHermitianEigenWorkload({
order,
maximumSweeps = DEFAULT_HERMITIAN_JACOBI_MAX_SWEEPS,
}) {
assertPositiveInteger(order, "order");
assertMaximumSweeps(maximumSweeps);
const matrixElementCount = checkedProduct([order, order], "matrix element count");
const pairCount = checkedProduct([order, order - 1], "Jacobi pair count") / 2;
const worstCaseRotations = checkedProduct(
[pairCount, maximumSweeps],
"Jacobi rotation count",
);
// A rotation updates n-2 matrix row pairs and n eigenvector row pairs.
const pairUpdatesPerRotation = Math.max(0, 2 * order - 2);
const worstCasePairUpdates = checkedProduct(
[worstCaseRotations, pairUpdatesPerRotation],
"Jacobi pair-update count",
);
// Two split-complex matrices plus normalized and physical eigenvalue arrays.
const typedArrayElementCount = checkedProduct(
[4 * matrixElementCount + 2 * order, FLOAT64_BYTES],
"Hermitian eigensolver allocation",
) / FLOAT64_BYTES;
const allocationBytes = checkedProduct(
[typedArrayElementCount, FLOAT64_BYTES],
"Hermitian eigensolver allocation",
);
return Object.freeze({
order,
maximumSweeps,
matrixElementCount,
pairCount,
pairUpdatesPerRotation,
worstCaseRotations,
worstCasePairUpdates,
allocationBytes,
});
}
function inferOrder(real, imaginary, explicitOrder) {
if (real == null || imaginary == null) {
throw new TypeError("real and imaginary matrix components are required.");
}
const order = explicitOrder ?? Math.sqrt(real.length);
assertPositiveInteger(order, "order");
const expectedLength = checkedProduct([order, order], "matrix element count");
if (real.length !== expectedLength || imaginary.length !== expectedLength) {
throw new RangeError(
`Hermitian matrix components must each contain ${expectedLength} values.`,
);
}
return order;
}
function matrixFrobeniusNorm(real, imaginary, order) {
let sum = 0;
for (let index = 0; index < order * order; index += 1) {
sum += real[index] * real[index] + imaginary[index] * imaginary[index];
}
return Math.sqrt(sum);
}
function offDiagonalFrobeniusNorm(real, imaginary, order) {
let upperTriangleSum = 0;
for (let row = 0; row < order; row += 1) {
for (let column = row + 1; column < order; column += 1) {
const index = row * order + column;
upperTriangleSum +=
real[index] * real[index] + imaginary[index] * imaginary[index];
}
}
return Math.sqrt(2 * upperTriangleSum);
}
function rotatePair(matrixReal, matrixImaginary, vectorsReal, vectorsImaginary, order, p, q) {
const pq = p * order + q;
const offReal = matrixReal[pq];
const offImaginary = matrixImaginary[pq];
const magnitude = Math.hypot(offReal, offImaginary);
if (magnitude === 0) return false;
const diagonalP = matrixReal[p * order + p];
const diagonalQ = matrixReal[q * order + q];
const halfDifference = (diagonalQ - diagonalP) / 2;
const tangent = halfDifference === 0
? 1
: Math.sign(halfDifference) * magnitude /
(Math.abs(halfDifference) + Math.hypot(halfDifference, magnitude));
const cosine = 1 / Math.hypot(1, tangent);
const sine = tangent * cosine;
const phaseReal = offReal / magnitude;
const phaseImaginary = offImaginary / magnitude;
for (let row = 0; row < order; row += 1) {
if (row === p || row === q) continue;
const rowP = row * order + p;
const rowQ = row * order + q;
const pReal = matrixReal[rowP];
const pImaginary = matrixImaginary[rowP];
const qReal = matrixReal[rowQ];
const qImaginary = matrixImaginary[rowQ];
const conjugatePhaseTimesQReal =
phaseReal * qReal + phaseImaginary * qImaginary;
const conjugatePhaseTimesQImaginary =
phaseReal * qImaginary - phaseImaginary * qReal;
const phaseTimesPReal = phaseReal * pReal - phaseImaginary * pImaginary;
const phaseTimesPImaginary = phaseReal * pImaginary + phaseImaginary * pReal;
const nextPReal = cosine * pReal - sine * conjugatePhaseTimesQReal;
const nextPImaginary = cosine * pImaginary - sine * conjugatePhaseTimesQImaginary;
const nextQReal = sine * phaseTimesPReal + cosine * qReal;
const nextQImaginary = sine * phaseTimesPImaginary + cosine * qImaginary;
matrixReal[rowP] = nextPReal;
matrixImaginary[rowP] = nextPImaginary;
matrixReal[p * order + row] = nextPReal;
matrixImaginary[p * order + row] = -nextPImaginary;
matrixReal[rowQ] = nextQReal;
matrixImaginary[rowQ] = nextQImaginary;
matrixReal[q * order + row] = nextQReal;
matrixImaginary[q * order + row] = -nextQImaginary;
}
matrixReal[p * order + p] = diagonalP - tangent * magnitude;
matrixImaginary[p * order + p] = 0;
matrixReal[q * order + q] = diagonalQ + tangent * magnitude;
matrixImaginary[q * order + q] = 0;
matrixReal[pq] = 0;
matrixImaginary[pq] = 0;
matrixReal[q * order + p] = 0;
matrixImaginary[q * order + p] = 0;
for (let row = 0; row < order; row += 1) {
const rowP = row * order + p;
const rowQ = row * order + q;
const pReal = vectorsReal[rowP];
const pImaginary = vectorsImaginary[rowP];
const qReal = vectorsReal[rowQ];
const qImaginary = vectorsImaginary[rowQ];
const conjugatePhaseTimesQReal =
phaseReal * qReal + phaseImaginary * qImaginary;
const conjugatePhaseTimesQImaginary =
phaseReal * qImaginary - phaseImaginary * qReal;
const phaseTimesPReal = phaseReal * pReal - phaseImaginary * pImaginary;
const phaseTimesPImaginary = phaseReal * pImaginary + phaseImaginary * pReal;
vectorsReal[rowP] = cosine * pReal - sine * conjugatePhaseTimesQReal;
vectorsImaginary[rowP] = cosine * pImaginary - sine * conjugatePhaseTimesQImaginary;
vectorsReal[rowQ] = sine * phaseTimesPReal + cosine * qReal;
vectorsImaginary[rowQ] = sine * phaseTimesPImaginary + cosine * qImaginary;
}
return true;
}
function canonicalizeColumnPhases(real, imaginary, order) {
for (let column = 0; column < order; column += 1) {
let pivotRow = 0;
let pivotMagnitude = -1;
for (let row = 0; row < order; row += 1) {
const index = row * order + column;
const magnitude = Math.hypot(real[index], imaginary[index]);
if (magnitude > pivotMagnitude) {
pivotMagnitude = magnitude;
pivotRow = row;
}
}
const pivotIndex = pivotRow * order + column;
const pivotReal = real[pivotIndex];
const pivotImaginary = imaginary[pivotIndex];
const normalizedPivotMagnitude = Math.hypot(pivotReal, pivotImaginary);
if (!(normalizedPivotMagnitude > 0) || !Number.isFinite(normalizedPivotMagnitude)) {
throw new RangeError("Jacobi eigenvector phase canonicalization failed.");
}
const phaseReal = pivotReal / normalizedPivotMagnitude;
const phaseImaginary = -pivotImaginary / normalizedPivotMagnitude;
for (let row = 0; row < order; row += 1) {
const index = row * order + column;
const valueReal = real[index];
const valueImaginary = imaginary[index];
real[index] = valueReal * phaseReal - valueImaginary * phaseImaginary;
imaginary[index] = valueReal * phaseImaginary + valueImaginary * phaseReal;
}
real[pivotIndex] = normalizedPivotMagnitude;
imaginary[pivotIndex] = 0;
}
}
function projectedInputEntry(real, imaginary, order, row, column, scale) {
if (row === column) return [real[row * order + row] / scale, 0];
const forward = row * order + column;
const reverse = column * order + row;
return [
(real[forward] + real[reverse]) / (2 * scale),
(imaginary[forward] - imaginary[reverse]) / (2 * scale),
];
}
function residualFrobeniusNorm(
inputReal,
inputImaginary,
vectorsReal,
vectorsImaginary,
normalizedEigenvalues,
order,
scale,
) {
if (scale === 0) return 0;
let residualSquared = 0;
for (let column = 0; column < order; column += 1) {
const eigenvalue = normalizedEigenvalues[column];
for (let row = 0; row < order; row += 1) {
let productReal = 0;
let productImaginary = 0;
for (let inner = 0; inner < order; inner += 1) {
const [matrixReal, matrixImaginary] = projectedInputEntry(
inputReal,
inputImaginary,
order,
row,
inner,
scale,
);
const vectorIndex = inner * order + column;
const vectorReal = vectorsReal[vectorIndex];
const vectorImaginary = vectorsImaginary[vectorIndex];
productReal += matrixReal * vectorReal - matrixImaginary * vectorImaginary;
productImaginary += matrixReal * vectorImaginary + matrixImaginary * vectorReal;
}
const vectorIndex = row * order + column;
const differenceReal = productReal - eigenvalue * vectorsReal[vectorIndex];
const differenceImaginary =
productImaginary - eigenvalue * vectorsImaginary[vectorIndex];
residualSquared +=
differenceReal * differenceReal + differenceImaginary * differenceImaginary;
}
}
return Math.sqrt(residualSquared);
}
function orthogonalityError(real, imaginary, order) {
let errorSquared = 0;
for (let left = 0; left < order; left += 1) {
for (let right = 0; right < order; right += 1) {
let productReal = 0;
let productImaginary = 0;
for (let row = 0; row < order; row += 1) {
const leftIndex = row * order + left;
const rightIndex = row * order + right;
productReal +=
real[leftIndex] * real[rightIndex] +
imaginary[leftIndex] * imaginary[rightIndex];
productImaginary +=
real[leftIndex] * imaginary[rightIndex] -
imaginary[leftIndex] * real[rightIndex];
}
if (left === right) productReal -= 1;
errorSquared += productReal * productReal + productImaginary * productImaginary;
}
}
return Math.sqrt(errorSquared);
}
/**
* Deterministic full cyclic Jacobi decomposition of a complex Hermitian
* matrix. Split-complex matrices are row-major; eigenvectors are returned as
* row-major columns, so component `row` of eigenvector `column` is at
* `row * order + column`.
*
* The input is projected to its nearest Hermitian matrix only after its
* anti-Hermitian discrepancy has passed the reported roundoff-scale audit.
* No diagonal loading or eigenvalue flooring is performed.
*/
export function decomposeHermitian({
real,
imaginary,
order: explicitOrder,
maximumSweeps = DEFAULT_HERMITIAN_JACOBI_MAX_SWEEPS,
relativeTolerance,
maximumWorkBytes = DEFAULT_MAX_HERMITIAN_EIGEN_WORK_BYTES,
maximumPairUpdates = DEFAULT_MAX_HERMITIAN_JACOBI_PAIR_UPDATES,
}) {
const order = inferOrder(real, imaginary, explicitOrder);
const workload = estimateHermitianEigenWorkload({ order, maximumSweeps });
if (!(maximumWorkBytes >= 0) || !Number.isFinite(maximumWorkBytes)) {
throw new TypeError("maximumWorkBytes must be finite and nonnegative.");
}
if (!(maximumPairUpdates >= 0) || !Number.isFinite(maximumPairUpdates)) {
throw new TypeError("maximumPairUpdates must be finite and nonnegative.");
}
if (workload.allocationBytes > maximumWorkBytes) {
throw new RangeError(
`Hermitian eigensolver requires ${workload.allocationBytes} typed-array bytes; limit is ${maximumWorkBytes}.`,
);
}
if (workload.worstCasePairUpdates > maximumPairUpdates) {
throw new RangeError(
`Hermitian eigensolver worst-case work is ${workload.worstCasePairUpdates} pair updates; limit is ${maximumPairUpdates}.`,
);
}
let scale = 0;
for (let index = 0; index < workload.matrixElementCount; index += 1) {
const valueReal = real[index];
const valueImaginary = imaginary[index];
if (!Number.isFinite(valueReal) || !Number.isFinite(valueImaginary)) {
throw new TypeError("Hermitian matrix components must be finite.");
}
// Component scaling avoids overflow in hypot() for otherwise finite
// split-complex entries near Number.MAX_VALUE.
scale = Math.max(scale, Math.abs(valueReal), Math.abs(valueImaginary));
}
const inputHermitianTolerance =
scale === 0 ? 0 : 32 * Number.EPSILON * Math.max(1, order) * scale;
let inputHermitianError = 0;
for (let row = 0; row < order; row += 1) {
inputHermitianError = Math.max(
inputHermitianError,
Math.abs(imaginary[row * order + row]),
);
for (let column = row + 1; column < order; column += 1) {
const forward = row * order + column;
const reverse = column * order + row;
inputHermitianError = Math.max(
inputHermitianError,
Math.hypot(
real[forward] - real[reverse],
imaginary[forward] + imaginary[reverse],
),
);
}
}
if (inputHermitianError > inputHermitianTolerance) {
throw new RangeError(
`matrix is not Hermitian within roundoff: error ${inputHermitianError}, tolerance ${inputHermitianTolerance}.`,
);
}
const matrixReal = new Float64Array(workload.matrixElementCount);
const matrixImaginary = new Float64Array(workload.matrixElementCount);
const vectorsReal = new Float64Array(workload.matrixElementCount);
const vectorsImaginary = new Float64Array(workload.matrixElementCount);
const normalizedEigenvalues = new Float64Array(order);
const eigenvalues = new Float64Array(order);
if (scale > 0) {
for (let row = 0; row < order; row += 1) {
const diagonal = row * order + row;
matrixReal[diagonal] = real[diagonal] / scale;
for (let column = row + 1; column < order; column += 1) {
const forward = row * order + column;
const reverse = column * order + row;
const projectedReal = (real[forward] + real[reverse]) / (2 * scale);
const projectedImaginary =
(imaginary[forward] - imaginary[reverse]) / (2 * scale);
matrixReal[forward] = projectedReal;
matrixImaginary[forward] = projectedImaginary;
matrixReal[reverse] = projectedReal;
matrixImaginary[reverse] = -projectedImaginary;
}
}
}
for (let index = 0; index < order; index += 1) {
vectorsReal[index * order + index] = 1;
}
const matrixNormNormalized = matrixFrobeniusNorm(
matrixReal,
matrixImaginary,
order,
);
const effectiveRelativeTolerance = relativeTolerance ?? roundoffGamma(32 * order);
if (!(effectiveRelativeTolerance > 0) || !Number.isFinite(effectiveRelativeTolerance)) {
throw new TypeError("relativeTolerance must be finite and positive.");
}
const absoluteTolerance = effectiveRelativeTolerance * matrixNormNormalized;
let offDiagonalNormalized = offDiagonalFrobeniusNorm(
matrixReal,
matrixImaginary,
order,
);
let converged = offDiagonalNormalized <= absoluteTolerance;
let sweeps = 0;
let rotations = 0;
const pairTolerance = workload.pairCount === 0
? 0
: absoluteTolerance / Math.sqrt(2 * workload.pairCount);
while (!converged && sweeps < maximumSweeps) {
let sweepRotations = 0;
for (let p = 0; p < order; p += 1) {
for (let q = p + 1; q < order; q += 1) {
const pq = p * order + q;
if (Math.hypot(matrixReal[pq], matrixImaginary[pq]) <= pairTolerance) {
continue;
}
if (
rotatePair(
matrixReal,
matrixImaginary,
vectorsReal,
vectorsImaginary,
order,
p,
q,
)
) {
sweepRotations += 1;
}
}
}
rotations += sweepRotations;
sweeps += 1;
offDiagonalNormalized = offDiagonalFrobeniusNorm(
matrixReal,
matrixImaginary,
order,
);
converged = offDiagonalNormalized <= absoluteTolerance;
if (sweepRotations === 0) break;
}
const stableOrder = Array.from({ length: order }, (_, index) => ({
index,
value: matrixReal[index * order + index],
}));
stableOrder.sort((left, right) => {
if (left.value > right.value) return -1;
if (left.value < right.value) return 1;
return left.index - right.index;
});
for (let column = 0; column < order; column += 1) {
const sourceColumn = stableOrder[column].index;
const normalizedEigenvalue = stableOrder[column].value;
normalizedEigenvalues[column] = Object.is(normalizedEigenvalue, -0)
? 0
: normalizedEigenvalue;
const physicalEigenvalue = normalizedEigenvalues[column] * scale;
if (!Number.isFinite(physicalEigenvalue)) {
throw new RangeError("Hermitian eigenvalue exceeds the finite Float64 range.");
}
eigenvalues[column] = Object.is(physicalEigenvalue, -0) ? 0 : physicalEigenvalue;
for (let row = 0; row < order; row += 1) {
const destination = row * order + column;
const source = row * order + sourceColumn;
matrixReal[destination] = vectorsReal[source];
matrixImaginary[destination] = vectorsImaginary[source];
}
}
// A product of unitary Jacobi rotations already has unit-norm columns.
// Canonicalize phase only; do not hide orthogonality drift by renormalizing.
canonicalizeColumnPhases(matrixReal, matrixImaginary, order);
const residualNormalized = residualFrobeniusNorm(
real,
imaginary,
matrixReal,
matrixImaginary,
normalizedEigenvalues,
order,
scale,
);
const relativeResidual = matrixNormNormalized === 0
? 0
: residualNormalized / matrixNormNormalized;
const relativeOffDiagonal = matrixNormNormalized === 0
? 0
: offDiagonalNormalized / matrixNormNormalized;
const residualTolerance = roundoffGamma(128 * order);
const orthogonalityTolerance = residualTolerance * Math.sqrt(order);
const measuredOrthogonalityError = orthogonalityError(
matrixReal,
matrixImaginary,
order,
);
const numericallyVerified =
converged &&
relativeResidual <= residualTolerance &&
measuredOrthogonalityError <= orthogonalityTolerance;
return {
order,
eigenvalues,
eigenvectors: {
real: matrixReal,
imaginary: matrixImaginary,
layout: "row-major-columns",
},
converged,
sweeps,
rotations,
scale,
inputHermitianError,
inputHermitianTolerance,
relativeTolerance: effectiveRelativeTolerance,
offDiagonalFrobeniusNorm: offDiagonalNormalized * scale,
relativeOffDiagonal,
residualFrobeniusNorm: residualNormalized * scale,
relativeResidual,
residualTolerance,
orthogonalityError: measuredOrthogonalityError,
orthogonalityTolerance,
numericallyVerified,
workload,
};
}