Particle In A Box Eigenstates

A general simulation is a powerful tool, especially so in our instructional venue. The more interactivity and responsiveness we place in the students hands, the richer and more effective the educational experience.

Let's continue down the path of integrating HTML controls into the simulation. These new controls set the wave function to a particle in a box energy eigenstate ψ = ψ n . We will see that these eigenstates demonstrate different, and uniquely quantum, behaviors when compared to the Gaussian wave packet.

Particle in a box. ◼ stops the simulation, ▸ restarts it. Other buttons set initial conditions when the simulation is stopped.

The Gaussian wave packet, representing a localized particle, bounces back and forth between the sides of the box. The magnitude of the wave function, | Ψ x t | 2 , in green, shows shifting peaks and valleys as the wave function spreads out and interferes with itself.

The energy eigenstates show a dramatically different behavior. Once set to one of the energy eigenstates, ψ n , the magnitude of the wave function is stationary while the real and imaginary components rotate into one another with an energy dependent velocity. This illustrates why energy eigenstates are frequently referred to as stationary states.

The Wave Function

We already have a shader based initializer for the wave function, however, that shader sets up a Gaussian wave packet that models a free particle. Now we want to set up energy eigenstates for an infinite square well. This time we demonstrate a different approach. We start with a JavaScript function rather than a shader to evaluate the wave function. Then load those values into the storage arrays for use in the simulation.

The particle in a box wave function is well known:

ψ n ( x ) = 2 a sin n π x a , n = 1 , 2 , 3...

where n indicates the energy level of the particle. Luckily it not too hard to express this as a JavaScript function.


  /**
   * Compute values for the particle in a box wave function where the potential is
   * infinite everywhere except 0 ≤ x ≤ vWidth
   */
  function particleInABoxWaveFunction(vWidth, x, n) {
    return Math.sqrt(2.0 / vWidth) * Math.sin((n * Math.PI * x) / vWidth);
  }
            

We need to populate an array covering our problem grid with the particle in a box wave function within the box where v = 0, and zero outside the box where v is very large.



  /**
   * Evaluate the nth energy eigenfunction for an infinite square
   * well in a system of length length with xResolution grid
   * points. v=0 where vLeft ≤ v ≤ vLeft+vWidth, v=∞ elsewhere.
   * Load the results into the data array.
   *
   * @param {float}         length      The physical length of the simulation.
   * @param {Number}        xResolution The number of grid elements in the x direction.
   * @param {Number}        vLeft       The position of the left edge of the potential well.
   * @param {Number}        vWidth      The length of the potential well.
   * @param {Number}        n           The energy eigenvalue for this wave function.
   * @param {Array<Number>} data        An array to hold the values for the wave function.
   *
   * @returns {Array<Number>} The data array populated with values data[i]=psi(i*length/xResolution).
   */
  function waveFunction(length, xResolution, vLeft, vWidth, n, data) {
    for(let i=0; i<xResolution; ++i) {
      const x = (length*i)/xResolution;
      if (x >= vLeft && x <= vLeft+vWidth) {
        data[2*i] = particleInABoxWaveFunction(vWidth, x-vLeft, n);
      } else {
        data[2*i] = 0;
      }
      // The imaginary part of the initial wave function is always zero.
      data[2*i+1] = 0;
    }
    return data;
  }
      

Now, we need to copy this array to the shader. This need is so common that it is built into WebGPU as the writeBuffer method.


  device.queue.writeBuffer(buffer, bufferOffset, data, dataOffset, size);
            
buffer
The WebGPU buffer we are writing into. In this case it will be a wave function buffer such as waveFunctionBuffer0 or waveFunctionBuffer1.
bufferOffset
The offset in bytes where we start writing data into the buffer. We overwrite the entire buffer, so we start at the beginning with a 0 offset.
data
The data to be written to the buffer. This must be one of the types introduced to JavaScript for GPU programming. We wrap it in the Float32Array before passing it to the writeBuffer method call.
dataOffset
Offset where we begin to read the data. For typed arrays, such as the Float32Array we use, this is a count of array elements, otherwise it is a count of bytes.
size
How much data to copy. For typed arrays this is given in elements of the array, and in bytes otherwise.

