diff --git a/src/roboticstoolbox/backends/PyPlot/PyPlot.py b/src/roboticstoolbox/backends/PyPlot/PyPlot.py index bdde273c7..a866d67e4 100644 --- a/src/roboticstoolbox/backends/PyPlot/PyPlot.py +++ b/src/roboticstoolbox/backends/PyPlot/PyPlot.py @@ -283,6 +283,8 @@ def step(self, dt: float = 0.05) -> None: :param dt: time step in seconds, defaults to 50 (0.05 s) :type dt: int, optional + :raises ValueError: if ``dt`` is not greater than zero in a window, where the + plot pauses for ``dt`` ``env.step(args)`` triggers an update of the 3D scene in the matplotlib window referenced by ``env``. @@ -295,10 +297,20 @@ def step(self, dt: float = 0.05) -> None: updated in the internal state of the robot object. - The control type is defined by the robot object, and not all robot objects support all control types. - - Execution is blocked for the specified interval + - In a window the execution is blocked for the specified interval, which + must be greater than zero: Matplotlib's ``pause(0)`` does not return, + since a timeout of zero means to wait until the event loop is stopped. + In a notebook there is no pause and ``dt`` of zero is fine. + .. versionchanged:: 1.5.0 + In a window, ``dt`` of zero or less raises ``ValueError``, it used to wait + for ever. """ - + if dt <= 0 and self.render_mode == "window": + raise ValueError( + f"dt is {dt}, it must be greater than zero: a pause of zero seconds " + "does not return" + ) # update the robot's state for rpl in self.robots: diff --git a/src/roboticstoolbox/backends/PyPlot/PyPlot2.py b/src/roboticstoolbox/backends/PyPlot/PyPlot2.py index 376322d88..c17aa3b5f 100644 --- a/src/roboticstoolbox/backends/PyPlot/PyPlot2.py +++ b/src/roboticstoolbox/backends/PyPlot/PyPlot2.py @@ -191,8 +191,17 @@ def step(self, dt: float = 0.05) -> None: by the robot object, and not all robot objects support all control types. + :param dt: time step in seconds, which must be greater than zero in a window, + where the plot pauses for ``dt``; in a notebook there is no pause + :raises ValueError: if ``dt`` is not greater than zero in a window: + Matplotlib's ``pause(0)`` does not return, since a timeout of zero means + to wait until the event loop is stopped """ - + if dt <= 0 and self.render_mode == "window": + raise ValueError( + f"dt is {dt}, it must be greater than zero: a pause of zero seconds " + "does not return" + ) self._step_robots(dt) diff --git a/tests/test_step_dt.py b/tests/test_step_dt.py new file mode 100644 index 000000000..a5608d6f3 --- /dev/null +++ b/tests/test_step_dt.py @@ -0,0 +1,82 @@ +""" +A time step of zero used to hang: ``plt.pause(0)`` waits until the event loop is stopped. +""" + +import matplotlib + +matplotlib.use("Agg") + +import matplotlib.pyplot as plt # noqa: E402 +import pytest # noqa: E402 + +import roboticstoolbox as rtb # noqa: E402 +from roboticstoolbox import ET2 # noqa: E402 +from roboticstoolbox.backends.PyPlot import PyPlot, PyPlot2 # noqa: E402 + +pytestmark = pytest.mark.timeout(20) + + +@pytest.fixture(autouse=True) +def close_figures(): + plt.close("all") + yield + plt.close("all") + + +@pytest.fixture +def env3d(): + env = PyPlot() + env.launch() + env.add(rtb.models.DH.Puma560(), readonly=True) + return env + + +@pytest.fixture +def env2d(): + env = PyPlot2() + env.launch() + env.add(rtb.Robot2(ET2.R() * ET2.tx(1) * ET2.R() * ET2.tx(1)), readonly=True) + return env + + +@pytest.mark.parametrize("dt", [0, 0.0, -0.05]) +def test_3d_step_needs_a_positive_dt(env3d, dt): + with pytest.raises(ValueError, match="greater than zero"): + env3d.step(dt) + + +@pytest.mark.parametrize("dt", [0, 0.0, -0.05]) +def test_2d_step_needs_a_positive_dt(env2d, dt): + with pytest.raises(ValueError, match="greater than zero"): + env2d.step(dt) + + +def inline_env(cls, robot): + env = cls() + env.launch(render_mode="notebook-inline") + env.add(robot, readonly=True) + return env + + +@pytest.mark.parametrize("dt", [0, 0.0]) +def test_in_a_notebook_there_is_no_pause_and_zero_is_fine(dt): + # the notebook animation draws a frame for each step and does not pause + inline_env(PyPlot, rtb.models.DH.Puma560()).step(dt) + inline_env( + PyPlot2, rtb.Robot2(ET2.R() * ET2.tx(1) * ET2.R() * ET2.tx(1)) + ).step(dt) + + +def test_a_small_dt_is_fine(env3d, env2d): + env3d.step(0.001) + env2d.step(0.001) + + +def test_the_default_dt_is_fine(env3d): + env3d.step() + + +def test_plot_with_a_zero_dt_raises_and_does_not_hang(): + puma = rtb.models.DH.Puma560() + with pytest.raises(ValueError, match="greater than zero"): + puma.plot(puma.qr, backend="pyplot", dt=0, block=False)