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 `yield`ed, 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 `yield`s `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 `yield`s 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 `yield`ed 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 `yield`ed 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.