Quantum Mechanics

Particle in a BOX

Interactive visualization of a quantum particle confined in an infinite square well. Explore wave functions, energy levels, superposition, and time evolution.

Wave Function Visualization
Eₙ / E₁
1.00
Nodes
0
λₙ / L
2.00
⟨x⟩ / L
0.50

Superposition Mode

ψ₁(x)
ψ₂(x)
ψ₃(x)
ψ₄(x)
Normalization: Σ|cₙ|² = 1.00
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

// Physical constants (in atomic units)
    const PI = Math.PI;
    const HBAR = 1; // ℏ = 1 in atomic units

    // State
    let state = {
      n: 1,
      L: 1,
      m: 1,
      mode: '1d',
      displayModes: { psi: true, psi2: true, re: false, im: false },
      nx: 1,
      ny: 1,
      c: [1, 0, 0, 0],
      speed: 1,
      time: 0,
      isSuperposition: false,
      animationId: null
    };

    // DOM elements
    const canvas = document.getElementById('mainCanvas');
    const ctx = canvas.getContext('2d');
    const nSlider = document.getElementById('nSlider');
    const lSlider = document.getElementById('lSlider');
    const mSlider = document.getElementById('mSlider');
    const speedSlider = document.getElementById('speedSlider');
    const nxSlider = document.getElementById('nxSlider');
    const nySlider = document.getElementById('nySlider');
    const nValue = document.getElementById('nValue');
    const lValue = document.getElementById('lValue');
    const mValue = document.getElementById('mValue');
    const speedValue = document.getElementById('speedValue');
    const nxValue = document.getElementById('nxValue');
    const nyValue = document.getElementById('nyValue');
    const energyRatio = document.getElementById('energyRatio');
    const nodesDisplay = document.getElementById('nodes');
    const wavelengthDisplay = document.getElementById('wavelength');

    // Quantum functions
    function psi_n(x, n, L) {
      // ψₙ(x) = √(2/L) * sin(nπx/L)
      return Math.sqrt(2 / L) * Math.sin(n * PI * x / L);
    }

    function psi_n_2d(x, y, nx, ny, L) {
      // ψ(x,y) = (2/L) * sin(nₓπx/L) * sin(nᵧπy/L)
      return (2 / L) * Math.sin(nx * PI * x / L) * Math.sin(ny * PI * y / L);
    }

    function energy_n(n, L, m) {
      // Eₙ = n²π²ℏ²/(2mL²)
      // In atomic units with ℏ=1: Eₙ = n²π²/(2mL²)
      return (n * n * PI * PI) / (2 * m * L * L);
    }

    function wavefunction_time(x, n, t, L, m) {
      // Ψₙ(x,t) = ψₙ(x) * e^(-iEₙt/ℏ)
      const psi = psi_n(x, n, L);
      const E = energy_n(n, L, m);
      const phase = E * t;
      return {
        re: psi * Math.cos(phase),
        im: psi * Math.sin(phase)
      };
    }

    function wavefunction_superposition(x, t, L, m, c, maxN = 4) {
      // Ψ(x,t) = Σ cₙ ψₙ(x) e^(-iEₙt/ℏ)
      let re = 0, im = 0;
      for (let n = 1; n <= maxN; n++) {
        if (c[n - 1] > 0.001) {
          const psi = psi_n(x, n, L);
          const E = energy_n(n, L, m);
          const phase = E * t;
          const cos_p = Math.cos(phase);
          const sin_p = Math.sin(phase);
          re += c[n - 1] * psi * cos_p;
          im += c[n - 1] * psi * sin_p;
        }
      }
      return { re, im };
    }

    // Update stats
    function updateStats() {
      const E = energy_n(state.n, state.L, state.m);
      const E1 = energy_n(1, state.L, state.m);
      energyRatio.textContent = (E / E1).toFixed(2);
      nodesDisplay.textContent = state.n - 1;
      const lambda = (2 * state.L) / state.n;
      wavelengthDisplay.textContent = (lambda / state.L).toFixed(2);

      // Normalization check
      let norm = 0;
      for (let i = 0; i < 4; i++) {
        norm += state.c[i] * state.c[i];
      }
      document.getElementById('normValue').textContent = norm.toFixed(2);
    }

    // Draw the visualization
    function draw() {
      const w = canvas.width;
      const h = canvas.height;

      // Clear canvas
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, w, h);

      if (state.mode === '1d') {
        draw1D();
      } else {
        draw2D();
      }
    }

    function draw1D() {
      const w = canvas.width;
      const h = canvas.height;
      const margin = 80;
      const plotWidth = w - 2 * margin;
      const plotHeight = h - 2 * margin;
      const x0 = margin;
      const y0 = margin + plotHeight / 2;

      // Draw box walls (infinite potential)
      ctx.strokeStyle = '#555555';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.moveTo(x0, y0 - plotHeight / 3);
      ctx.lineTo(x0, y0 + plotHeight / 3);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(x0 + plotWidth, y0 - plotHeight / 3);
      ctx.lineTo(x0 + plotWidth, y0 + plotHeight / 3);
      ctx.stroke();

      // Draw axis line
      ctx.strokeStyle = '#2a2a2a';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(x0, y0);
      ctx.lineTo(x0 + plotWidth, y0);
      ctx.stroke();

      // Draw energy level diagram on left
      drawEnergyLevels(x0 - 50, y0, 30);

      // Sample the wave function
      const samples = 400;
      const maxVal = Math.sqrt(2 / state.L) * 1.5;
      const isSuperposition = state.c[1] > 0.001 || state.c[2] > 0.001 || state.c[3] > 0.001;

      // Draw probability density |ψ|²
      if (state.displayModes.psi2) {
        ctx.fillStyle = 'rgba(255, 34, 0, 0.2)';
        ctx.beginPath();
        ctx.moveTo(x0, y0);
        for (let i = 0; i <= samples; i++) {
          const xNorm = i / samples;
          const x = xNorm * state.L;
          let psi2;
          if (isSuperposition) {
            const wf = wavefunction_superposition(x, state.time, state.L, state.m, state.c);
            psi2 = wf.re * wf.re + wf.im * wf.im;
          } else {
            const psi = psi_n(x, state.n, state.L);
            psi2 = psi * psi;
          }
          const canvasX = x0 + xNorm * plotWidth;
          const canvasY = y0 - (psi2 / (2 / state.L)) * plotHeight / 3;
          ctx.lineTo(canvasX, canvasY);
        }
        ctx.lineTo(x0 + plotWidth, y0);
        ctx.closePath();
        ctx.fill();
      }

      // Draw wave function ψ(x) - above zero
      if (state.displayModes.psi && !isSuperposition) {
        ctx.strokeStyle = '#00c896';
        ctx.lineWidth = 2;
        ctx.beginPath();
        for (let i = 0; i <= samples; i++) {
          const xNorm = i / samples;
          const x = xNorm * state.L;
          const psi = psi_n(x, state.n, state.L);
          const canvasX = x0 + xNorm * plotWidth;
          const canvasY = y0 - (psi / maxVal) * plotHeight / 3;
          if (i === 0) ctx.moveTo(canvasX, canvasY);
          else ctx.lineTo(canvasX, canvasY);
        }
        ctx.stroke();
      }

      // Draw Re(Ψ) and Im(Ψ) for superposition
      if (state.displayModes.re || state.displayModes.im || isSuperposition) {
        if (state.displayModes.re) {
          ctx.strokeStyle = '#ff2200';
          ctx.lineWidth = 2;
          ctx.beginPath();
          for (let i = 0; i <= samples; i++) {
            const xNorm = i / samples;
            const x = xNorm * state.L;
            const wf = isSuperposition ? wavefunction_superposition(x, state.time, state.L, state.m, state.c) : wavefunction_time(x, state.n, state.time, state.L, state.m);
            const canvasX = x0 + xNorm * plotWidth;
            const canvasY = y0 - (wf.re / maxVal) * plotHeight / 3;
            if (i === 0) ctx.moveTo(canvasX, canvasY);
            else ctx.lineTo(canvasX, canvasY);
          }
          ctx.stroke();
        }

        if (state.displayModes.im) {
          ctx.strokeStyle = '#4488ff';
          ctx.lineWidth = 2;
          ctx.beginPath();
          for (let i = 0; i <= samples; i++) {
            const xNorm = i / samples;
            const x = xNorm * state.L;
            const wf = wavefunction_superposition(x, state.time, state.L, state.m, state.c);
            const canvasX = x0 + xNorm * plotWidth;
            const canvasY = y0 - (wf.im / maxVal) * plotHeight / 3;
            if (i === 0) ctx.moveTo(canvasX, canvasY);
            else ctx.lineTo(canvasX, canvasY);
          }
          ctx.stroke();
        }
      }

      // Draw axes labels
      ctx.fillStyle = '#555555';
      ctx.font = '12px "DM Mono"';
      ctx.textAlign = 'center';
      ctx.fillText('x', x0 + plotWidth / 2, h - 30);
      ctx.textAlign = 'right';
      ctx.fillText('ψ(x)', 40, y0 - 20);

      // Draw x-axis ticks and labels
      for (let i = 0; i <= 4; i++) {
        const xNorm = i / 4;
        const canvasX = x0 + xNorm * plotWidth;
        ctx.strokeStyle = '#2a2a2a';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(canvasX, y0 - 5);
        ctx.lineTo(canvasX, y0 + 5);
        ctx.stroke();
        ctx.fillStyle = '#555555';
        ctx.font = '11px "DM Mono"';
        ctx.textAlign = 'center';
        const xLabel = (xNorm * state.L).toFixed(2);
        ctx.fillText(xLabel, canvasX, y0 + 20);
      }
    }

    function drawEnergyLevels(x, yCenter, width) {
      // Draw energy level diagram
      const levelHeight = 40;
      const maxLevel = 8;

      ctx.strokeStyle = '#1e1e1e';
      ctx.lineWidth = 1;
      for (let i = 1; i <= maxLevel; i++) {
        const y = yCenter - (maxLevel / 2 - i) * levelHeight;
        ctx.strokeStyle = i === state.n ? '#ff2200' : '#1e1e1e';
        ctx.lineWidth = i === state.n ? 2 : 1;
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.lineTo(x + width, y);
        ctx.stroke();

        ctx.fillStyle = i === state.n ? '#ff2200' : '#555555';
        ctx.font = i === state.n ? 'bold 11px "DM Mono"' : '11px "DM Mono"';
        ctx.textAlign = 'right';
        const E = energy_n(i, state.L, state.m);
        const E1 = energy_n(1, state.L, state.m);
        const label = 'E' + i + '=' + (i * i).toFixed(0) + 'E₁';
        ctx.fillText(label, x - 5, y + 4);
      }
    }

    function draw2D() {
      const w = canvas.width;
      const h = canvas.height;
      const margin = 60;
      const size = Math.min(w, h) - 2 * margin;
      const x0 = (w - size) / 2;
      const y0 = (h - size) / 2;

      // Draw 2D heatmap
      const resolution = 100;
      const pixelSize = size / resolution;

      const imageData = ctx.createImageData(resolution, resolution);
      const data = imageData.data;

      let maxPsi2 = 0;
      const psi2Data = [];
      for (let py = 0; py < resolution; py++) {
        for (let px = 0; px < resolution; px++) {
          const x = (px / resolution) * state.L;
          const y = (py / resolution) * state.L;
          const psi = psi_n_2d(x, y, state.nx, state.ny, state.L);
          const psi2 = psi * psi;
          psi2Data.push(psi2);
          maxPsi2 = Math.max(maxPsi2, psi2);
        }
      }

      for (let i = 0; i < psi2Data.length; i++) {
        const val = psi2Data[i] / maxPsi2;
        const hue = (1 - val) * 240; // Blue for low, red for high
        const rgb = hslToRgb(hue / 360, 0.7, 0.5);
        data[i * 4] = rgb[0];
        data[i * 4 + 1] = rgb[1];
        data[i * 4 + 2] = rgb[2];
        data[i * 4 + 3] = 255;
      }

      ctx.putImageData(imageData, x0, y0);

      // Draw border
      ctx.strokeStyle = '#555555';
      ctx.lineWidth = 2;
      ctx.strokeRect(x0, y0, size, size);

      // Labels
      ctx.fillStyle = '#555555';
      ctx.font = '12px "DM Mono"';
      ctx.textAlign = 'center';
      ctx.fillText('x', x0 + size / 2, h - 20);
      ctx.save();
      ctx.translate(20, y0 + size / 2);
      ctx.rotate(-Math.PI / 2);
      ctx.fillText('y', 0, 0);
      ctx.restore();
    }

    function hslToRgb(h, s, l) {
      let r, g, b;
      if (s === 0) {
        r = g = b = l;
      } else {
        const hue2rgb = (p, q, t) => {
          if (t < 0) t += 1;
          if (t > 1) t -= 1;
          if (t < 1 / 6) return p + (q - p) * 6 * t;
          if (t < 1 / 2) return q;
          if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
          return p;
        };
        const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        const p = 2 * l - q;
        r = hue2rgb(p, q, h + 1 / 3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1 / 3);
      }
      return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }

    // Animation loop
    function animate() {
      state.time += 0.01 * state.speed;
      draw();
      state.animationId = requestAnimationFrame(animate);
    }

    // Event listeners
    nSlider.addEventListener('input', (e) => {
      state.n = parseInt(e.target.value);
      nValue.textContent = state.n;
      updateStats();
    });

    lSlider.addEventListener('input', (e) => {
      state.L = parseFloat(e.target.value);
      lValue.textContent = state.L.toFixed(1);
      updateStats();
    });

    mSlider.addEventListener('input', (e) => {
      state.m = parseFloat(e.target.value);
      mValue.textContent = state.m.toFixed(1);
      updateStats();
    });

    speedSlider.addEventListener('input', (e) => {
      state.speed = parseFloat(e.target.value);
      speedValue.textContent = state.speed.toFixed(1) + 'x';
    });

    nxSlider.addEventListener('input', (e) => {
      state.nx = parseInt(e.target.value);
      nxValue.textContent = state.nx;
    });

    nySlider.addEventListener('input', (e) => {
      state.ny = parseInt(e.target.value);
      nyValue.textContent = state.ny;
    });

    // Mode toggle
    document.querySelectorAll('[data-mode]').forEach((btn) => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('[data-mode]').forEach((b) => b.classList.remove('active'));
        btn.classList.add('active');
        state.mode = btn.dataset.mode;
        document.getElementById('twoDBtnsSection').style.display = state.mode === '2d' ? 'block' : 'none';
      });
    });

    // Display mode toggle
    document.querySelectorAll('[data-display]').forEach((btn) => {
      btn.addEventListener('click', () => {
        btn.classList.toggle('active');
        state.displayModes[btn.dataset.display] = btn.classList.contains('active');
      });
    });

    // Superposition sliders
    for (let i = 1; i <= 4; i++) {
      document.getElementById(`c${i}Slider`).addEventListener('input', (e) => {
        state.c[i - 1] = parseFloat(e.target.value);
        document.getElementById(`c${i}Value`).textContent = state.c[i - 1].toFixed(2);
        state.isSuperposition = state.c.some(c => c > 0.001);
        updateStats();
      });
    }

    // Initialize
    updateStats();
    animate();