Skip to content

Commit 58b916e

Browse files
committed
ENH: add animations for motor propellant mass and tank fluid volumes
1 parent 793e5f6 commit 58b916e

File tree

4 files changed

+209
-3
lines changed

4 files changed

+209
-3
lines changed

rocketpy/plots/motor_plots.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import matplotlib.pyplot as plt
22
import numpy as np
33
from matplotlib.patches import Polygon
4+
from matplotlib.animation import FuncAnimation
45

5-
from ..plots.plot_helpers import show_or_save_plot
6+
from ..plots.plot_helpers import show_or_save_plot, show_or_save_animation
67

78

89
class _MotorPlots:
@@ -520,6 +521,70 @@ def _set_plot_properties(self, ax):
520521
plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
521522
plt.tight_layout()
522523

524+
def animate_propellant_mass(self, filename=None, fps=30):
525+
"""Animates the propellant mass of the motor as a function of time.
526+
527+
Parameters
528+
----------
529+
filename : str | None, optional
530+
The path the animation should be saved to. By default None, in which
531+
case the animation will be shown instead of saved.
532+
fps : int, optional
533+
Frames per second for the animation. Default is 30.
534+
535+
Returns
536+
-------
537+
matplotlib.animation.FuncAnimation
538+
The created animation object.
539+
"""
540+
541+
# Extract time and mass data
542+
times = self.motor.propellant_mass.times
543+
values = self.motor.propellant_mass.values
544+
545+
# Create figure and axis
546+
fig, ax = plt.subplots()
547+
548+
# Configure axis
549+
ax.set_xlim(times[0], times[-1])
550+
ax.set_ylim(min(values), max(values))
551+
ax.set_xlabel("Time (s)")
552+
ax.set_ylabel("Propellant Mass (kg)")
553+
ax.set_title("Propellant Mass Evolution")
554+
555+
# Create line and current point marker
556+
(line,) = ax.plot([], [], lw=2, color="blue", label="Propellant Mass")
557+
(point,) = ax.plot([], [], "ko")
558+
559+
ax.legend()
560+
561+
# Initialization
562+
def init():
563+
line.set_data([], [])
564+
point.set_data([], [])
565+
return line, point
566+
567+
# Update per frame
568+
def update(frame_index):
569+
line.set_data(times[:frame_index], values[:frame_index])
570+
point.set_data(times[frame_index], values[frame_index])
571+
return line, point
572+
573+
# Build animation
574+
animation = FuncAnimation(
575+
fig,
576+
update,
577+
frames=len(times),
578+
init_func=init,
579+
interval=1000 / fps,
580+
blit=True,
581+
)
582+
583+
# Show or save animation
584+
show_or_save_animation(animation, filename, fps=fps)
585+
586+
return animation
587+
523588
def all(self):
524589
"""Prints out all graphs available about the Motor. It simply calls
525590
all the other plotter methods in this class.

rocketpy/plots/plot_helpers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,38 @@ def show_or_save_fig(fig: Figure, filename=None):
6565
Path(filename).parent.mkdir(parents=True, exist_ok=True)
6666

6767
fig.savefig(filename, dpi=SAVEFIG_DPI)
68+
69+
70+
def show_or_save_animation(animation, filename=None, fps=30):
71+
"""Shows or saves the given matplotlib animation. If a filename is given,
72+
the animation will be saved. Otherwise, it will be shown.
73+
74+
Parameters
75+
----------
76+
animation : matplotlib.animation.FuncAnimation
77+
The animation object to be saved or shown.
78+
filename : str | None, optional
79+
The path the animation should be saved to, by default None. Supported
80+
file endings is: gif.
81+
fps : int, optional
82+
Frames per second when saving the animation. Default is 30.
83+
"""
84+
if filename is None:
85+
plt.show()
86+
else:
87+
file_ending = Path(filename).suffix
88+
supported_endings = [".gif"]
89+
90+
if file_ending not in supported_endings:
91+
raise ValueError(
92+
f"Unsupported file ending '{file_ending}'."
93+
f"Supported file endings are: {supported_endings}."
94+
)
95+
96+
# Before export, ensure the folder the file should go into exists
97+
Path(filename).parent.mkdir(parents=True, exist_ok=True)
98+
99+
if file_ending == ".gif":
100+
animation.save(filename, fps=fps, writer="pillow")
101+
102+
plt.close()

rocketpy/plots/tank_plots.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import matplotlib.pyplot as plt
22
import numpy as np
33
from matplotlib.patches import Polygon
4+
from matplotlib.animation import FuncAnimation
45

56
from rocketpy.mathutils.function import Function
67

7-
from .plot_helpers import show_or_save_plot
8+
from .plot_helpers import show_or_save_plot, show_or_save_animation
89

910

1011
class _TankPlots:
@@ -180,6 +181,77 @@ def fluid_center_of_mass(self, filename=None):
180181
ax.legend(["Liquid", "Gas", "Total"])
181182
show_or_save_plot(filename)
182183

184+
def animate_fluid_volume(self, filename=None, fps=30):
185+
"""Animates the liquid and gas volumes inside the tank as a function of time.
186+
187+
Parameters
188+
----------
189+
filename : str | None, optional
190+
The path the animation should be saved to. By default None, in which
191+
case the animation will be shown instead of saved.
192+
fps : int, optional
193+
Frames per second for the animation. Default is 30.
194+
195+
Returns
196+
-------
197+
matplotlib.animation.FuncAnimation
198+
The created animation object.
199+
"""
200+
201+
t_start, t_end = self.flux_time
202+
times = np.linspace(t_start, t_end, 200)
203+
204+
liquid_values = self.tank.liquid_volume.get_value(times)
205+
gas_values = self.tank.gas_volume.get_value(times)
206+
207+
fig, ax = plt.subplots()
208+
209+
ax.set_xlim(times[0], times[-1])
210+
max_val = max(liquid_values.max(), gas_values.max())
211+
ax.set_ylim(0, max_val * 1.1)
212+
213+
ax.set_xlabel("Time (s)")
214+
ax.set_ylabel("Volume (m³)")
215+
ax.set_title("Liquid/Gas Volume Evolution")
216+
(line_liquid,) = ax.plot([], [], lw=2, color="blue", label="Liquid Volume")
217+
(line_gas,) = ax.plot([], [], lw=2, color="red", label="Gas Volume")
218+
219+
(point_liquid,) = ax.plot([], [], "ko")
220+
(point_gas,) = ax.plot([], [], "ko")
221+
222+
ax.legend()
223+
224+
def init():
225+
line_liquid.set_data([], [])
226+
line_gas.set_data([], [])
227+
point_liquid.set_data([], [])
228+
point_gas.set_data([], [])
229+
return line_liquid, line_gas, point_liquid, point_gas
230+
231+
def update(frame_index):
232+
# Liquid part
233+
line_liquid.set_data(times[:frame_index], liquid_values[:frame_index])
234+
point_liquid.set_data(times[frame_index], liquid_values[frame_index])
235+
236+
# Gas part
237+
line_gas.set_data(times[:frame_index], gas_values[:frame_index])
238+
point_gas.set_data(times[frame_index], gas_values[frame_index])
239+
240+
return line_liquid, line_gas, point_liquid, point_gas
241+
242+
animation = FuncAnimation(
243+
fig,
244+
update,
245+
frames=len(times),
246+
init_func=init,
247+
interval=1000 / fps,
248+
blit=True,
249+
)
250+
251+
show_or_save_animation(animation, filename, fps=fps)
252+
253+
return animation
254+
183255
def all(self):
184256
"""Prints out all graphs available about the Tank. It simply calls
185257
all the other plotter methods in this class.

