From 575b50fe7b9a0eebf33f289f4f72f180f1e6c919 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Mon, 8 Apr 2019 22:17:03 +0100 Subject: [PATCH 1/6] Added method to add blocks to an animation --- animatplot/animation.py | 62 +++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/animatplot/animation.py b/animatplot/animation.py index ce19522..c4b953c 100644 --- a/animatplot/animation.py +++ b/animatplot/animation.py @@ -4,6 +4,7 @@ import numpy as np from animatplot import Timeline +from animatplot.blocks.base import Block class Animation: @@ -33,17 +34,33 @@ def __init__(self, blocks, timeline=None, fig=None): else: self.timeline = timeline + self.fig = plt.gcf() if fig is None else fig + + self.animation = self._animate(blocks) + + def _animate(self, blocks): + """ + + Parameters + ---------- + blocks + + Returns + ------- + + """ _len_time = len(self.timeline) for block in blocks: if len(block) != _len_time: - raise ValueError("All blocks must animate for the same amount of time") + print(len(block)) + raise ValueError( + "All blocks must animate for the same amount of time") self.blocks = blocks - self.fig = plt.gcf() if fig is None else fig self._has_slider = False self._pause = False - def animate(i): + def update_all(i): updates = [] for block in self.blocks: updates.append(block._update(self.timeline.index)) @@ -52,11 +69,8 @@ def animate(i): self.timeline._update() return updates - self.animation = FuncAnimation( - self.fig, animate, - frames=self.timeline._len, - interval=1000/self.timeline.fps - ) + return FuncAnimation(self.fig, update_all, frames=self.timeline._len, + interval=1000 / self.timeline.fps) def toggle(self, ax=None): """Creates a play/pause button to start/stop the animation @@ -176,7 +190,8 @@ def save_gif(self, filename): the name of the file to be created without the file extension """ self.timeline.index -= 1 # required for proper starting point for save - self.animation.save(filename+'.gif', writer=PillowWriter(fps=self.timeline.fps)) + self.animation.save(filename+'.gif', + writer=PillowWriter(fps=self.timeline.fps)) def save(self, *args, **kwargs): """Saves an animation @@ -185,3 +200,32 @@ def save(self, *args, **kwargs): """ self.timeline.index -= 1 # required for proper starting point for save self.animation.save(*args, **kwargs) + + def add(self, new): + """ + Updates the animation object by adding additional blocks. + + Parameters + ---------- + new : animatplot.animation, or list of animatplot.block.Block objects + Either blocks to add to animation instance, or another animation + instance whose blocks should be combined with this animation. + """ + + if isinstance(new, Animation): + raise NotImplementedError("Cannot append to animation instances yet") + + else: + if not isinstance(new, list): + new = [new] + + for i, block in enumerate(new): + if not isinstance(block, Block): + err = f"Block number {i} passed is of type {type(block)}, " \ + f" not of type animatplot.blocks.Block (or a subclass)." + raise TypeError(err) + + self.blocks.append(block) + + self.animation = self._animate(self.blocks) + return self From 71e077b957a5bce4b72cb0fa5badc992ce487e46 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Mon, 8 Apr 2019 22:17:18 +0100 Subject: [PATCH 2/6] Added tests --- tests/test_animation.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_animation.py b/tests/test_animation.py index 203bf74..bb79beb 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -5,6 +5,8 @@ import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import PillowWriter +import numpy.testing as npt + import animatplot as amp from tests.tools import animation_compare @@ -41,3 +43,40 @@ def test_save(): anim.save_gif(base+'save') plt.close('all') assert os.path.exists(base+'save.gif') + + +@pytest.fixture() +def line_block(): + def make_line_block(t_length=5): + x = np.linspace(0, 1, 10) + t = np.linspace(0, 1, t_length) + x_grid, t_grid = np.meshgrid(x, t) + y_data = np.sin(2 * np.pi * (x_grid + t_grid)) + + return amp.blocks.Line(x, y_data) + return make_line_block + + +class TestAdd: + def test_add_blocks(self, line_block): + anim = amp.Animation([line_block()]) + anim2 = anim.add(line_block()) + + assert isinstance(anim2, amp.Animation) + assert len(anim2.blocks) == 2 + for actual in anim2.blocks: + assert len(actual) == 5 + npt.assert_equal(actual.line.get_xdata(), + np.linspace(0, 1, 10)) + + def test_wrong_length_block(self, line_block): + anim = amp.Animation([line_block()]) + + with pytest.raises(ValueError): + anim.add(line_block(t_length=6)) + + def test_wrong_type(self, line_block): + anim = amp.Animation([line_block()]) + + with pytest.raises(TypeError): + anim.add('not a block') From 257ed0ae51ca9e16640797fe25276016a2b619df Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Apr 2019 00:40:14 +0100 Subject: [PATCH 3/6] Extended add method to accept animations as well as lists of blocks --- animatplot/animation.py | 51 +++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/animatplot/animation.py b/animatplot/animation.py index c4b953c..ccaab76 100644 --- a/animatplot/animation.py +++ b/animatplot/animation.py @@ -36,25 +36,14 @@ def __init__(self, blocks, timeline=None, fig=None): self.fig = plt.gcf() if fig is None else fig - self.animation = self._animate(blocks) + self.animation = self._animate(blocks, self.timeline) - def _animate(self, blocks): - """ - - Parameters - ---------- - blocks - - Returns - ------- - - """ - _len_time = len(self.timeline) + def _animate(self, blocks, timeline): + _len_time = len(timeline) for block in blocks: if len(block) != _len_time: - print(len(block)) - raise ValueError( - "All blocks must animate for the same amount of time") + raise ValueError("All blocks must animate for the same amount " + "of time") self.blocks = blocks self._has_slider = False @@ -205,27 +194,35 @@ def add(self, new): """ Updates the animation object by adding additional blocks. + The new blocks can be passed as a list, or as part of a second animaion. + If passed as part of a new animation, the timeline of this new + animation object will replace the old one. + Parameters ---------- - new : animatplot.animation, or list of animatplot.block.Block objects + new : amp.animation.Animation, or list of amp.block.Block objects Either blocks to add to animation instance, or another animation instance whose blocks should be combined with this animation. """ if isinstance(new, Animation): - raise NotImplementedError("Cannot append to animation instances yet") + new_blocks = new.blocks + new_timeline = new.timeline else: if not isinstance(new, list): - new = [new] + new_blocks = [new] + else: + new_blocks = new + new_timeline = self.timeline - for i, block in enumerate(new): - if not isinstance(block, Block): - err = f"Block number {i} passed is of type {type(block)}, " \ - f" not of type animatplot.blocks.Block (or a subclass)." - raise TypeError(err) + for i, block in enumerate(new_blocks): + if not isinstance(block, Block): + raise TypeError(f"Block number {i} passed is of type " + f"{type(block)}, not of type " + f"animatplot.blocks.Block (or a subclass)") - self.blocks.append(block) + self.blocks.append(block) - self.animation = self._animate(self.blocks) - return self + self.animation = self._animate(self.blocks, new_timeline) + return self From 442c6583c1d6c99e555b972b63d33417784bd6b7 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Apr 2019 00:40:32 +0100 Subject: [PATCH 4/6] Tests for adding animations to animations --- tests/test_animation.py | 60 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/tests/test_animation.py b/tests/test_animation.py index bb79beb..70f81f9 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -1,16 +1,21 @@ -from matplotlib.testing import setup -setup() import os + import pytest -import numpy as np + +import matplotlib.testing import matplotlib.pyplot as plt from matplotlib.animation import PillowWriter + +import numpy as np import numpy.testing as npt import animatplot as amp from tests.tools import animation_compare +matplotlib.testing.setup() + + @pytest.mark.xfail @animation_compare(baseline_images='Animation/controls', nframes=5, tol=.5) def test_controls(): @@ -57,7 +62,25 @@ def make_line_block(t_length=5): return make_line_block -class TestAdd: +@pytest.fixture() +def line_anim(): + def make_line_anim(t_length=5, timeline=False): + x = np.linspace(0, 1, 10) + t = np.linspace(0, 1, t_length) + x_grid, t_grid = np.meshgrid(x, t) + y_data = np.sin(2 * np.pi * (x_grid + t_grid)) + + block = amp.blocks.Line(x, y_data) + + if timeline: + return amp.Animation([block], timeline=amp.Timeline(t)) + else: + return amp.Animation([block]) + + return make_line_anim + + +class TestAddBlocks: def test_add_blocks(self, line_block): anim = amp.Animation([line_block()]) anim2 = anim.add(line_block()) @@ -80,3 +103,32 @@ def test_wrong_type(self, line_block): with pytest.raises(TypeError): anim.add('not a block') + + +class TestAddAnimations: + def test_add_animations(self, line_anim): + anim = line_anim() + anim2 = anim.add(line_anim()) + + assert isinstance(anim2, amp.Animation) + + def test_add_animation_with_timeline(self, line_anim): + anim = line_anim() + anim2 = anim.add(line_anim(timeline=True)) + + assert isinstance(anim2, amp.Animation) + assert len(anim2.timeline) == 5 + + @pytest.mark.xfail(reason="This isn't testing desired behaviour" + "because both timelines are the same") + def test_add_animations_both_with_timelines(self, line_anim): + anim = line_anim(timeline=True) + anim2 = anim.add(line_anim(timeline=True)) + + assert isinstance(anim2, amp.Animation) + assert len(anim2.timeline) == 5 + + def test_add_animations_different_lengths(self, line_anim): + anim = line_anim() + with pytest.raises(ValueError): + anim.add(line_anim(t_length=6)) From 6f7c5d7f011386e66242321f91f09be4df7deaff Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Apr 2019 10:59:58 +0100 Subject: [PATCH 5/6] Now properly replaces timeline when adding animations --- animatplot/animation.py | 12 ++++++------ tests/test_animation.py | 35 +++++++++++++++++++++-------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/animatplot/animation.py b/animatplot/animation.py index ccaab76..20948c5 100644 --- a/animatplot/animation.py +++ b/animatplot/animation.py @@ -27,6 +27,11 @@ class Animation: a matplotlib animation returned from FuncAnimation """ def __init__(self, blocks, timeline=None, fig=None): + self.fig = plt.gcf() if fig is None else fig + + self.animation = self._animate(blocks, timeline) + + def _animate(self, blocks, timeline): if timeline is None: self.timeline = Timeline(range(len(blocks[0]))) elif not isinstance(timeline, Timeline): @@ -34,12 +39,7 @@ def __init__(self, blocks, timeline=None, fig=None): else: self.timeline = timeline - self.fig = plt.gcf() if fig is None else fig - - self.animation = self._animate(blocks, self.timeline) - - def _animate(self, blocks, timeline): - _len_time = len(timeline) + _len_time = len(self.timeline) for block in blocks: if len(block) != _len_time: raise ValueError("All blocks must animate for the same amount " diff --git a/tests/test_animation.py b/tests/test_animation.py index 70f81f9..27933d1 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -83,11 +83,11 @@ def make_line_anim(t_length=5, timeline=False): class TestAddBlocks: def test_add_blocks(self, line_block): anim = amp.Animation([line_block()]) - anim2 = anim.add(line_block()) + result = anim.add(line_block()) - assert isinstance(anim2, amp.Animation) - assert len(anim2.blocks) == 2 - for actual in anim2.blocks: + assert isinstance(result, amp.Animation) + assert len(result.blocks) == 2 + for actual in result.blocks: assert len(actual) == 5 npt.assert_equal(actual.line.get_xdata(), np.linspace(0, 1, 10)) @@ -108,25 +108,32 @@ def test_wrong_type(self, line_block): class TestAddAnimations: def test_add_animations(self, line_anim): anim = line_anim() - anim2 = anim.add(line_anim()) + result = anim.add(line_anim()) - assert isinstance(anim2, amp.Animation) + assert isinstance(result, amp.Animation) def test_add_animation_with_timeline(self, line_anim): anim = line_anim() - anim2 = anim.add(line_anim(timeline=True)) + result = anim.add(line_anim(timeline=True)) - assert isinstance(anim2, amp.Animation) - assert len(anim2.timeline) == 5 + assert isinstance(result, amp.Animation) + assert len(result.timeline) == 5 - @pytest.mark.xfail(reason="This isn't testing desired behaviour" - "because both timelines are the same") def test_add_animations_both_with_timelines(self, line_anim): anim = line_anim(timeline=True) - anim2 = anim.add(line_anim(timeline=True)) + anim2 = line_anim() + t = 10*np.arange(5) + anim2.timeline = amp.Timeline(t) - assert isinstance(anim2, amp.Animation) - assert len(anim2.timeline) == 5 + print(anim2.timeline) + + result = anim.add(anim2) + + print(result.timeline) + + assert isinstance(result, amp.Animation) + assert len(result.timeline) == 5 + npt.assert_equal(result.timeline.t, t) def test_add_animations_different_lengths(self, line_anim): anim = line_anim() From f322b4d2def78038822e3a87a97eaab3aa87b9b7 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Apr 2019 11:04:54 +0100 Subject: [PATCH 6/6] Animation.add() method now returns None --- animatplot/animation.py | 1 - tests/test_animation.py | 30 +++++++++++++----------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/animatplot/animation.py b/animatplot/animation.py index 20948c5..7bf99fe 100644 --- a/animatplot/animation.py +++ b/animatplot/animation.py @@ -225,4 +225,3 @@ def add(self, new): self.blocks.append(block) self.animation = self._animate(self.blocks, new_timeline) - return self diff --git a/tests/test_animation.py b/tests/test_animation.py index 27933d1..da3475a 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -83,11 +83,11 @@ def make_line_anim(t_length=5, timeline=False): class TestAddBlocks: def test_add_blocks(self, line_block): anim = amp.Animation([line_block()]) - result = anim.add(line_block()) + anim.add(line_block()) - assert isinstance(result, amp.Animation) - assert len(result.blocks) == 2 - for actual in result.blocks: + assert isinstance(anim, amp.Animation) + assert len(anim.blocks) == 2 + for actual in anim.blocks: assert len(actual) == 5 npt.assert_equal(actual.line.get_xdata(), np.linspace(0, 1, 10)) @@ -108,16 +108,16 @@ def test_wrong_type(self, line_block): class TestAddAnimations: def test_add_animations(self, line_anim): anim = line_anim() - result = anim.add(line_anim()) + anim.add(line_anim()) - assert isinstance(result, amp.Animation) + assert isinstance(anim, amp.Animation) def test_add_animation_with_timeline(self, line_anim): anim = line_anim() - result = anim.add(line_anim(timeline=True)) + anim.add(line_anim(timeline=True)) - assert isinstance(result, amp.Animation) - assert len(result.timeline) == 5 + assert isinstance(anim, amp.Animation) + assert len(anim.timeline) == 5 def test_add_animations_both_with_timelines(self, line_anim): anim = line_anim(timeline=True) @@ -125,15 +125,11 @@ def test_add_animations_both_with_timelines(self, line_anim): t = 10*np.arange(5) anim2.timeline = amp.Timeline(t) - print(anim2.timeline) + anim.add(anim2) - result = anim.add(anim2) - - print(result.timeline) - - assert isinstance(result, amp.Animation) - assert len(result.timeline) == 5 - npt.assert_equal(result.timeline.t, t) + assert isinstance(anim, amp.Animation) + assert len(anim.timeline) == 5 + npt.assert_equal(anim.timeline.t, t) def test_add_animations_different_lengths(self, line_anim): anim = line_anim()