Before running this notebook, be sure that you have previously executed the following commands, which we specified in the Before You Begin section of the book:

pip install ipympl
conda install nodejs
jupyter labextension install @jupyter-widgets/jupyterlab-manager
jupyter labextension install jupyter-matplotlib
In [ ]:
%matplotlib widget

11.15 Visualizing Algorithms

  • Animated visualizations can help you understand how algorithms work

Pysine: Playing Musical Notes

  • To enhance the animation, we’ll use the Pysine module to play musical notes representing the bar’s magnitudes
  • Pysine uses MIDI (Musical Instrument Digital Interface) to generate musical notes based on sound frequencies specified in hertz
  • pip install pysine

Executing the Animation

  • To sort the values in the range 1–10
    ipython selectionsortanimation.py 10
  • Bar colors:
    • Gray bars are not yet sorted and are not currently being compared
    • When locating the smallest element to swap into position, the animation displays one purple and one red bar
      • The purple bar represents the value at index smallest
      • The red bar is the other remaining element currently being compared
      • The red bar moves once for each comparison
      • The purple bar moves only if a smaller element is found during the current pass

Executing the Animation (cont.)

  • Bar colors (cont.)
    • The animation displays in purple both bars participating in a swap
    • A light green bar indicates an element that has been placed into its final position
    • Dark green bars are displayed one at a time when the sort completes to emphasize the final sorted order

11.15.1 Generator Functions

  • Generator expressions are similar to list comprehensions, but create generator objects that produce values on demand using lazy evaluation
  • The selection_sort function in the next subsection implements the selection sort algorithm but is interspersed with:
    • statements that keep track of the information we display in the animation,
    • yield and yield from statements that provide values for the FuncAnimation to pass to the update function
    • play_sound calls to play musical notes

11.15.1 Generator Functions (cont.)

  • yield and yield from make selection_sort a generator function
  • Generator functions are lazy and return values on demand
  • The FuncAnimation obtains values on demand from the generator and passes them to update to animate the algorithm.

yield Statements

  • yield keyword returnq the next generated item, then the function's execution suspends until the program requests another item
  • When the Python interpreter encounters a generator function call, it creates an iterable generator object that keeps track of the next value to generate

yield Statements (cont.)

  • A generator function that iterates through a sequence of values and returns the square of each value:
In [1]:
def square_generator(values):
    for value in values:
        yield value ** 2
In [2]:
numbers = [10, 20, 30]
In [3]:
squares = square_generator(numbers)
In [4]:
for number in squares:
    print(number, end='  ')
100  400  900  

yield Statements (cont.)

  • You must create a new generator object each time you wish to iterate through the generator’s values again
  • Can access generator values one at a time with built in function next
  • When there are no more items to process, the generator raises a StopIteration exception, which is how a for statement knows when to stop iterating through any iterable object
In [5]:
squares = square_generator(numbers)
In [6]:
next(squares)
Out[6]:
100
In [7]:
next(squares)
Out[7]:
400
In [8]:
next(squares)
Out[8]:
900
In [9]:
next(squares)
------------------------------------------------------------------------
StopIteration                          Traceback (most recent call last)
<ipython-input-9-e7cf8d24b3b2> in <module>
----> 1 next(squares)

StopIteration: 

11.15.2 Implementing the Selection Sort Animation

import Statements

  • ch11soundutilities.py contains our function play_sound
In [10]:
# selectionsortanimation.py
"""Animated selection sort visualization."""
from matplotlib import animation
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import sys
from ch11soundutilities import play_sound

update Function That Displays Each Animation Frame

  • Matplotlib FuncAnimation calls update once per animation frame to redraw the animation’s graphical elements
  • The update method’s frame_data parameter receives a tuple, which we unpack into:
    • data—the array being sorted
    • colors—an array of color names containing the specific color for each bar
    • swaps—an integer representing the number of swaps performed so far
    • comparisons—an integer representing the number of comparisons performed so far
In [11]:
def update(frame_data):
    """Display bars representing the current state."""    
    # unpack info for graph update
    data, colors, swaps, comparisons = frame_data
    plt.cla()  # clear old contents of current Figure

    # create barplot and set its xlabel
    bar_positions = np.arange(len(data))
    axes = sns.barplot(bar_positions, data, palette=colors)  # new bars
    axes.set(xlabel=f'Comparisons: {comparisons}; Swaps: {swaps}',
             xticklabels=data)  

flash_bars Function That Flashes the Bars About to be Swapped

  • We emphasize when the algorithm swaps two element values by calling flash_bars before and after a swap to flash the corresponding bars
  • Last two parameters are used only in the yield statements
    • Each yield throughout this example returns a tuple of the values the FuncAnimation passes to update's frame_data parameter
  • When the play_sound calls occur before the swap, the notes play in decreasing pitch to indicate that the values are out of order
  • When the play_sound calls occur after the swap, the notes play in increasing pitch to indicate that the values are now in order
In [12]:
def flash_bars(index1, index2, data, colors, swaps, comparisons):
    """Flash the bars about to be swapped and play their notes."""
    for x in range(0, 2):
        colors[index1], colors[index2] = 'white', 'white'
        yield (data, colors, swaps, comparisons) 
        colors[index1], colors[index2] = 'purple', 'purple'
        yield (data, colors, swaps, comparisons) 
        play_sound(data[index1], seconds=0.05)
        play_sound(data[index2], seconds=0.05)

