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 setup energy eigenstates for an infinite square well. This time We will take a different approach. We start with a JavaScript function rather than a shader to evaluate the wave function. Then populate an array with function values, and load those values into a texture for use in the simulation. Recognize this as exactly what we do with the potential, and we will parallel that process for the wave function.

  1. Write a function to provide the wave function values.
  2. Fill in an array with wave function values corresponding to our grid points.
  3. Write that array to the wave function texture.

Let's take a look back at the original version of the code where the wave function textures are created and filled in during the initial setup. On startup we create some empty textures. Remember that with the leapfrog method we have three textures, one for each of Ψ x t - Δ t , Ψ x t , and Ψ x t + Δ t .


  var waveFunctionData, waveFunctionTexture0, waveFunctionTexture1, waveFunctionTexture2;

  waveFunctionTexture0 = gpgpUtility.makeTexture(WebGLRenderingContext.FLOAT, null);
  waveFunctionTexture1 = gpgpUtility.makeTexture(WebGLRenderingContext.FLOAT, null);
  waveFunctionTexture2 = gpgpUtility.makeTexture(WebGLRenderingContext.FLOAT, null);
      

then we render into them with a shader that evaluates a Gaussian wave packet.


  // Initially use the same wave function at t and t-deltat
  fbos = schrodinger.getSourceFramebuffers();
  schrodingerInit.initialize(fbos[0], length, k, x0, w);
  schrodingerInit.initialize(fbos[1], length, k, x0, w);
      

Now we create an additional path to setup an energy eigenstate for our system.

The spatial part of a particle in a box wave function is well known.

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

Luckily it not to hard to express this as a JavaScript function.


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

  /**
   * 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, usually a typed 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 {Number[]} data        An array to hold the values for the wave function.
   *
   * @returns {Number[]} The data array populated with values data[i]=psi(i*length/xResolution).
   */
  function waveFunction(length, xResolution, vLeft, vWidth, n, data) {
    var nelements;
    var x;

    nelements = data.length/4;

    for(var i=0; i<nelements; ++i) {
      x = (length*i)/xResolution;
      if (x >= vLeft && x <= vLeft+vWidth) {
        data[4*i] = particleInABoxWaveFunction(vWidth, x-vLeft, n);
      } else {
        data[4*i] = 0;
      }
    }
    return data;
  }
      

We set a new wave function by populating the Ψ x t , and Ψ x t - Δ t textures. An important aspect this time around is that rather then create new textures, we load values into existing textures.

The Schrodinger simulator knows which two textures it will use as inputs for the next time step. These are the same textures that are attached to the framebuffers returned by getSourceFramebuffers. As a technical aside, this shows the equivalence of loading data into a texture via texImage2D and using a shader to rendering to a framebuffer object (fbo) with the texture attached to it.


  /**
   * Retrieve the two textures for the old and current wave functions in the next timestep.
   * Fill these with initial values for the wave function.
   *
   * @returns {WebGLTexture[]} The source textures for the next timestep.
   */
  this.getSourceTextures     = function()
  {
    var value = [];
    value[0] = textures[step];
    value[1] = textures[(step+1)%3];
    return value;
  }
      

Now that we have the textures, we need a method to populate them. In our original implementation we described a process to

  1. Create a texture
  2. Bind the texture
  3. Set minification and magnification parameters for the bound texture
  4. Load data into the bound texture
  5. Unbind the current texture (bind a null texture)

The textures have already been defined and we will not be changing any of the parameters. This time we will only bind the texture, load the data, then unbind the texture. Since this is a generally useful method, we build it into the GPGPUtility class.


  /**
   * Refresh the data in a preexisting texture using texSubImage2D() to avoid
   * repeated allocation of texture memory.
   *
   * @param {WebGLTexture}    texture The texture to be populated with data.
   * @param {number}          type    A valid texture type. FLOAT, UNSIGNED_BYTE, etc.
   * @param {number[] | null} data    Texture data, may not be null.
   */
  this.refreshTexture      =  function(texture, type, data)
  {
    // Bind the texture so the following methods effect it.
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // Replace the texture data
    gl.texSubImage2D(gl.TEXTURE_2D, // Target, matches bind above.
                     0,             // Level of detail.
                     0,             // xOffset
                     0,             // yOffset
                     problemWidth,  // Width - normalized to s.
                     problemHeight, // Height - normalized to t.
                     gl.RGBA,       // Format for each pixel.
                     type,          // Data type for each chanel.
                     data);         // Image data in the described format.

    // Unbind the texture.
    gl.bindTexture(gl.TEXTURE_2D, null);

   return texture;
  }
      

User Controls

Now we can setup 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.


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

  for (var 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
   */
  function setEigenstate(event)
  {
    var n;
    var target;
    var textures;

    // Default if not provided on button
    n      = 1.0;
    target = event.currentTarget;
    if (target.hasAttribute("data-n")) {
      n = target.getAttribute("data-n");
    }
    
    // Square well starts at vLeft, and runs for vWidth. nth energy eigenvalue
    waveFunctionData = waveFunction(length, xResolution, vLeft, vWidth, n, waveFunctionData);
    textures         = setWaveFunction(waveFunctionData);
    renderer.setPsiMax(1.0);
    renderer.show(textures[0]);
  }
      

Once the waveFunctionData has been filled in, we need to load the data into textures so the shaders can access it. The setWaveFunction method does this for us. The setWaveFunction method retreives the input textures from the schrodinger object, and uses the refreshTexture to load the data into these textures. The textures are returned so that the setEigenstate method can render it, and we see the value for the wave function as soon as we set it.


  function setWaveFunction(data)
  {
    var textures;

    textures = schrodinger.getSourceTextures();
    gpgpUtility.refreshTexture(textures[0], WebGLRenderingContext.FLOAT, data);
    gpgpUtility.refreshTexture(textures[1], WebGLRenderingContext.FLOAT, data);

    return textures;
  }
      

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.