0

I am trying to make a script which will show animation of 100 moving points. Every point is going to make 100 steps and after it, every point is replaced by different point which will make another 100 steps. And I would like to do that process like 1k times (1000 generations of points). In every generations- 100 points able to do 100 steps. I read coordinates from pickles, but I have no idea how should I animate this actions. I wrote some code but I kinda know that it's wrong and I don't know what should I do next. I am waiting for some help ;) PS.: coordinates are save as a tensors

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import pickle

fig, ax = plt.subplots()
xdata, ydata = [], []
ln, = plt.plot([], [], 'ro')


def init():
    ax.set_xlim(0, 20) 
    ax.set_ylim(0, 20)
    return ln,


def update(i):
    with open("output.txt", "rb") as file:
        try:
            while True:
                for point, coordinate in pickle.load(file).items():
                    for i in range(100):
                        if point == i:
                            xdata.append(coordinate.cpu().numpy()[0]) 
                            ydata.append(coordinate.cpu().numpy()[1])
                            ln.set_data(xdata, ydata)
                            return ln,
        except EOFError:
            pass

ani = FuncAnimation(fig, update, np.arange(1, 1000), init_func=init, blit=True)
plt.show()

output.txt is a huge file whose contents were generated like this:

output = open("output.txt", "wb")
for i in range(number_of_points):
    self.points[i].point_step()
    points = { i: self.points[i].point_location }
    pickle.dump(points, output)
    output.close()
  • Could you upload the output.txt file, please? – Jack Aug 28 '19 at 17:33
  • `update` takes `i` as an argument, but you are also using `for i in range(100)` within the body of the `update` function. If you remove the `for-loop`, then `FuncAnimation` will call `update` and pass a frame number as the argument to `update`. – unutbu Aug 28 '19 at 17:40
  • If the `output.txt` file is not too large, it would be more efficient to read all the data *once* outside of the `update` function, load all the data into a global variable, and then define `xdata` and `ydata` to be slices of that data inside `update`. – unutbu Aug 28 '19 at 17:42
  • The file is huge, but the point is that I save data like: –  Aug 28 '19 at 17:43
  • output = open("output.txt", "wb") for i in range(number_of_points): self.points[i].point_step() agents = { i: self.points[i].point_location } pickle.dump(points, output) output.close() –  Aug 28 '19 at 17:43
  • Code is not very readable inside comments, so I moved your code into the body of your question. Please check the edit, and maybe simplify it to make the question self-contained. What is the relationship between `self.points` and `points`? Are they the same? Can `agents` be removed? – unutbu Aug 28 '19 at 17:47
  • Right know I deleted the loop and every point is printed one by one (steps are separated in time). Do you know how should code looks like to update them in the same time (every point making move at the same moment)? –  Aug 28 '19 at 17:48
  • Agents and points are the same objects. I edited the codeblock in question. Points is the same what self.points, but without some parameters. I write to the file just number of point and coordinate and I just would like to animate their movement. –  Aug 28 '19 at 17:53
  • Do you want to read 100 (new) items from `pickle.load(file).items()` each time `update` is called? – unutbu Aug 28 '19 at 17:54
  • Exactly. One call - one step of every point. –  Aug 28 '19 at 17:58
  • When the end of the file is reached, do you want the data to loop back to the beginning of the file? Is that the purpose of the `try..except` and `while`-loop? – unutbu Aug 28 '19 at 18:00
  • I don't know how long is the file, but no, not really. I have all generations of points in one file so I want to iterate it just once by all fields. –  Aug 28 '19 at 18:03
  • But I want to make a stop by every generation –  Aug 28 '19 at 18:04
  • So once per 100 steps of 100 points. –  Aug 28 '19 at 18:04

1 Answers1

1

If I understand correctly, the file, output.txt, contains a sequence of pickled dicts. Each dict is of the form

{ i: self.points[i].point_location }

Each call to update should read 100 (new) dicts from this file. We can do this by making a generator function, get_data, which yields items from the file one at a time. Then define

data = get_data(path)

