Leo
Leo

Reputation: 1168

Real time data plotting from a high throughput source

I want to plot Real time in a way that updates fast.

The data I have:

The problem with my current plotting loop is that it runs slower than 62.5[Hz], meaning I miss some data coming in from serial port.

I am looking for any solution to this problem that allows for:

Here is my code, I am using random data to simulate the serial port data.

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

#extra plot debugging
hz_ = [] #list of speed
time_=[] #list for time vs Hz plot


 #store all data generated
store_data = np.zeros((1, 33))
 #only data to plot 
to_plot = np.zeros((1, 33)) 
 #color each line 
colours = [f"C{i}" for i in range (1,33)]

fig,ax = plt.subplots(1,1, figsize=(10,8))
ax.set_xlabel('time(s)')
ax.set_ylabel('y')
ax.set_ylim([0, 300])
ax.set_xlim([0, 200])

start_time = time.time()
for i in range (100):
    loop_time = time.time()
     #make data with col0=time and col[1:11] = y values
    data = np.random.randint(1,255,(1,32)).astype(float) #simulated data, usually comes in at 62.5 [Hz]
    data =  np.insert(data, 0, time.time()-start_time).reshape(1,33) #adding time for first column
    store_data = np.append(store_data, data , axis=0)
    to_plot = store_data[-100:,]
    
    for i in range(1, to_plot.shape[1]):
        ax.plot(to_plot[:,0], to_plot[:,i],c = colours[i-1], marker=(5, 2), linewidth=0, label=i)
        #ax.lines = ax.lines[-33:] #This soluition speeds it up, to clear old code. 

    fig.canvas.draw()  
    fig.canvas.flush_events()
    Hz = 1/(time.time()-loop_time)
     #for time vs Hz plot
    hz_.append(Hz)
    time_.append( time.time()-start_time)
    print(1/(time.time()-loop_time), "Hz - frequncy program loops at")
   
 #extra fig showing how speed drops off vs time
fig,ax = plt.subplots(1,1, figsize=(10,8))
fig.suptitle('Decreasingn Speed vs Time', fontsize=20)
ax.set_xlabel('time(s)')
ax.set_ylabel('Hz')

ax.plot(time_, hz_)
fig.show()

enter image description here

I also tried while using

ax.lines = ax.lines[-33:]

to remove older points, and this speed up the plotting, but still slower than the speed i aquire data. enter image description here

Any library/solution to make sure I collect all data and plot the general trendlines (so even not all points) is ok. Maybe something that runs acquiring data and plotting in parallel?

Upvotes: 4

Views: 7398

Answers (1)

norok2
norok2

Reputation: 26906

You could try to have two separate processes:

  • one for acquiring and storing the data
  • one for plotting the data

Below there are two basic scripts to get the idea. You first run gen.py which starts to generate numbers and save them in a file. Then, in the same directory, you can run plot.py which will read the last part of the file and will update the a Matplotlib plot.

Here is the gen.py script to generate data:

#!/usr/bin/env python3

import time
import random

LIMIT_TIME = 100  # s
DATA_FILENAME = "data.txt"


def gen_data(filename, limit_time):
    start_time = time.time()
    elapsed_time = time.time() - start_time
    with open(filename, "w") as f:
        while elapsed_time < limit_time:
            f.write(f"{time.time():30.12f} {random.random():30.12f}\n")  # produces 64 bytes
            f.flush()
            elapsed = time.time() - start_time
            

gen_data(DATA_FILENAME, LIMIT_TIME)

and here is the plot.py script to plot the data (reworked from this one):

#!/usr/bin/env python3


import io
import time
import matplotlib.pyplot as plt
import matplotlib as mpl
import matplotlib.animation


BUFFER_LEN = 64
DATA_FILENAME = "data.txt"
PLOT_LIMIT = 20
ANIM_FILENAME = "video.gif"


fig, ax = plt.subplots(1, 1, figsize=(10,8))
ax.set_title("Plot of random numbers from `gen.py`")
ax.set_xlabel("time / s")
ax.set_ylabel("random number / #")
ax.set_ylim([0, 1])


def get_data(filename, buffer_len, delay=0.0):
    with open(filename, "r") as f:
        f.seek(0, io.SEEK_END)
        data = f.read(buffer_len)
        if delay:
            time.sleep(delay)
    return data


def animate(i, xs, ys, limit=PLOT_LIMIT, verbose=False):
    # grab the data
    try:
        data = get_data(DATA_FILENAME, BUFFER_LEN)
        if verbose:
            print(data)
        x, y = map(float, data.split())
        if x > xs[-1]:
            # Add x and y to lists
            xs.append(x)
            ys.append(y)
            # Limit x and y lists to 10 items
            xs = xs[-limit:]
            ys = ys[-limit:]
        else:
            print(f"W: {time.time()} :: STALE!")
    except ValueError:
        print(f"W: {time.time()} :: EXCEPTION!")
    else:
        # Draw x and y lists
        ax.clear()
        ax.set_ylim([0, 1])
        ax.plot(xs, ys)


# save video (only to attach here) 
#anim = mpl.animation.FuncAnimation(fig, animate, fargs=([time.time()], [None]), interval=1, frames=3 * PLOT_LIMIT, repeat=False)
#anim.save(ANIM_FILENAME, writer='imagemagick', fps=10)
#print(f"I: Saved to `{ANIM_FILENAME}`")

# show interactively
anim = mpl.animation.FuncAnimation(fig, animate, fargs=([time.time()], [None]), interval=1)
plt.show()
plt.close()

Anim

Note that I have also included and commented out the portion of code that I used to generate the animated GIF above.

I believe this should be enough to get you going.

Upvotes: 4

Related Questions