Ajstros / pyripherals

Python solution for communicating with peripheral ICs
GNU General Public License v3.0
2 stars 3 forks source link

Faster DDR3.data_arrays plotting #11

Closed Ajstros closed 2 years ago

Ajstros commented 2 years ago

Is your feature request related to a problem? Please describe. For debugging purposes especially, it is convenient to plot the data in the DDR3 data_arrays to make sure it matches what we expect. However, these arrays are very long and thus take some time and memory to print.

Describe the solution you'd like Because the data in these arrays often represents a flat or square wave, where several values are repeated, the high length of the arrays becomes a hindrance without providing any other helpful information. Because matplotlib can plot line graphs, connecting points automatically, several points in a flat section can be represented by two points instead: a beginning and an end. For example, a 1001 point array following the pattern [0, 0.001, 0.002, ..., 1] will appear the same on the graph as the 2 point array [0, 1]. I would like a solution that plots only the first and last points of a section of repeated values to save time and memory.

Describe alternatives you've considered One alternative would be cutting down the dataset without regard to repeated values. For example, just taking every other data point. This would help and would actually outperform my above suggestion in cases without repeated values, like a sine wave, but it leaves the chance to change the plot image and would underperform against my above solution in cases of long sections of repeated values.

Ajstros commented 2 years ago

This code below is what I am thinking.

import matplotlib.pyplot as plt
import numpy as np
import time

def plt_uniques(data, ax=None):
    """Plot only the first and last of each group of unique data points to save time and memory.

    Parameters
    ----------
    data : np.ndarray
        Data to plot.
    """

    if len(data.shape) < 1:
        raise Exception(f'expected data.shape >= 1 but data.shape is {data.shape}')

    if ax is None:
        # No axis provided, create new
        fig, ax = plt.subplots()

    if len(data.shape) > 1:
        # 2-dimensional, plot each row as a separate line
        for d in data:
            plt_uniques(data=d, ax=ax)

    else:
        # len(data.shape) == 1, we can plot as normal
        uniques, indices = np.unique(data, return_index=True)   # Get indices of first appearance of unique values
        indices = np.append(indices, indices - 1)               # Get indices from just before unique values to get first and last of each group of unique values
        indices = np.append(indices, len(data) - 1)             # Add last index so full data is plotted

        indices = np.sort(indices[indices >= 0])                # Sort data for plotting and remove negative indices (-1 from unique at 0)
        unique_data = data[indices]                             # Grab data at indices

        plt.plot(indices, unique_data, linestyle='dotted')      # Plot dotted line so we can see original underneath
        plt.show(block=False)

data = np.array([i // 100 for i in range(1000)])
fig, ax = plt.subplots()

start = time.time()
ax.plot(data)
plt.show(block=False)
end = time.time()
print(f'Normal plotting: {end - start} seconds')

start = time.time()
plt_uniques(data, ax)
end = time.time()
print(f'Unique plotting: {end - start} seconds')

plt_uniques(data=np.array([[i // 100 for i in range(1000)], [i // 100 + 1 for i in range(1000)]]))
plt_uniques(data=np.array([[[i // 100 for i in range(1000)], [i // 100 + 1 for i in range(1000)]], [[i // 100 for i in range(1000)], [i // 100 + 1 for i in range(1000)]]]))
try:
    plt_uniques(data = np.array(0))
except Exception as e:
    print('Got expected error:', e)

plt.show()

When run, that outputs

Normal plotting: 0.0652000904083252 seconds
Unique plotting: 0.002001047134399414 seconds
Got expected error: expected data.shape >= 1 but data.shape is ()

which shows a decent speed up.

The function in the above code would likely go in the pyripherals.utils module.