outside of update and pass it to update as an argument (using FuncAnimation's fargs parameter.) Inside of update, the loop

for point, coordinate in itertools.islice(data, 100):

iterates through 100 items from data. Since data is an iterator, itertools.islice will yield 100 new items from data each time it is called.


import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import pickle
import itertools as IT


def init():
    ax.set_xlim(0, 20)
    ax.set_ylim(0, 20)
    return (pathcol,)


def pickleLoader(f):
    """
    Return generator which yields unpickled objects from file object f
    # Based on https://stackoverflow.com/a/18675864/190597 (Darko Veberic)
    """
    try:
        while True:
            yield pickle.load(f)
    except EOFError:
        pass


def get_data(path):
    with open(path, "rb") as f:
        for dct in pickleLoader(f):
            yield from dct.items()


def update(i, data, pathcol, texts, title, num_points):
    title.set_text("Generation {}".format(i))
    xdata, ydata, color = [], [], []
    for point, coordinate in IT.islice(data, num_points):
        texti = texts[point]
        x, y = coordinate.cpu().numpy()
        xdata.append(x)
        ydata.append(y)
        color.append(point)
        texti.set_position((x, y))
    color = np.array(color, dtype="float64")
    color /= num_points
    pathcol.set_color = color
    pathcol.set_offsets(np.column_stack([xdata, ydata]))

    return [pathcol, title] + texts


class Coord:
    # just to make the code runnable
    def __init__(self, coord):
        self.coord = coord

    def cpu(self):
        return Cpu(self.coord)


class Cpu:
    # just to make the code runnable
    def __init__(self, coord):
        self.coord = coord

    def numpy(self):
        return self.coord


def make_data(path, num_frames=1000, num_points=100):
    # just to make the code runnable
    with open(path, "wb") as output:
        for frame in range(num_frames):
            for i in range(num_points):
                points = {i: Coord(20 * np.random.random((2,)))}
                pickle.dump(points, output)


def _blit_draw(self, artists, bg_cache):
    # https://stackoverflow.com/a/17562747/190597 (tacaswell)
    # Handles blitted drawing, which renders only the artists given instead
    # of the entire figure.
    updated_ax = []
    for a in artists:
        # If we haven't cached the background for this axes object, do
        # so now. This might not always be reliable, but it's an attempt
        # to automate the process.
        if a.axes not in bg_cache:
            # bg_cache[a.axes] = a.figure.canvas.copy_from_bbox(a.axes.bbox)
            # change here
            bg_cache[a.axes] = a.figure.canvas.copy_from_bbox(a.axes.figure.bbox)
        a.axes.draw_artist(a)
        updated_ax.append(a.axes)

    # After rendering all the needed artists, blit each axes individually.
    for ax in set(updated_ax):
        # and here
        # ax.figure.canvas.blit(ax.bbox)
        ax.figure.canvas.blit(ax.figure.bbox)


# MONKEY PATCH!!
matplotlib.animation.Animation._blit_draw = _blit_draw

num_points = 100
num_frames = 1000
fig, ax = plt.subplots()
pathcol = ax.scatter(
    [0] * num_points, [0] * num_points, c=np.linspace(0, 1, num_points), s=100
)
title = ax.text(
    0.5,
    1.05,
    "",
    transform=ax.transAxes,
    horizontalalignment="center",
    fontsize=15,
    animated=True,
)

texts = []
for i in range(num_points):
    t = ax.text(0, 0, str(i), fontsize=10, animated=True)
    texts.append(t)

path = "/tmp/output.txt"
make_data(path, num_frames, num_points)  # just to make the code runnable
data = get_data(path)

ani = FuncAnimation(
    fig,
    update,
    range(1, num_frames + 1),
    init_func=init,
    blit=True,
    fargs=(data, pathcol, texts, title, num_points),
    interval=1000,  # decrease to speed up the animation
    repeat=False,
)
plt.show()
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Ok. I think it's work but to determine it I need every point name with id. Let's say that id of the point is "i" during saving points = { i: self.points[i].point_location }. How to plot number of point instead of red dot? The animation is pretty chaotic and it's hard to say if it's work. How to add counter of the generation and steps to the plot? Sorry, that I ask a lot, but I'm new in the matplotlib. Thanks for your post :) –  Aug 28 '19 at 21:52
  • Instead of animating plot numbers, how about assigning a different color to each dot? – unutbu Aug 28 '19 at 22:00
  • For 100 points color is illegible. But for numbers if I will set a proper interval it should be easy to observe how the single point has changed position. –  Aug 28 '19 at 22:13
  • Text labels on the points, and an animated generation counter add. – unutbu Aug 28 '19 at 23:32