So the code to copy our data to the GPU buffers is:


  const float32Data = new Float32Array(data);

  device.queue.writeBuffer(waveFunctionBuffer0, 0, float32Data, 0, 2*xResolution);
  device.queue.writeBuffer(waveFunctionBuffer1, 0, float32Data, 0, 2*xResolution);
            

This does require a small change to our code. When the wave function buffers are created we must include the GPUBufferUsage.COPY_DST usage flag, allowing data to be copied to them:


waveFunctionBuffer0 = device.createBuffer({
  label: "Wave function 0",
  size: 2*xResolution*Float32Array.BYTES_PER_ELEMENT,
  usage: debug ? GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC : GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
});
            

and similarly for the other wave function buffers.

User Controls

Now we can set up the buttons that tie all this together and use these methods to set the current wave function in the simulation. Each of the buttons that sets the wave function to a particle in a box eigenstate has the eigenstate class. We use this to build a collection of these buttons. Our use of MathML to set the contents of the buttons is yet another example of the expressive power in some of the corners of HTML.


  <button id="freeParticle" class="psi setter">
    Set <math>
          <mi>&psi;</mi>
          <mo>=</mo>
        </math>
    Gaussian wave packet
  </button>

  <button data-n="1" class="psi setter eigenstate">
    Set <math>
          <mi>&psi;</mi>
          <mo>=</mo>
          <msub><mi>&psi;</mi><mn>1</mn></msub>
        </math>
  </button>

  <button data-n="2" class="psi setter eigenstate">
    Set <math>
          <mi>&psi;</mi>
          <mo>=</mo>
          <msub><mi>&psi;</mi><mn>2</mn></msub>
        </math>
  </button>

  <button data-n="3" class="psi setter eigenstate">
    Set <math>
          <mi>&psi;</mi>
          <mo>=</mo>
          <msub><mi>&psi;</mi><mn>3</mn></msub>
        </math>
  </button>

  <button data-n="4" class="psi setter eigenstate">
    Set <math>
          <mi>&psi;</mi>
          <mo>=</mo>
          <msub><mi>&psi;</mi><mn>4</mn></msub>
        </math>
  </button>
      

We use getElementsByClassName("eigenstate") to get a list of the eigenstate buttons. Once we have that list, we bind an event listener to each button. We can use a single event listener for the buttons because each button has a data-n HTML5 data attribute to define which eigenstate to set.


  const eigenButtons = document.getElementsByClassName("eigenstate");
  const nbuttons = eigenButtons.length;

  for (let i=0; i<nbuttons; ++i)
  {
    eigenButtons[i].addEventListener("click", setEigenstate, false);
  }
      

The setEigenstate method is invoked when one of the buttons is clicked. The method looks up the data-n attribute to know which eigenstate to set, then calls the waveFunction method to fill in the waveFunctionData data array.


  /**
   * Set an initial wave function as an energy eigenstate for a particle in a box where
   * v=0 where vLeft <= x <= vLeft+vWidth
   */
  async function setEigenstate(event) {
    // Default if not provided on button
    let n = 1.0;
    let target = event.currentTarget;
    if (target.hasAttribute("data-n")) {
      n = target.getAttribute("data-n");
    }

    // Get the nth energy eigenfunction for an infinite square well starting at vLeft,
    // and having width vWidth.
    waveFunctionData = waveFunction(length, xResolution, vLeft, vWidth, n, waveFunctionData);
    await schrodinger.setWaveFunction(waveFunctionData);
    schrodingerRenderer.setPsiMax(1.0);
    await schrodingerRenderer.render(schrodinger.getWaveFunctionBuffer0());
  }
      

Once the waveFunctionData has been filled in, the setWaveFunction method uses the writeBuffer technique we covered earlier to make the data available to the shader. As a last step, we render the wave function so we see the new values as soon as they are set.

Starting and Stopping

When we stop the simulation, enable buttons to set the wave function to a known state. Technically, we don't need to stop the simulation to do this, but changing a running simulation mid flight is confusing and doesn't steer the learner towards examining the differences among the states, nor towards observing the stationary nature of the stationary states.


  /**
   * Start/stop the simulation, enable or disable initial
   * condition buttons as appropriate.
   */
  function toggleAnimation()
  {
    running                     = !running;
    stopButton.disabled         = !running;
    startButton.disabled        = running;
    freeParticleButton.disabled = running;
    setEigenButtons(running);
    if (running)
    {
      requestAnimationFrame(nextFrame);
    }
  }
     
Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License.