tests/unit/test_plots.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
from unittest.mock import MagicMock, patch
33

44
import matplotlib.pyplot as plt
5+
from matplotlib.animation import FuncAnimation
56
import pytest
67

78
from rocketpy.plots.compare import Compare
8-
from rocketpy.plots.plot_helpers import show_or_save_fig, show_or_save_plot
9+
from rocketpy.plots.plot_helpers import show_or_save_fig, show_or_save_plot, show_or_save_animation
910

1011

1112
@patch("matplotlib.pyplot.show")
@@ -89,3 +90,36 @@ def test_show_or_save_fig(filename):
8990
else:
9091
assert os.path.exists(filename)
9192
os.remove(filename)
93+
94+
95+
@pytest.mark.parametrize("filename", [None, "test.gif"])
96+
@patch("matplotlib.pyplot.show")
97+
def test_show_or_save_animation(mock_show, filename):
98+
"""This test is to check if the show_or_save_animation function is
99+
working properly.
100+
101+
Parameters
102+
----------
103+
mock_show :
104+
Mocks the matplotlib.pyplot.show() function to avoid showing the animation.
105+
filename : str
106+
Name of the file to save the animation. If None, the animation will be
107+
shown instead.
108+
"""
109+
110+
# Create a simple animation object
111+
fig, ax = plt.subplots()
112+
113+
def update(frame):
114+
ax.plot([0, frame], [0, frame])
115+
return ax
116+
117+
animation = FuncAnimation(fig, update, frames=5)
118+
119+
show_or_save_animation(animation, filename)
120+
121+
if filename is None:
122+
mock_show.assert_called_once()
123+
else:
124+
assert os.path.exists(filename)
125+
os.remove(filename)

0 commit comments

Comments
 (0)