ts-quantum
Version:
TypeScript library for quantum mechanics calculations and utilities
408 lines • 15.9 kB
JavaScript
/**
* Angular momentum composition implementation
* Includes Clebsch-Gordan coefficients and angular momentum addition
*/
import { StateVector } from '../states/stateVector';
import { validateJ, isValidM } from './core';
import { logFactorial } from '../utils/math';
import * as math from 'mathjs';
// Cache for computed coefficients (sparse map)
const cgCache = new Map();
/**
* Helper to create a key for the sparse map
*/
function cgKey(j1, m1, j2, m2, j, m) {
return `${j1},${m1},${j2},${m2},${j},${m}`;
}
/**
* Validates angular momentum quantum numbers for Clebsch-Gordan coefficient
*
* @param j1 First angular momentum
* @param m1 Magnetic quantum number for j1
* @param j2 Second angular momentum
* @param m2 Magnetic quantum number for j2
* @param j Total angular momentum
* @param m Total magnetic quantum number
* @throws Error if constraints are violated
*/
function validateAngularMomentum(j1, m1, j2, m2, j, m) {
// Validate each individual angular momentum
validateJ(j1);
validateJ(j2);
validateJ(j);
// Validate magnetic quantum numbers
if (!isValidM(j1, m1)) {
throw new Error(`Invalid m1=${m1} for j1=${j1}`);
}
if (!isValidM(j2, m2)) {
throw new Error(`Invalid m2=${m2} for j2=${j2}`);
}
if (!isValidM(j, m)) {
throw new Error(`Invalid m=${m} for j=${j}`);
}
// Triangle inequality: |j1-j2| ≤ j ≤ j1+j2
if (j < Math.abs(j1 - j2) || j > j1 + j2) {
throw new Error(`Total angular momentum j=${j} must satisfy |j1-j2| ≤ j ≤ j1+j2, where j1=${j1}, j2=${j2}`);
}
// m = m1 + m2 (conservation of angular momentum)
if (Math.abs(m - (m1 + m2)) > 1e-10) {
throw new Error(`m=${m} must equal m1+m2=${m1}+${m2}=${m1 + m2}`);
}
}
/**
* Checks if a Clebsch-Gordan coefficient is zero based on selection rules
*
* @param j1 First angular momentum
* @param m1 Magnetic quantum number for j1
* @param j2 Second angular momentum
* @param m2 Magnetic quantum number for j2
* @param j Total angular momentum
* @param m Total magnetic quantum number
* @returns true if coefficient is zero
*/
function isZeroCG(j1, m1, j2, m2, j, m) {
// Check the three selection rules for zero coefficients
// 1. m ≠ m1 + m2 (magnetic quantum number conservation)
if (Math.abs(m - (m1 + m2)) > 1e-10) {
return true;
}
// 2. j > j1 + j2 (triangle inequality upper bound)
if (j > j1 + j2) {
return true;
}
// 3. j < |j1 - j2| (triangle inequality lower bound)
if (j < Math.abs(j1 - j2)) {
return true;
}
// Also check if individual m values are valid
if (Math.abs(m1) > j1 || Math.abs(m2) > j2 || Math.abs(m) > j) {
return true;
}
return false;
}
/**
* Calculates a single Clebsch-Gordan coefficient
*
* @param j1 First angular momentum
* @param m1 Magnetic quantum number for j1
* @param j2 Second angular momentum
* @param m2 Magnetic quantum number for j2
* @param j Total angular momentum
* @param m Total magnetic quantum number
* @returns Complex number representing the coefficient
*/
function clebschGordan(j1, m1, j2, m2, j, m) {
try {
validateAngularMomentum(j1, m1, j2, m2, j, m);
}
catch (error) {
return math.complex(0, 0);
}
if (isZeroCG(j1, m1, j2, m2, j, m)) {
return math.complex(0, 0);
}
// if (Math.abs(j1 - 0.5) < 1e-10 && Math.abs(j2 - 0.5) < 1e-10) {
// return clebschGordanSpinHalf(j1, m1, j2, m2, j, m);
// }
const cacheKey = `${j1},${j2}`;
let table;
if (cgCache.has(cacheKey)) {
table = cgCache.get(cacheKey);
}
else {
table = generateCGSparseMap(j1, j2);
cgCache.set(cacheKey, table);
}
const key = cgKey(j1, m1, j2, m2, j, m);
if (table.has(key)) {
return math.complex(table.get(key), 0);
}
return math.complex(0, 0);
}
/**
* Generates a sparse map of Clebsch-Gordan coefficients for given j1, j2
*/
function generateCGSparseMap(j1, j2) {
const map = new Map();
const jMin = Math.abs(j1 - j2);
const jMax = j1 + j2;
for (let j = jMin; j <= jMax; j++) {
for (let m = -j; m <= j; m++) {
for (let m1 = -j1; m1 <= j1; m1++) {
const m2 = m - m1;
if (m2 < -j2 || m2 > j2)
continue;
if (isZeroCG(j1, m1, j2, m2, j, m))
continue;
// Use the existing recursive or special-case logic
let coeff = generateCGTableCoeff(j1, m1, j2, m2, j, m);
if (Math.abs(coeff) > 1e-12) {
map.set(cgKey(j1, m1, j2, m2, j, m), coeff);
}
}
}
}
return map;
}
function generateCGTableCoeff(j1, m1, j2, m2, j, m) {
// We already have selection rules checks in isZeroCG, but let's add a safety check
if (isZeroCG(j1, m1, j2, m2, j, m)) {
return 0;
}
// Special case for j=0 coupling of two spin-1/2 particles
if (Math.abs(j1 - 0.5) < 1e-10 && Math.abs(j2 - 0.5) < 1e-10 && Math.abs(j) < 1e-10) {
// For singlet state, coefficient is -1/√2 for m1=1/2,m2=-1/2 and +1/√2 for m1=-1/2,m2=1/2
if (Math.abs(m1 - 0.5) < 1e-10 && Math.abs(m2 + 0.5) < 1e-10) {
return -1 / Math.sqrt(2);
}
if (Math.abs(m1 + 0.5) < 1e-10 && Math.abs(m2 - 0.5) < 1e-10) {
return 1 / Math.sqrt(2);
}
}
// Special case for maximum m value where coefficient should be exactly 1
if (Math.abs(m - (j1 + j2)) < 1e-10 && Math.abs(m1 - j1) < 1e-10 && Math.abs(m2 - j2) < 1e-10) {
return 1;
}
// Calculate log of terms to avoid overflow
const logPrefactor = Math.log(2 * j + 1);
// Log of delta (triangle condition)
const logDelta = 0.5 * (logFactorial(Math.round(j + j1 - j2)) +
logFactorial(Math.round(j - j1 + j2)) +
logFactorial(Math.round(j1 + j2 - j)) -
logFactorial(Math.round(j1 + j2 + j + 1)));
// Log of term1
const logTerm1 = 0.5 * (logFactorial(Math.round(j + m)) +
logFactorial(Math.round(j - m)) +
logFactorial(Math.round(j1 - m1)) +
logFactorial(Math.round(j1 + m1)) +
logFactorial(Math.round(j2 - m2)) +
logFactorial(Math.round(j2 + m2)));
// Calculate the sum term (Racah formula)
let sum = 0;
const kMin = Math.max(0, j2 - j - m1, j1 - j + m2);
const kMax = Math.min(j1 + j2 - j, j1 - m1, j2 + m2);
for (let k = Math.ceil(kMin); k <= Math.floor(kMax); k++) {
const sign = (k % 2 === 0) ? 1 : -1;
// Calculate log of all 6 denominator terms
const logDenominatorTerms = logFactorial(k) +
logFactorial(Math.round(j1 + j2 - j - k)) +
logFactorial(Math.round(j1 - m1 - k)) +
logFactorial(Math.round(j2 + m2 - k)) +
logFactorial(Math.round(j - j1 + m2 + k)) +
logFactorial(Math.round(j - j2 - m1 + k));
// Add the term using exp(log) to avoid overflow
const termValue = Math.exp(-logDenominatorTerms); // It's 1/denominator
if (!isNaN(termValue) && isFinite(termValue)) {
sum += sign * termValue;
}
}
const result = Math.exp(0.5 * logPrefactor + logDelta + logTerm1) * sum;
return result;
}
/**
* Combines two quantum states using Clebsch-Gordan coefficients
*
* @param state1 First quantum state
* @param j1 Angular momentum of first state
* @param state2 Second quantum state
* @param j2 Angular momentum of second state
* @returns Combined quantum state
*/
function addAngularMomenta(state1, j1, state2, j2) {
// Check dimensions
const dim1 = Math.floor(2 * j1 + 1);
const dim2 = Math.floor(2 * j2 + 1);
if (state1.dimension !== dim1) {
throw new Error(`State 1 dimension ${state1.dimension} does not match angular momentum j1=${j1} (expected ${dim1})`);
}
if (state2.dimension !== dim2) {
throw new Error(`State 2 dimension ${state2.dimension} does not match angular momentum j2=${j2} (expected ${dim2})`);
}
// Total angular momentum can range from |j1-j2| to j1+j2
const jMin = Math.abs(j1 - j2);
const jMax = j1 + j2;
// Initialize the result as an empty object
const resultStates = {};
// For each possible value of j
for (let j = jMin; j <= jMax; j++) {
resultStates[j.toString()] = {};
// For each possible value of m
for (let m = -j; m <= j; m++) {
resultStates[j.toString()][m.toString()] = math.complex(0, 0);
// For each possible value of m1 and m2
for (let m1 = -j1; m1 <= j1; m1++) {
const m2 = m - m1;
// Skip invalid m2 values
if (m2 < -j2 || m2 > j2) {
continue;
}
// Get the Clebsch-Gordan coefficient
const cg = clebschGordan(j1, m1, j2, m2, j, m);
// Skip zero coefficients
if (math.abs(cg.re ?? cg) < 1e-10) {
continue;
}
// Get the amplitude of the |j1,m1⟩ state
const dim1 = Math.floor(2 * j1 + 1);
const idx1 = dim1 - 1 - Math.floor(j1 + m1);
const amp1 = state1.amplitudes[idx1];
// Get the amplitude of the |j2,m2⟩ state
const dim2 = Math.floor(2 * j2 + 1);
const idx2 = dim2 - 1 - Math.floor(j2 + m2);
const amp2 = state2.amplitudes[idx2];
// Multiply by the coefficient and add to the result
const term = math.multiply(math.multiply(amp1, amp2), cg);
resultStates[j.toString()][m.toString()] = math.add(resultStates[j.toString()][m.toString()], term);
}
}
}
// Convert to a single state vector
const totalDim = Math.floor(Math.pow(j1 + j2 + 1, 2) - Math.pow(Math.abs(j1 - j2), 2));
const resultAmplitudes = [];
// Find the dominant j and m values from the state amplitudes
let maxJ = jMin;
let maxM = -maxJ;
let maxAmplitude = 0;
// First pass: find the maximum amplitude and its j,m values
for (let j = jMax; j >= jMin; j--) {
for (let m = j; m >= -j; m--) {
const amp = resultStates[j.toString()][m.toString()];
const magnitude = math.abs(amp.re ?? amp);
if (magnitude > maxAmplitude) {
maxAmplitude = magnitude;
maxJ = j;
maxM = m;
}
}
}
// Second pass: build the state vector in order of decreasing j and decreasing m
for (let j = jMax; j >= jMin; j--) {
for (let m = j; m >= -j; m--) {
const amp = resultStates[j.toString()][m.toString()];
if (math.abs(amp.re ?? amp) > 1e-12 || math.abs(amp.im ?? 0) > 1e-12) {
resultAmplitudes.push(amp);
}
else {
resultAmplitudes.push(math.complex(0, 0));
}
}
}
// Create basis label with actual j,m values
const basisLabel = `|(${j1},${j2}),${maxJ},${maxM}⟩`;
// Create the result state and normalize it
const unnormalizedResult = new StateVector(totalDim, resultAmplitudes, basisLabel);
const result = unnormalizedResult.normalize();
// Get coupling history from input states
const history1 = state1.getAngularMomentumMetadata()?.couplingHistory || [];
const history2 = state2.getAngularMomentumMetadata()?.couplingHistory || [];
// Calculate J component layout based on ACTUAL non-zero amplitudes
const jComponents = new Map();
let amplitudeIndex = 0;
// Build metadata only for J components that actually have non-zero amplitudes
for (let j = jMax; j >= jMin; j -= 0.5) {
const dimension = Math.floor(2 * j + 1);
// Check if this J component has any non-zero amplitudes
let hasNonZeroAmplitudes = false;
for (let mIndex = 0; mIndex < dimension; mIndex++) {
if (amplitudeIndex + mIndex < resultAmplitudes.length) {
const amp = resultAmplitudes[amplitudeIndex + mIndex];
if (math.abs(amp.re ?? amp) > 1e-12 || math.abs(amp.im ?? 0) > 1e-12) {
hasNonZeroAmplitudes = true;
break;
}
}
}
// Only add to metadata if component exists
if (hasNonZeroAmplitudes) {
jComponents.set(j, {
j: j,
startIndex: amplitudeIndex,
dimension: dimension,
normalizationFactor: 1
});
}
amplitudeIndex += dimension;
}
// Create comprehensive angular momentum metadata
const metadata = {
type: 'angular_momentum',
j: jMax, // Maximum possible J
mRange: [-jMax, jMax],
couplingHistory: [
...history1,
...history2,
{
operation: 'coupling',
j1: j1,
j2: j2,
resultJ: Array.from({ length: Math.floor(2 * (jMax - jMin) + 1) }, (_, i) => jMin + i * 0.5),
timestamp: Date.now()
}
],
jComponents: jComponents,
isComposite: true
};
result.setAngularMomentumMetadata(metadata);
return result;
}
/**
* Decomposes a coupled state into uncoupled basis states
*
* @param state Coupled quantum state
* @param j1 First angular momentum
* @param j2 Second angular momentum
* @returns Map of uncoupled states
*/
function decomposeAngularState(state, j1, j2) {
// Decompose a coupled state into uncoupled basis states
const result = new Map();
// Calculate dimensions
const dim1 = Math.floor(2 * j1 + 1);
const dim2 = Math.floor(2 * j2 + 1);
const totalDim = dim1 * dim2;
// Initialize the uncoupled state amplitudes
const uncoupledAmplitudes = new Array(totalDim).fill(null).map(() => math.complex(0, 0));
// Total angular momentum can range from |j1-j2| to j1+j2
const jMin = Math.abs(j1 - j2);
const jMax = j1 + j2;
// For each possible value of j
let stateIndex = 0;
for (let j = jMax; j >= jMin; j--) {
// For each possible value of m
for (let m = j; m >= -j; m--) {
// Get the amplitude for the |j,m⟩ state
const amp = state.amplitudes[stateIndex++];
// Skip zero amplitudes
if (math.abs(amp.re ?? amp) < 1e-10) {
continue;
}
// Decompose this state into uncoupled basis
for (let m1 = -j1; m1 <= j1; m1++) {
const m2 = m - m1;
// Skip invalid m2 values
if (m2 < -j2 || m2 > j2) {
continue;
}
// Get the Clebsch-Gordan coefficient
const cg = clebschGordan(j1, m1, j2, m2, j, m);
// Skip zero coefficients
if (math.abs(cg.re ?? cg) < 1e-10) {
continue;
}
// Calculate index in uncoupled basis
const idx1 = dim1 - 1 - Math.floor(j1 + m1);
const idx2 = dim2 - 1 - Math.floor(j2 + m2);
const uncoupledIdx = idx1 * dim2 + idx2;
// Add contribution to the uncoupled state
uncoupledAmplitudes[uncoupledIdx] = math.add(uncoupledAmplitudes[uncoupledIdx], math.multiply(amp, cg));
}
}
}
// Create the uncoupled state
const uncoupledState = new StateVector(totalDim, uncoupledAmplitudes, `|j1,m1⟩|j2,m2⟩`);
result.set('uncoupled', uncoupledState);
return result;
}
// Export the public API
export { clebschGordan, addAngularMomenta, decomposeAngularState, validateAngularMomentum, isZeroCG };
//# sourceMappingURL=composition.js.map