ts-quantum
Version:
TypeScript library for quantum mechanics calculations and utilities
541 lines • 23 kB
JavaScript
/**
* Quantum operator implementations using math.js for enhanced numerical stability
*/
import { StateVector } from '../states/stateVector';
import { validateMatDims, validateMatchDims, validatePartialTrace } from '../utils/validation';
import { eigenDecomposition } from '../utils/matrixOperations';
import { IdentityOperator, DiagonalOperator, isDiagonalMatrix } from './specialized';
import * as math from 'mathjs';
// Helper function to ensure math.js output is converted to Complex type
function ensureComplex(value) {
if (typeof value === 'number') {
return math.complex(value, 0);
}
if (math.typeOf(value) === 'Complex') {
return value;
}
if (typeof value === 'object' && value !== null && 're' in value && 'im' in value) {
return math.complex(value.re, value.im);
}
throw new Error(`Cannot convert ${math.typeOf(value)} to Complex`);
}
/**
* Creates a zero matrix of the specified dimension
* @param dimension The dimension of the square matrix
* @returns A dimension x dimension matrix filled with complex zeros
*/
export function createZeroMatrix(dimension) {
if (dimension <= 0 || !Number.isInteger(dimension)) {
throw new Error('Dimension must be a positive integer');
}
return Array(dimension).fill(null)
.map(() => Array(dimension).fill(null)
.map(() => math.complex(0, 0)));
}
/**
* Implementation of operator using matrix representation
*/
export class MatrixOperator {
objectType = 'operator'; // Discriminator property
dimension;
type;
matrix;
validateTypeConstraints;
constructor(matrix, type = 'general', validateTypeConstraints = true, additionalProps = {}) {
validateMatDims(matrix);
const dim = matrix.length;
// Validate operator type
if (type !== 'general' && type !== 'unitary' && type !== 'hermitian' && type !== 'projection') {
throw new Error('Invalid operator type');
}
this.dimension = dim;
this.type = type;
// Deep clone the matrix using math.js
this.matrix = matrix.map(row => row.map(elem => math.clone(elem)));
this.validateTypeConstraints = validateTypeConstraints;
// Add any additional properties
Object.assign(this, additionalProps);
// Validate type constraints only if requested
if (validateTypeConstraints) {
if (type === 'hermitian' && !this.isHermitian()) {
throw new Error('Matrix is not Hermitian');
}
else if (type === 'projection' && !this.isProjection()) {
throw new Error('Matrix is not a projection');
}
else if (type === 'unitary') {
// Direct unitary check without recursion
const adjointMatrix = Array(this.dimension).fill(null)
.map((_, i) => Array(this.dimension).fill(null)
.map((_, j) => math.conj(this.matrix[j][i])));
// Compute product manually without creating new operators
const productMatrix = Array(dim).fill(null)
.map(() => Array(dim).fill(null)
.map(() => math.complex()));
for (let i = 0; i < dim; i++) {
for (let j = 0; j < dim; j++) {
for (let k = 0; k < dim; k++) {
const term = math.multiply(this.matrix[i][k], adjointMatrix[k][j]);
productMatrix[i][j] = ensureComplex(math.add(productMatrix[i][j], term));
}
}
}
// Check if product is identity
for (let i = 0; i < dim; i++) {
for (let j = 0; j < dim; j++) {
const expected = i === j ? math.complex(1, 0) : math.complex(0, 0);
const diff = math.subtract(productMatrix[i][j], expected);
if (Number(math.abs(ensureComplex(diff))) > 1e-10) {
throw new Error('Matrix is not unitary');
}
}
}
}
}
}
// Add this method to the MatrixOperator class
/**
* Returns a string representation of the operator in matrix form
* @param precision Number of decimal places (default: 3)
*/
toString(precision = 3) {
const rows = this.matrix.map(row => {
return row.map(element => {
const re = element.re.toFixed(precision);
const im = element.im.toFixed(precision);
if (Math.abs(element.im) < Math.pow(10, -precision)) {
return `${re}`;
}
return `${re}${Number(im) >= 0 ? '+' : ''}${im}i`;
}).join('\t');
});
// Find the maximum width of any element for padding
const maxWidth = Math.max(...rows.map(row => Math.max(...row.split('\t').map(el => el.length))));
// Add padding and format with brackets
const paddedRows = rows.map(row => '│ ' + row.split('\t')
.map(el => el.padStart(maxWidth))
.join(' ') + ' │');
// Create top and bottom borders
const borderLine = '─'.repeat(maxWidth * this.dimension + 2 * this.dimension + 1);
const topBorder = `┌${borderLine}┐`;
const bottomBorder = `└${borderLine}┘`;
return [topBorder, ...paddedRows, bottomBorder].join('\n');
}
/**
* Applies operator to state vector: |ψ'⟩ = O|ψ⟩
*/
apply(state) {
validateMatchDims(state.dimension, this.dimension);
const newAmplitudes = new Array(this.dimension).fill(null)
.map(() => math.complex(0, 0));
// Apply matrix multiplication
for (let i = 0; i < this.dimension; i++) {
for (let j = 0; j < this.dimension; j++) {
const term = math.multiply(this.matrix[i][j], state.amplitudes[j]);
newAmplitudes[i] = ensureComplex(math.add(newAmplitudes[i], term));
}
}
// Find the index of the non-zero amplitude to determine basis state
const maxIndex = newAmplitudes.reduce((maxIdx, current, idx, arr) => {
const currentMag = math.abs(current);
const maxMag = math.abs(arr[maxIdx]);
return currentMag > maxMag ? idx : maxIdx;
}, 0);
// Determine new basis label based on the operation
let newBasis = state.basis;
if (this.dimension === 2) {
// Single qubit operations
if (maxIndex === 1) {
newBasis = '|1⟩';
}
else if (maxIndex === 0) {
newBasis = '|0⟩';
}
// Special case for Hadamard creating superposition
if (Math.abs(Math.abs(newAmplitudes[0].re) - 1 / Math.sqrt(2)) < 1e-10 &&
Math.abs(Math.abs(newAmplitudes[1].re) - 1 / Math.sqrt(2)) < 1e-10) {
newBasis = newAmplitudes[1].re > 0 ? '|+⟩' : '|-⟩';
}
}
else if (this.dimension === 4) {
// Two qubit operations
const binaryStr = maxIndex.toString(2).padStart(2, '0');
newBasis = `|${binaryStr}⟩`;
}
return new StateVector(this.dimension, newAmplitudes, newBasis);
}
/**
* Composes with another operator: O₁O₂
*/
compose(other) {
validateMatchDims(other.dimension, this.dimension);
// Get matrix representation of the other operator
const otherMatrix = other.toMatrix();
// Initialize result matrix with zeros
const resultMatrix = Array(this.dimension).fill(null)
.map(() => Array(this.dimension).fill(null)
.map(() => math.complex(0, 0)));
// Manual matrix multiplication with explicit complex number handling
for (let i = 0; i < this.dimension; i++) {
for (let j = 0; j < this.dimension; j++) {
let sum = math.complex(0, 0);
for (let k = 0; k < this.dimension; k++) {
// Ensure proper complex multiplication
const term = math.multiply(math.complex(this.matrix[i][k].re, this.matrix[i][k].im), math.complex(otherMatrix[k][j].re, otherMatrix[k][j].im));
sum = math.add(sum, term);
}
resultMatrix[i][j] = sum;
}
}
// Determine resulting operator type with proper inheritance
let resultType = 'general';
if (this.type === other.type) {
if (this.type === 'hermitian' || this.type === 'unitary' || this.type === 'projection') {
resultType = this.type;
}
}
else if (this.type === 'hermitian' && other.type === 'unitary') {
resultType = 'hermitian';
}
else if (this.type === 'unitary' && other.type === 'hermitian') {
resultType = 'hermitian';
}
return new MatrixOperator(resultMatrix, resultType);
}
/**
* Returns the adjoint (Hermitian conjugate) of the operator
*/
adjoint() {
// Initialize adjoint matrix with proper dimensions
const adjointMatrix = Array(this.dimension).fill(null)
.map(() => Array(this.dimension).fill(null)
.map(() => math.complex(0, 0)));
// Calculate adjoint elements with proper complex conjugate
for (let i = 0; i < this.dimension; i++) {
for (let j = 0; j < this.dimension; j++) {
// Take complex conjugate and transpose
const elem = this.matrix[j][i];
adjointMatrix[i][j] = math.complex(elem.re, -elem.im);
}
}
// Determine adjoint type
let adjointType = 'general';
if (this.type === 'unitary')
adjointType = 'unitary';
if (this.type === 'hermitian')
adjointType = 'hermitian';
if (this.type === 'projection')
adjointType = 'projection';
// Create adjoint without validation to prevent recursion
return new MatrixOperator(adjointMatrix, adjointType, false);
}
/**
* Returns matrix representation
*/
toMatrix() {
// Create new matrix with explicit positive zeros
return this.matrix.map(row => row.map(elem =>
// Ensure positive zero in real and imaginary parts
math.complex(elem.re === 0 ? 0 : elem.re, elem.im === 0 ? 0 : elem.im)));
}
/**
* Checks if matrix is Hermitian (self-adjoint)
*/
isHermitian(tolerance = 1e-10) {
// Only need to check upper triangle against lower triangle's conjugate
for (let i = 0; i < this.dimension; i++) {
// Check diagonal elements are real
if (Math.abs(this.matrix[i][i].im) > tolerance) {
return false;
}
// Check off-diagonal elements are conjugates
for (let j = i + 1; j < this.dimension; j++) {
const upper = this.matrix[i][j];
const lower = this.matrix[j][i];
// Check if upper[i][j] = conjugate(lower[j][i])
if (Math.abs(upper.re - lower.re) > tolerance ||
Math.abs(upper.im + lower.im) > tolerance) {
return false;
}
}
}
return true;
}
/**
* Checks if matrix is unitary
*/
isUnitary(tolerance = 1e-10) {
// Convert to math.js matrix
const matA = math.matrix(this.matrix);
// Compute U†U
const matAH = math.transpose(math.conj(matA));
const product = math.multiply(matA, matAH);
// Create identity matrix of same size
const identity = math.identity(this.dimension, 'dense');
// Subtract identity and check if difference is within tolerance
const diff = math.subtract(product, identity);
const maxDiffValue = math.max(math.abs(diff).valueOf());
return maxDiffValue < tolerance;
}
/**
* Checks if matrix is a projection operator (P² = P)
*/
isProjection(tolerance = 1e-10) {
// Convert to math.js matrix
const matP = math.matrix(this.matrix);
// Compute P²
const matP2 = math.multiply(matP, matP);
// Subtract P² - P and check if difference is within tolerance
const diff = math.subtract(matP2, matP);
const maxDiffValue = math.max(math.abs(diff).valueOf());
return maxDiffValue < tolerance;
}
/**
* Creates tensor product with another operator
*/
tensorProduct(other) {
const otherMatrix = other.toMatrix();
const newDim = this.dimension * other.dimension;
const resultMatrix = Array(newDim).fill(null)
.map(() => Array(newDim).fill(null)
.map(() => math.complex()));
// Compute tensor product matrix elements
for (let i1 = 0; i1 < this.dimension; i1++) {
for (let j1 = 0; j1 < this.dimension; j1++) {
for (let i2 = 0; i2 < other.dimension; i2++) {
for (let j2 = 0; j2 < other.dimension; j2++) {
const i = i1 * other.dimension + i2;
const j = j1 * other.dimension + j2;
resultMatrix[i][j] = ensureComplex(math.multiply(this.matrix[i1][j1], otherMatrix[i2][j2]));
}
}
}
}
// Determine resulting operator type
let resultType = 'general';
if (this.type === 'unitary' && other.type === 'unitary') {
resultType = 'unitary';
}
return new MatrixOperator(resultMatrix, resultType);
}
/**
* Creates the identity operator of given dimension
*/
static identity(dimension) {
return new IdentityOperator(dimension);
}
/**
* Creates a zero operator of given dimension
*/
static zero(dimension) {
const matrix = Array(dimension).fill(null)
.map(() => Array(dimension).fill(null)
.map(() => math.complex(0, 0)));
return new MatrixOperator(matrix);
}
/**
* Create optimized operator based on matrix structure
*/
static createOptimized(matrix, type) {
const dimension = matrix.length;
// Check for identity matrix
let isIdentity = true;
for (let i = 0; i < dimension && isIdentity; i++) {
for (let j = 0; j < dimension && isIdentity; j++) {
const expected = i === j ? 1 : 0;
const element = matrix[i][j];
if (Math.abs(element.re - expected) > 1e-12 || Math.abs(element.im) > 1e-12) {
isIdentity = false;
}
}
}
if (isIdentity) {
return new IdentityOperator(dimension);
}
// Check for diagonal matrix
if (isDiagonalMatrix(matrix)) {
const diagonal = matrix.map((row, i) => math.clone(row[i]));
return new DiagonalOperator(diagonal);
}
// Default to standard matrix operator
return new MatrixOperator(matrix, type);
}
/**
* Scales operator by a complex number
*/
scale(scalar) {
const scaledMatrix = this.matrix.map(row => row.map(elem => ensureComplex(math.multiply(elem, scalar))));
return new MatrixOperator(scaledMatrix);
}
/**
* Adds this operator with another operator
*/
add(other) {
validateMatchDims(other.dimension, this.dimension);
const otherMatrix = other.toMatrix();
const sumMatrix = Array(this.dimension).fill(null)
.map((_, i) => Array(this.dimension).fill(null)
.map((_, j) => math.add(math.complex(this.matrix[i][j].re, this.matrix[i][j].im), math.complex(otherMatrix[i][j].re, otherMatrix[i][j].im))));
return new MatrixOperator(sumMatrix);
}
/**
* Performs partial trace operation over specified quantum subsystems
*
* In quantum mechanics, the partial trace is an operation that reduces the dimensionality of
* a quantum system by "tracing out" (removing) certain subsystems. This is a fundamental
* operation used to obtain the reduced density matrix of a composite system.
*
* @example
* // For a 4-dimensional system (2⊗2) representing two qubits:
* // Get reduced density matrix by tracing out the second qubit
* const reducedOperator = operator.partialTrace([2, 2], [1]);
*
* @example
* // For an 8-dimensional system (2⊗2⊗2) representing three qubits:
* // Trace out the first and third qubits
* const reducedOperator = operator.partialTrace([2, 2, 2], [0, 2]);
*
* @param dims - Array of dimensions for each subsystem. Product must equal this.dimension
* @param traceOutIndices - Array of indices indicating which subsystems to trace out
* @returns A new operator representing the reduced system after partial trace
* @throws Error if dimensions are invalid or indices are out of bounds
*/
partialTrace(dims, traceOutIndices) {
// Use standardized validation
validatePartialTrace(dims, this.dimension, traceOutIndices);
// Calculate remaining dimension after trace
const remainingDim = dims.filter((_, i) => !traceOutIndices.includes(i))
.reduce((a, b) => a * b, 1);
// Initialize result matrix
const resultMatrix = createZeroMatrix(remainingDim);
// Array(remainingDim).fill(null)
// .map(() => Array(remainingDim).fill(null)
// .map(() => math.complex(0, 0)));
// Perform partial trace
const traceRange = Array(this.dimension).fill(0)
.map((_, i) => i);
// Implementation of partial trace operation
for (let i = 0; i < remainingDim; i++) {
for (let j = 0; j < remainingDim; j++) {
for (const k of traceRange) {
// Map indices to multi-dimensional coordinates
const iCoords = indexToCoords(i, dims.filter((_, idx) => !traceOutIndices.includes(idx)));
const jCoords = indexToCoords(j, dims.filter((_, idx) => !traceOutIndices.includes(idx)));
const kCoords = indexToCoords(k, dims.filter((_, idx) => traceOutIndices.includes(idx)));
// Combine coordinates
const fullICoords = combineCoords(iCoords, kCoords, traceOutIndices);
const fullJCoords = combineCoords(jCoords, kCoords, traceOutIndices);
// Map back to flat indices
const fullI = coordsToIndex(fullICoords, dims);
const fullJ = coordsToIndex(fullJCoords, dims);
// Add to result using math.js
resultMatrix[i][j] = ensureComplex(math.add(resultMatrix[i][j], this.matrix[fullI][fullJ]));
}
}
}
return new MatrixOperator(resultMatrix);
}
/**
* Returns eigenvalues and eigenvectors of the operator
* Only works for Hermitian operators
*/
eigenDecompose() {
const { values, vectors } = eigenDecomposition(this.matrix, { computeEigenvectors: true });
// Create operators from eigenvectors
if (!vectors || vectors.length === 0) {
throw new Error('No eigenvectors found');
}
const vectorOperators = vectors.map(v => {
// Create full matrix for the eigenvector operator
const matrix = Array(this.dimension).fill(null)
.map((_, i) => Array(this.dimension).fill(null)
.map((_, j) => i === j ? math.clone(v[i]) : math.complex(0, 0)));
return new MatrixOperator(matrix, 'general');
});
return {
values: values.map(v => math.clone(v)),
vectors: vectorOperators
};
}
/**
* Projects onto eigenspace with given eigenvalue
*/
projectOntoEigenspace(eigenvalue, tolerance = 1e-10) {
const { values, vectors } = this.eigenDecompose();
// Find eigenvectors for this eigenvalue
const matchingVectors = vectors.filter((_, i) => Math.abs(values[i].re - eigenvalue.re) < tolerance &&
Math.abs(values[i].im - eigenvalue.im) < tolerance);
if (matchingVectors.length === 0) {
throw new Error('No eigenvectors found for given eigenvalue');
}
// Construct projection operator
return matchingVectors.reduce((sum, vector) => {
const vectorMatrix = vector.toMatrix()[0]; // Get the first row since vector is 1xN matrix
const proj = new MatrixOperator([vectorMatrix], 'projection');
return sum ? sum.add(proj) : proj;
});
}
/**
* Calculates the operator norm (Frobenius norm)
*/
norm() {
let sum = 0;
for (let i = 0; i < this.dimension; i++) {
for (let j = 0; j < this.dimension; j++) {
const element = this.matrix[i][j];
sum += element.re * element.re + element.im * element.im;
}
}
return Math.sqrt(sum);
}
/**
* Tests whether the operator is identically zero
* @param tolerance Numerical tolerance for zero comparison (default: 1e-12)
* @returns true if all matrix elements are within tolerance of zero
*/
isZero(tolerance = 1e-12) {
for (let i = 0; i < this.dimension; i++) {
for (let j = 0; j < this.dimension; j++) {
const element = this.matrix[i][j];
if (Math.abs(element.re) > tolerance || Math.abs(element.im) > tolerance) {
return false;
}
}
}
return true;
}
}
// Helper functions for partial trace implementation
function indexToCoords(index, dims) {
const coords = [];
let remainder = index;
for (let i = dims.length - 1; i >= 0; i--) {
coords.unshift(remainder % dims[i]);
remainder = Math.floor(remainder / dims[i]);
}
return coords;
}
function coordsToIndex(coords, dims) {
let index = 0;
let factor = 1;
for (let i = coords.length - 1; i >= 0; i--) {
index += coords[i] * factor;
factor *= dims[i];
}
return index;
}
function combineCoords(coords1, coords2, traceIndices) {
const result = [];
let i1 = 0;
let i2 = 0;
for (let i = 0; i < coords1.length + coords2.length; i++) {
if (traceIndices.includes(i)) {
result.push(coords2[i2++]);
}
else {
result.push(coords1[i1++]);
}
}
return result;
}
//# sourceMappingURL=operator.js.map