selection_sort as a Generator Function

  • The colors array specifies that all the bars should be 'lightgray' initially
  • We modify this array’s elements throughout the algorithm to emphasize bars in different ways
  • Each time values are yielded, the FuncAnimation passes them to the update function
  • First yield causes the animation to display the bars in their initial unsorted order
In [13]:
def selection_sort(data):
    """Sort data using the selection sort algorithm and
    yields values that update uses to visualize the algorithm."""
    swaps = 0  
    comparisons = 0
    colors = ['lightgray'] * len(data)  # list of bar colors
    
    # display initial bars representing shuffled values
    yield (data, colors, swaps, comparisons)    
    
    # loop over len(data) - 1 elements    
    for index1 in range(0, len(data) - 1):
        print('outerloop')
        smallest = index1
        
        # loop to find index of smallest element's index   
        for index2 in range(index1 + 1, len(data)):
            print('innerloop')
            comparisons += 1
            colors[smallest] = 'purple'
            colors[index2] = 'red'            
            yield (data, colors, swaps, comparisons) 
            play_sound(data[index2], seconds=0.05)

            # compare elements at positions index and smallest
            if data[index2] < data[smallest]:
                colors[smallest] = 'lightgray'
                smallest = index2
                colors[smallest] = 'purple'
                yield (data, colors, swaps, comparisons) 
            else: 
                colors[index2] = 'lightgray'
                yield (data, colors, swaps, comparisons) 
            
        # ensure that last bar is not purple
        colors[-1] = 'lightgray'
        
        # flash the bars about to be swapped
        yield from flash_bars(index1, smallest, data, colors, 
                              swaps, comparisons)

        # swap the elements at positions index1 and smallest
        swaps += 1
        data[smallest], data[index1] = data[index1], data[smallest]  
        
        # flash the bars that were just swapped
        yield from flash_bars(index1, smallest, data, colors, 
                              swaps, comparisons)
        
        # indicate that bar index1 is now in its final spot
        colors[index1] = 'lightgreen'
        yield (data, colors, swaps, comparisons)    

    # indicate that last bar is now in its final spot
    colors[-1] = 'lightgreen'
    yield (data, colors, swaps, comparisons)    
    play_sound(data[-1], seconds=0.05)

    # play each bar's note once and color it darker green
    for index in range(len(data)):
        colors[index] = 'green'
        yield (data, colors, swaps, comparisons)
        play_sound(data[index], seconds=0.05)

selection_sort as a Generator Function (cont.)

  • Every iteration of the nested loop adds one to comparisons to keep track of the total number of comparisons performed
  • The bar at the index smallest is always colored purple during a pass of the algorithm
  • The bar at the index index2 is always colored red (the value being compared with the smallest) during a pass of the algorithm’s nested loop
  • Every nested loop iteration yields data, colors, swaps and comparisons, so we can see the purple and red bars indicating the values that are being compared

selection_sort as a Generator Function (cont.)

  • The nested loop's nested if/else statement ensures that only one bar is red and one is purple during the comparisons and yields values for the next animation frame
  • Each iteration of the outer loop ends with a swap, so we flash the corresponding bars before the swap, increment the swaps counter, perform the swap and flash the corresponding bars again

selection_sort as a Generator Function (cont.)

  • When one generator function (in this case selection_sort) needs to yield the results of another, chained generator functions are required
  • In this case, you must call the latter function (flash_bars), in a yield from statement to ensure that the values yielded by flash_bars “pass through” to the FuncAnimation for the next update call

selection_sort as a Generator Function (cont.)

  • When the algorithm’s outer loop completes, the last element of data is in its final position, so we color it light green, yield to draw the next animation frame and play that bar’s note
  • To complete the animation, we convert each bar one at a time to a darker green, yield to draw the next animation frame and play that bar’s musical note
  • The FuncAnimation terminates when the source of its animation-frame data has no more values

main Function That Launches the Animation

  • Recall that a FuncAnimation's frames keyword argument receives a value that specifies how many animation frames to execute
  • In this example, the frames keyword argument receives a call to the generator function selection_sort
  • The number of animation frames depends on when the generator “runs out” of values
  • Values yielded by selection_sort are the source of the update function’s frame_data
In [14]:
def main():
    number_of_values = int(sys.argv[1]) if len(sys.argv) == 2 else 10 

    figure = plt.figure('Selection Sort')  # Figure to display barplot
    numbers = np.arange(1, number_of_values + 1)  # create array 
    np.random.shuffle(numbers)  # shuffle the array

    # start the animation
    anim = animation.FuncAnimation(figure, update, repeat=False,
        frames=selection_sort(numbers), interval=50)

    plt.show()  # display the Figure

Instuctor Note: If this does not execute in your notebook, the Jupyter Lab extensions may have changed--this was still under development when we created the notebook. In this case, just run the script with IPython at the command line.

In [ ]:
# call main to execute the animation (we removed the if statement from the script in the book)
main()

Sound Utility Functions

The file ch11soundutilities.py contains two constants that we use to calculate musical note frequencies programmatically and three functions that play MIDI notes


©1992–2020 by Pearson Education, Inc. All Rights Reserved. This content is based on Chapter 5 of the book Intro to Python for Computer Science and Data Science: Learning to Program with AI, Big Data and the Cloud.

DISCLAIMER: The authors and publisher of this book have used their best efforts in preparing the book. These efforts include the development, research, and testing of the theories and programs to determine their effectiveness. The authors and publisher make no warranty of any kind, expressed or implied, with regard to these programs or to the documentation contained in these books. The authors and publisher shall not be liable in any event for incidental or consequential damages in connection with, or arising out of, the furnishing, performance, or use of these programs.