Physics

Maxwell-Boltzmann Distribution

Interactive simulation of molecular speed distribution at different temperatures. Explore how gas molecules move in thermal equilibrium.

Equation: f(v) = 4π·(m/2πkT)^(3/2) · v² · e^(−mv²/2kT) — where m is molecular mass, k is Boltzmann's constant, T is absolute temperature.
300 K
1 curve
Most Probable Speed
v_p = √(2kT/m)
Mean Speed
⟨v⟩ = √(8kT/πm)
RMS Speed
v_rms = √(3kT/m)
Mean Kinetic Energy
⟨E_k⟩ = (3/2)kT
Peak Distribution Value
Escape Velocity Fraction
Above 11.2 km/s
Developer Reference

Core Algorithm & Standalone Script

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

// Constants
    const k_B = 1.380649e-23; // Boltzmann constant (J/K)
    const u = 1.66053906660e-27; // Atomic mass unit (kg)
    const c = 299792458; // Speed of light (m/s)

    // State
    let state = {
      temperature: 300,
      molecularMass: 32, // O2
      showVp: true,
      showVavg: true,
      showVrms: true,
      showEnergy: false,
      comparisonCount: 1,
      useLogScale: false,
      showParticleSim: false,
      showHistogram: false,
      showGrid: true,
      particleSpeedMult: 1
    };

    let particles = [];
    let speedHistogram = new Map();

    // Gas presets
    const gasPresets = {
      '2': { name: 'H₂', mass: 2 },
      '4': { name: 'He', mass: 4 },
      '20': { name: 'Ne', mass: 20 },
      '28': { name: 'N₂', mass: 28 },
      '32': { name: 'O₂', mass: 32 },
      '44': { name: 'CO₂', mass: 44 },
      '131': { name: 'Xe', mass: 131 }
    };

    // Calculate characteristic speeds
    function calculateSpeeds(T, mass) {
      const m = mass * u;
      const vp = Math.sqrt(2 * k_B * T / m);
      const vavg = Math.sqrt(8 * k_B * T / (Math.PI * m));
      const vrms = Math.sqrt(3 * k_B * T / m);
      return { vp, vavg, vrms };
    }

    // Maxwell-Boltzmann distribution f(v)
    function maxwellBoltzmann(v, T, mass) {
      const m = mass * u;
      const coeff = 4 * Math.PI * Math.pow(m / (2 * Math.PI * k_B * T), 1.5);
      return coeff * v * v * Math.exp(-m * v * v / (2 * k_B * T));
    }

    // Maxwell-Boltzmann energy distribution f(E)
    function energyDistribution(E, T) {
      const coeff = 2 * Math.PI * Math.pow(1 / (Math.PI * k_B * T), 1.5);
      return coeff * Math.sqrt(E) * Math.exp(-E / (k_B * T));
    }

    // Get legend data
    function getLegendData() {
      const colors = [
        '#001a4d', '#0033cc', '#0066ff', '#0099ff',
        '#00ccff', '#00ffcc', '#00ff99', '#00ff66',
        '#66ff00', '#99ff00', '#ccff00', '#ffff00',
        '#ffcc00', '#ff9900', '#ff6600', '#ff3300'
      ];

      const temps = [];
      const baseTemp = state.temperature;
      for (let i = 0; i < state.comparisonCount; i++) {
        if (state.useLogScale) {
          const ratio = Math.pow(10, (Math.log10(10000) - Math.log10(50)) * (i / (state.comparisonCount - 1 || 1)));
          temps.push(Math.round(50 * ratio));
        } else {
          const ratio = i / (state.comparisonCount - 1 || 1);
          temps.push(Math.round(50 + (10000 - 50) * ratio));
        }
      }

      const html = temps.map((t, i) => `
        <div class="legend-item">
          <div class="legend-color" style="background: ${colors[i % colors.length]};"></div>
          <span>${t} K</span>
        </div>
      `).join('');
      document.getElementById('legend').innerHTML = html;
    }

    // Draw main distribution canvas
    function drawMainCanvas() {
      const canvas = document.getElementById('main-canvas');
      const ctx = canvas.getContext('2d');

      const width = canvas.parentElement.clientWidth - 40;
      const height = 400;
      canvas.width = width;
      canvas.height = height;

      const margin = { top: 40, right: 40, bottom: 60, left: 80 };
      const plotWidth = width - margin.left - margin.right;
      const plotHeight = height - margin.top - margin.bottom;

      // Find max velocity for plotting
      let maxV = 0;
      let maxF = 0;

      const temps = [];
      const baseTemp = state.temperature;
      for (let i = 0; i < state.comparisonCount; i++) {
        const ratio = i / (state.comparisonCount - 1 || 1);
        temps.push(50 + (10000 - 50) * ratio);
      }

      temps.forEach(T => {
        const speeds = calculateSpeeds(T, state.molecularMass);
        maxV = Math.max(maxV, speeds.vrms * 2.5);

        for (let v = 0; v <= speeds.vrms * 2.5; v += speeds.vrms / 100) {
          const f = state.showEnergy ?
            energyDistribution(0.5 * state.molecularMass * u * v * v, T) :
            maxwellBoltzmann(v, T, state.molecularMass);
          maxF = Math.max(maxF, f);
        }
      });

      // Background
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, width, height);

      // Grid
      if (state.showGrid) {
        ctx.strokeStyle = '#1e1e1e';
        ctx.lineWidth = 1;
        for (let i = 0; i <= 10; i++) {
          const x = margin.left + (plotWidth * i / 10);
          const y = margin.top + (plotHeight * i / 10);
          ctx.beginPath();
          ctx.moveTo(x, margin.top);
          ctx.lineTo(x, margin.top + plotHeight);
          ctx.stroke();
          ctx.beginPath();
          ctx.moveTo(margin.left, y);
          ctx.lineTo(margin.left + plotWidth, y);
          ctx.stroke();
        }
      }

      // Plot distribution curves
      const colors = [
        '#001a4d', '#0033cc', '#0066ff', '#0099ff',
        '#00ccff', '#00ffcc', '#00ff99', '#00ff66',
        '#66ff00', '#99ff00', '#ccff00', '#ffff00',
        '#ffcc00', '#ff9900', '#ff6600', '#ff3300'
      ];

      temps.forEach((T, idx) => {
        ctx.strokeStyle = colors[idx % colors.length];
        ctx.lineWidth = 2.5;
        ctx.beginPath();

        let first = true;
        const step = maxV / 200;
        for (let v = 0; v <= maxV; v += step) {
          const f = state.showEnergy ?
            energyDistribution(0.5 * state.molecularMass * u * v * v, T) :
            maxwellBoltzmann(v, T, state.molecularMass);

          const x = margin.left + (v / maxV) * plotWidth;
          const y = margin.top + plotHeight - (f / maxF) * plotHeight;

          if (first) {
            ctx.moveTo(x, y);
            first = false;
          } else {
            ctx.lineTo(x, y);
          }
        }
        ctx.stroke();
      });

      // Draw characteristic speeds for primary temperature
      const speeds = calculateSpeeds(state.temperature, state.molecularMass);
      const speedsToShow = [
        { v: speeds.vp, label: 'v_p', color: '#f5c518', show: state.showVp },
        { v: speeds.vavg, label: '⟨v⟩', color: '#00c896', show: state.showVavg },
        { v: speeds.vrms, label: 'v_rms', color: '#ff2200', show: state.showVrms }
      ];

      speedsToShow.forEach(({ v, label, color, show }) => {
        if (show && v <= maxV) {
          const x = margin.left + (v / maxV) * plotWidth;
          ctx.strokeStyle = color;
          ctx.lineWidth = 2;
          ctx.setLineDash([5, 5]);
          ctx.beginPath();
          ctx.moveTo(x, margin.top);
          ctx.lineTo(x, margin.top + plotHeight);
          ctx.stroke();
          ctx.setLineDash([]);

          // Label
          ctx.fillStyle = color;
          ctx.font = 'bold 12px DM Mono';
          ctx.textAlign = 'center';
          ctx.fillText(label, x, margin.top - 15);
        }
      });

      // Axes
      ctx.strokeStyle = '#555555';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(margin.left, margin.top);
      ctx.lineTo(margin.left, margin.top + plotHeight);
      ctx.lineTo(margin.left + plotWidth, margin.top + plotHeight);
      ctx.stroke();

      // Axis labels
      ctx.fillStyle = '#555555';
      ctx.font = '12px DM Mono';
      ctx.textAlign = 'center';
      ctx.fillText('Velocity (m/s)', margin.left + plotWidth / 2, height - 15);
      ctx.save();
      ctx.translate(15, margin.top + plotHeight / 2);
      ctx.rotate(-Math.PI / 2);
      const yLabel = state.showEnergy ? 'Probability (1/m)' : 'Probability (s/m)';
      ctx.fillText(yLabel, 0, 0);
      ctx.restore();

      // Axis values
      ctx.textAlign = 'right';
      for (let i = 0; i <= 10; i++) {
        const v = (maxV * i / 10).toFixed(0);
        const x = margin.left + (plotWidth * i / 10);
        ctx.fillText(v, x, margin.top + plotHeight + 20);
      }

      ctx.textAlign = 'left';
      for (let i = 0; i <= 5; i++) {
        const f = (maxF * i / 5).toExponential(1);
        const y = margin.top + plotHeight - (plotHeight * i / 5);
        ctx.fillText(f, 5, y + 4);
      }
    }

    // Initialize particles
    function initializeParticles() {
      particles = [];
      speedHistogram.clear();

      const boxSize = 200; // pixels
      const particleCount = 100;
      const speeds = calculateSpeeds(state.temperature, state.molecularMass);

      for (let i = 0; i < particleCount; i++) {
        // Sample from Maxwell-Boltzmann distribution
        let v;
        do {
          v = Math.random() * speeds.vrms * 3;
        } while (Math.random() > maxwellBoltzmann(v, state.temperature, state.molecularMass) /
                 maxwellBoltzmann(speeds.vp, state.temperature, state.molecularMass));

        const angle = Math.random() * 2 * Math.PI;
        particles.push({
          x: Math.random() * boxSize,
          y: Math.random() * boxSize,
          vx: Math.cos(angle) * v * state.particleSpeedMult / 100,
          vy: Math.sin(angle) * v * state.particleSpeedMult / 100,
          speed: v
        });
      }
    }

    // Update particle positions
    function updateParticles() {
      const boxSize = 200;
      particles.forEach(p => {
        p.x += p.vx;
        p.y += p.vy;

        // Boundary collisions
        if (p.x < 0 || p.x > boxSize) p.vx *= -1;
        if (p.y < 0 || p.y > boxSize) p.vy *= -1;

        p.x = Math.max(0, Math.min(boxSize, p.x));
        p.y = Math.max(0, Math.min(boxSize, p.y));
      });

      // Update histogram
      speedHistogram.clear();
      const speeds = calculateSpeeds(state.temperature, state.molecularMass);
      const binSize = speeds.vrms / 10;

      particles.forEach(p => {
        const bin = Math.floor(p.speed / binSize);
        speedHistogram.set(bin, (speedHistogram.get(bin) || 0) + 1);
      });
    }

    // Draw particle simulation
    function drawParticleCanvas() {
      const canvas = document.getElementById('particle-canvas');
      const ctx = canvas.getContext('2d');
      const width = 300;
      const height = 300;
      canvas.width = width;
      canvas.height = height;

      // Background
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, width, height);

      // Border
      ctx.strokeStyle = '#1e1e1e';
      ctx.lineWidth = 2;
      ctx.strokeRect(0, 0, width, height);

      // Draw particles
      const scale = width / 200;
      particles.forEach(p => {
        ctx.fillStyle = '#ff2200';
        ctx.beginPath();
        ctx.arc(p.x * scale, p.y * scale, 2, 0, 2 * Math.PI);
        ctx.fill();
      });

      // Update stats
      const avgSpeed = particles.reduce((sum, p) => sum + p.speed, 0) / particles.length;
      document.getElementById('particle-count').textContent = particles.length;
      document.getElementById('particle-avg-speed').textContent = avgSpeed.toFixed(0);
    }

    // Draw histogram
    function drawHistogramCanvas() {
      const canvas = document.getElementById('histogram-canvas');
      const ctx = canvas.getContext('2d');
      const width = canvas.parentElement.clientWidth - 40;
      const height = 300;
      canvas.width = width;
      canvas.height = height;

      const margin = { top: 40, right: 40, bottom: 60, left: 80 };
      const plotWidth = width - margin.left - margin.right;
      const plotHeight = height - margin.top - margin.bottom;

      // Background
      ctx.fillStyle = '#0a0a0a';
      ctx.fillRect(0, 0, width, height);

      const speeds = calculateSpeeds(state.temperature, state.molecularMass);
      const maxV = speeds.vrms * 2.5;
      const binSize = maxV / 20;

      // Find max bin count
      let maxCount = Math.max(...speedHistogram.values());
      maxCount = Math.max(maxCount, particles.length / 20);

      // Draw histogram bars
      ctx.fillStyle = '#ff220055';
      for (let i = 0; i < 20; i++) {
        const count = speedHistogram.get(i) || 0;
        const barHeight = (count / maxCount) * plotHeight;
        const x = margin.left + (i * plotWidth / 20);
        const y = margin.top + plotHeight - barHeight;
        const barWidth = plotWidth / 20 - 2;
        ctx.fillRect(x, y, barWidth, barHeight);
      }

      // Draw theoretical curve
      ctx.strokeStyle = '#00c896';
      ctx.lineWidth = 2;
      ctx.beginPath();
      let first = true;
      for (let v = 0; v <= maxV; v += maxV / 100) {
        const f = maxwellBoltzmann(v, state.temperature, state.molecularMass);
        const normalized = (f / maxwellBoltzmann(speeds.vp, state.temperature, state.molecularMass)) * maxCount;
        const x = margin.left + (v / maxV) * plotWidth;
        const y = margin.top + plotHeight - (normalized / maxCount) * plotHeight;
        if (first) {
          ctx.moveTo(x, y);
          first = false;
        } else {
          ctx.lineTo(x, y);
        }
      }
      ctx.stroke();

      // Axes
      ctx.strokeStyle = '#555555';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(margin.left, margin.top);
      ctx.lineTo(margin.left, margin.top + plotHeight);
      ctx.lineTo(margin.left + plotWidth, margin.top + plotHeight);
      ctx.stroke();

      // Labels
      ctx.fillStyle = '#555555';
      ctx.font = '12px DM Mono';
      ctx.textAlign = 'center';
      ctx.fillText('Velocity (m/s)', margin.left + plotWidth / 2, height - 15);
    }

    // Update statistics
    function updateStats() {
      const speeds = calculateSpeeds(state.temperature, state.molecularMass);
      const m = state.molecularMass * u;
      const meanKE = 1.5 * k_B * state.temperature;

      // Escape velocity is 11.2 km/s
      const escapeV = 11200;
      const escapeTemp = 2 * m * escapeV * escapeV / (3 * k_B);
      const escapeFraction = 1 - 0.5 * (1 + Math.erf((Math.sqrt(3 * state.temperature / escapeTemp))));

      document.getElementById('stat-vp').textContent = speeds.vp.toFixed(0) + ' m/s';
      document.getElementById('stat-vavg').textContent = speeds.vavg.toFixed(0) + ' m/s';
      document.getElementById('stat-vrms').textContent = speeds.vrms.toFixed(0) + ' m/s';
      document.getElementById('stat-ke').textContent = (meanKE * 1e22).toFixed(2) + '×10⁻²² J';

      const peak = maxwellBoltzmann(speeds.vp, state.temperature, state.molecularMass);
      document.getElementById('stat-peak').textContent = peak.toExponential(2);
      document.getElementById('stat-escape').textContent = (escapeFraction * 100).toFixed(2) + '%';
    }

    // Render loop
    function render() {
      getLegendData();
      drawMainCanvas();
      updateStats();

      if (state.showParticleSim) {
        updateParticles();
        drawParticleCanvas();
      }

      if (state.showHistogram && state.showParticleSim) {
        drawHistogramCanvas();
      }
    }

    // Event listeners
    document.getElementById('temp-slider').addEventListener('input', (e) => {
      if (state.useLogScale) {
        state.temperature = Math.round(50 * Math.pow(200, e.target.value / 10000));
      } else {
        state.temperature = parseInt(e.target.value);
      }
      document.getElementById('temp-value').textContent = state.temperature;
      if (state.showParticleSim) initializeParticles();
      render();
    });

    document.getElementById('log-scale').addEventListener('change', (e) => {
      state.useLogScale = e.target.checked;
    });

    document.getElementById('gas-select').addEventListener('change', (e) => {
      if (e.target.value === 'custom') {
        document.getElementById('custom-mass-field').style.display = 'flex';
      } else {
        document.getElementById('custom-mass-field').style.display = 'none';
        state.molecularMass = parseInt(e.target.value);
        if (state.showParticleSim) initializeParticles();
        render();
      }
    });

    document.getElementById('custom-mass').addEventListener('change', (e) => {
      state.molecularMass = parseInt(e.target.value) || 32;
      if (state.showParticleSim) initializeParticles();
      render();
    });

    document.getElementById('comparison-count').addEventListener('input', (e) => {
      state.comparisonCount = parseInt(e.target.value);
      document.getElementById('comparison-value').textContent = state.comparisonCount;
      render();
    });

    document.getElementById('particle-speed-mult').addEventListener('input', (e) => {
      state.particleSpeedMult = parseFloat(e.target.value);
      document.getElementById('particle-mult-value').textContent = state.particleSpeedMult.toFixed(1);
      if (state.showParticleSim) initializeParticles();
    });

    // Toggle buttons
    document.querySelectorAll('[data-toggle]').forEach(btn => {
      btn.addEventListener('click', () => {
        const toggle = btn.dataset.toggle;

        if (toggle === 'vp') {
          state.showVp = !state.showVp;
        } else if (toggle === 'vavg') {
          state.showVavg = !state.showVavg;
        } else if (toggle === 'vrms') {
          state.showVrms = !state.showVrms;
        } else if (toggle === 'energy') {
          state.showEnergy = !state.showEnergy;
        } else if (toggle === 'particle-sim') {
          state.showParticleSim = !state.showParticleSim;
          document.getElementById('particle-sim-container').style.display =
            state.showParticleSim ? 'grid' : 'none';
          if (state.showParticleSim) initializeParticles();
        } else if (toggle === 'histogram') {
          state.showHistogram = !state.showHistogram;
          document.getElementById('histogram-container').style.display =
            state.showHistogram ? 'block' : 'none';
        } else if (toggle === 'grid') {
          state.showGrid = !state.showGrid;
        }

        btn.classList.toggle('active');
        render();
      });
    });

    // Initialize
    render();

    // Animation loop for particles
    let lastUpdate = Date.now();
    function animationLoop() {
      const now = Date.now();
      if (now - lastUpdate > 50) { // Update every 50ms
        render();
        lastUpdate = now;
      }
      requestAnimationFrame(animationLoop);
    }
    animationLoop();

    // Responsive resize
    window.addEventListener('resize', render);