/** * Particle Sampler * Generates particle positions using CDF sampling of quantum probability distributions * Based on the approach from https://github.com/kavan010/Atoms */ class ParticleSampler { constructor(quantumEngine) { this.quantumEngine = quantumEngine; // Pre-calculated particle cache for common orbitals // Stores normalized particle positions (0-1 range) that can be scaled this.particleCache = null; this.initializeCache(); } /** * Initialize pre-calculated particle cache for common orbitals * This significantly speeds up loading for frequently used orbitals */ initializeCache() { console.log('Initializing particle cache for common orbitals...'); // Pre-generate particles for common orbitals with a base count const baseCount = 10000; // Base particle count for cache this.particleCache = { // 1s orbital (most common) '1_0_0': this.generateAndNormalizeParticles(1, 0, 0, baseCount), // 2s orbital '2_0_0': this.generateAndNormalizeParticles(2, 0, 0, baseCount), // 2p orbitals '2_1_-1': this.generateAndNormalizeParticles(2, 1, -1, baseCount), '2_1_0': this.generateAndNormalizeParticles(2, 1, 0, baseCount), '2_1_1': this.generateAndNormalizeParticles(2, 1, 1, baseCount), // 3s orbital '3_0_0': this.generateAndNormalizeParticles(3, 0, 0, baseCount), // 3p orbitals '3_1_-1': this.generateAndNormalizeParticles(3, 1, -1, baseCount), '3_1_0': this.generateAndNormalizeParticles(3, 1, 0, baseCount), '3_1_1': this.generateAndNormalizeParticles(3, 1, 1, baseCount), // 3d orbitals '3_2_-2': this.generateAndNormalizeParticles(3, 2, -2, baseCount), '3_2_-1': this.generateAndNormalizeParticles(3, 2, -1, baseCount), '3_2_0': this.generateAndNormalizeParticles(3, 2, 0, baseCount), '3_2_1': this.generateAndNormalizeParticles(3, 2, 1, baseCount), '3_2_2': this.generateAndNormalizeParticles(3, 2, 2, baseCount) }; console.log('Particle cache initialized with', Object.keys(this.particleCache).length, 'orbitals'); } /** * Generate and normalize particles for caching * @param {number} n - Principal quantum number * @param {number} l - Azimuthal quantum number * @param {number} m - Magnetic quantum number * @param {number} count - Number of particles * @returns {Array} Normalized particle data */ generateAndNormalizeParticles(n, l, m, count) { const particles = []; // Pre-calculate max probability for normalization const sampleSize = 100; let maxProb = 0; for (let i = 0; i < sampleSize; i++) { const r = this.sampleRadius(n, l); const theta = this.sampleTheta(l, m); const phi = this.samplePhi(m); const prob = this.quantumEngine.probabilityDensity(n, l, m, r, theta, phi); if (prob > maxProb) maxProb = prob; } // Generate particles for (let i = 0; i < count; i++) { const r = this.sampleRadius(n, l); const theta = this.sampleTheta(l, m); const phi = this.samplePhi(m); const cartesian = this.sphericalToCartesian( r * CONSTANTS.SCALE_FACTOR, theta, phi ); const probability = this.quantumEngine.probabilityDensity(n, l, m, r, theta, phi); particles.push({ position: cartesian, probability: maxProb > 0 ? probability / maxProb : 0 }); } return particles; } /** * Get particles from cache or generate new ones * @param {number} n - Principal quantum number * @param {number} l - Azimuthal quantum number * @param {number} m - Magnetic quantum number * @param {number} particleCount - Desired particle count * @returns {Array} Particle data */ getParticlesFromCache(n, l, m, particleCount) { const key = `${n}_${l}_${m}`; // Check if orbital is in cache if (this.particleCache && this.particleCache[key]) { const cached = this.particleCache[key]; const baseCount = cached.length; console.log(`Using cached particles for orbital (${n},${l},${m})`); // If requested count is close to base count, return cached particles if (particleCount <= baseCount * 1.2) { // Return subset or all cached particles if (particleCount >= baseCount) { return cached; } else { // Return random subset const step = Math.floor(baseCount / particleCount); return cached.filter((_, i) => i % step === 0).slice(0, particleCount); } } else { // Need more particles - use cached as base and add more const additionalCount = particleCount - baseCount; const additional = this.generateOrbitalParticlesInternal(n, l, m, additionalCount); return [...cached, ...additional]; } } // Not in cache, generate normally return null; } /** * Convert spherical coordinates to Cartesian * @param {number} r - Radius * @param {number} theta - Polar angle (0 to π) * @param {number} phi - Azimuthal angle (0 to 2π) * @returns {{x: number, y: number, z: number}} */ sphericalToCartesian(r, theta, phi) { const x = r * Math.sin(theta) * Math.cos(phi); const y = r * Math.sin(theta) * Math.sin(phi); const z = r * Math.cos(theta); return { x, y, z }; } /** * Sample radius using CDF (Cumulative Distribution Function) sampling * This is more efficient and accurate than rejection sampling * @param {number} n - Principal quantum number * @param {number} l - Azimuthal quantum number * @param {number} resolution - Number of samples for CDF (default 1000) * @returns {number} Sampled radius */ sampleRadius(n, l, resolution = 1000) { // Maximum radius to sample (most probability within ~3n² Bohr radii) const maxR = 3 * n * n * CONSTANTS.BOHR_RADIUS; // Minimum radius to avoid singularities at origin const minR = 0.01 * CONSTANTS.BOHR_RADIUS; // Build CDF array const cdf = []; let sum = 0; for (let i = 0; i < resolution; i++) { // Start from minR instead of 0 to avoid origin const r = minR + (i / resolution) * (maxR - minR); // Radial probability: P(r) = r² |R(n,l,r)|² const R = this.quantumEngine.radialWaveFunction(n, l, r); const prob = r * r * R * R; sum += prob; cdf.push(sum); } // Check if sum is valid if (sum === 0 || !isFinite(sum)) { console.warn(`Invalid CDF sum for radius sampling: ${sum}`); return maxR / 2; } // Normalize CDF for (let i = 0; i < cdf.length; i++) { cdf[i] /= sum; } // Sample from CDF const rand = Math.random(); for (let i = 0; i < cdf.length; i++) { if (rand <= cdf[i]) { return minR + (i / resolution) * (maxR - minR); } } return maxR; } /** * Sample theta (polar angle) using CDF sampling * @param {number} l - Azimuthal quantum number * @param {number} m - Magnetic quantum number * @param {number} resolution - Number of samples for CDF (default 500) * @returns {number} Sampled theta */ sampleTheta(l, m, resolution = 500) { // Build CDF array for theta const cdf = []; let sum = 0; // Small epsilon to avoid exact 0 and PI const epsilon = 0.001; for (let i = 0; i < resolution; i++) { // Map to range [epsilon, PI - epsilon] to avoid poles const theta = epsilon + (i / resolution) * (CONSTANTS.PI - 2 * epsilon); // For theta distribution, we need to integrate over phi // |Y(l,m,θ,φ)|² integrated over φ gives us the theta-only distribution // For real spherical harmonics, the phi integral gives a constant factor const absM = Math.abs(m); const legendre = window.legendrePolynomial(l, absM, Math.cos(theta)); const prob = legendre * legendre * Math.sin(theta); // sin(theta) is the Jacobian sum += prob; cdf.push(sum); } // Check if sum is valid if (sum === 0 || !isFinite(sum)) { console.warn(`Invalid CDF sum for theta sampling: ${sum}`); return CONSTANTS.PI / 2; // Return equator } // Normalize CDF for (let i = 0; i < cdf.length; i++) { cdf[i] /= sum; } // Sample from CDF const rand = Math.random(); for (let i = 0; i < cdf.length; i++) { if (rand <= cdf[i]) { return epsilon + (i / resolution) * (CONSTANTS.PI - 2 * epsilon); } } return CONSTANTS.PI / 2; } /** * Sample phi (azimuthal angle) using CDF sampling * For orbitals with m != 0, phi distribution depends on cos²(m*phi) or sin²(m*phi) * @param {number} m - Magnetic quantum number * @param {number} resolution - Number of samples for CDF (default 500) * @returns {number} Sampled phi */ samplePhi(m, resolution = 500) { // For m = 0, phi is uniformly distributed if (m === 0) { return Math.random() * CONSTANTS.TWO_PI; } // For m != 0, we need to sample according to the phi distribution // Real spherical harmonics: // m > 0: proportional to cos²(m*phi) // m < 0: proportional to sin²(|m|*phi) const cdf = []; let sum = 0; const absM = Math.abs(m); for (let i = 0; i < resolution; i++) { const phi = (i / resolution) * CONSTANTS.TWO_PI; let prob; if (m > 0) { // cos²(m*phi) distribution const val = Math.cos(m * phi); prob = val * val; } else { // sin²(|m|*phi) distribution const val = Math.sin(absM * phi); prob = val * val; } sum += prob; cdf.push(sum); } // Check if sum is valid if (sum === 0 || !isFinite(sum)) { console.warn(`Invalid CDF sum for phi sampling: ${sum}`); return Math.random() * CONSTANTS.TWO_PI; } // Normalize CDF for (let i = 0; i < cdf.length; i++) { cdf[i] /= sum; } // Sample from CDF const rand = Math.random(); for (let i = 0; i < cdf.length; i++) { if (rand <= cdf[i]) { return (i / resolution) * CONSTANTS.TWO_PI; } } return CONSTANTS.TWO_PI / 2; } /** * Generate particle positions for an orbital using CDF sampling * @param {number} n - Principal quantum number * @param {number} l - Azimuthal quantum number * @param {number} m - Magnetic quantum number * @param {number} particleCount - Number of particles to generate * @returns {Array<{position: {x, y, z}, probability: number}>} */ generateOrbitalParticles(n, l, m, particleCount) { validateQuantumNumbers(n, l, m); const count = Math.max(CONSTANTS.MIN_PARTICLE_COUNT, Math.min(particleCount, CONSTANTS.MAX_PARTICLE_COUNT)); console.log(`Generating ${count} particles for orbital (${n},${l},${m})`); // Try to get from cache first const cachedParticles = this.getParticlesFromCache(n, l, m, count); if (cachedParticles) { console.log(`Returned ${cachedParticles.length} particles from cache`); return cachedParticles; } // Not in cache, generate normally return this.generateOrbitalParticlesInternal(n, l, m, count); } /** * Internal method to generate particles (used when cache miss) * @param {number} n - Principal quantum number * @param {number} l - Azimuthal quantum number * @param {number} m - Magnetic quantum number * @param {number} count - Number of particles to generate * @returns {Array<{position: {x, y, z}, probability: number}>} */ generateOrbitalParticlesInternal(n, l, m, count) { const particles = []; // Pre-calculate some particles to find max probability for normalization const sampleSize = Math.min(100, count); let maxProb = 0; for (let i = 0; i < sampleSize; i++) { const r = this.sampleRadius(n, l); const theta = this.sampleTheta(l, m); const phi = this.samplePhi(m); const prob = this.quantumEngine.probabilityDensity(n, l, m, r, theta, phi); if (prob > maxProb) maxProb = prob; } // Generate all particles for (let i = 0; i < count; i++) { // Sample spherical coordinates using CDF const r = this.sampleRadius(n, l); const theta = this.sampleTheta(l, m); const phi = this.samplePhi(m); // Convert to Cartesian coordinates with scaling const cartesian = this.sphericalToCartesian( r * CONSTANTS.SCALE_FACTOR, theta, phi ); // Calculate probability density for color coding const probability = this.quantumEngine.probabilityDensity(n, l, m, r, theta, phi); particles.push({ position: cartesian, probability: maxProb > 0 ? probability / maxProb : 0 }); } console.log(`Generated ${particles.length} particles successfully`); return particles; } } // Make class available globally window.ParticleSampler = ParticleSampler;