Skip to content

Commit 0a47fd2

Browse files
Merge branch 'develop' into env_localise
2 parents 7903907 + a5d67c7 commit 0a47fd2

9 files changed

Lines changed: 329 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ Attention: The newest changes should be on top -->
3333
### Added
3434

3535
- ENH: Air brakes controller functions now support 8-parameter signature [#854](https://github.com/RocketPy-Team/RocketPy/pull/854)
36-
- ENH: Add save functionality to `_MonteCarloPlots.all` method [#848](https://github.com/RocketPy-Team/RocketPy/pull/848)
36+
- ENH: add animations for motor propellant mass and tank fluid volumes [#894](https://github.com/RocketPy-Team/RocketPy/pull/894)
37+
- ENH: Add axial_acceleration attribute to the Flight class [#876](https://github.com/RocketPy-Team/RocketPy/pull/876)
3738
- ENH: Rail button bending moments calculation in Flight class [#893](https://github.com/RocketPy-Team/RocketPy/pull/893)
3839
- ENH: Built-in flight comparison tool (`FlightComparator`) to validate simulations against external data [#888](https://github.com/RocketPy-Team/RocketPy/pull/888)
3940
- ENH: Add persistent caching for ThrustCurve API [#881](https://github.com/RocketPy-Team/RocketPy/pull/881)

docs/user/motors/liquidmotor.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,21 @@ For example:
160160

161161
example_liquid.exhaust_velocity.plot(0, 5)
162162

163+
The tanks added to a ``LiquidMotor`` can now be animated to visualize
164+
how the liquid and gas volumes evolve during the burn.
165+
166+
To animate the tanks, we can use the ``animate_fluid_volume()`` method:
167+
168+
.. jupyter-execute::
169+
170+
example_liquid.animate_fluid_volume(fps=30)
171+
172+
Optionally, the animation can be saved to a GIF file:
173+
174+
.. jupyter-execute::
175+
176+
example_liquid.animate_fluid_volume(fps=30, save_as="liquid_motor.gif")
177+
163178
Alternatively, you can plot all the information at once:
164179

165180
.. jupyter-execute::

docs/user/motors/tanks.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,21 @@ We can see some outputs with:
263263
# Evolution of the Propellant center of mass position
264264
N2O_mass_tank.center_of_mass.plot()
265265

266+
All tank types now include a built-in method for animating the evolution
267+
of liquid and gas volumes over time. This visualization aids in understanding the dynamic behavior
268+
of the tank's contents. To animate the tanks, we can use the
269+
``animate_fluid_volume()`` method:
270+
271+
.. jupyter-execute::
272+
273+
N2O_mass_tank.animate_fluid_volume(fps=30)
274+
275+
Optionally, the animation can be saved to a GIF file:
276+
277+
.. jupyter-execute::
278+
279+
N2O_mass_tank.animate_fluid_volume(fps=30, save_as="mass_based_tank.gif")
280+
266281

267282
Ullage Based Tank
268283
-----------------

rocketpy/plots/motor_plots.py

Lines changed: 67 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,71 @@ 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.Supported file
532+
ending is: .gif
533+
fps : int, optional
534+
Frames per second for the animation. Default is 30.
535+
536+
Returns
537+
-------
538+
matplotlib.animation.FuncAnimation
539+
The created animation object.
540+
"""
541+
542+
# Extract time and mass data
543+
times = self.motor.propellant_mass.x_array
544+
values = self.motor.propellant_mass.y_array
545+
546+
# Create figure and axis
547+
fig, ax = plt.subplots()
548+
549+
# Configure axis
550+
ax.set_xlim(times[0], times[-1])
551+
ax.set_ylim(min(values), max(values))
552+
ax.set_xlabel("Time (s)")
553+
ax.set_ylabel("Propellant Mass (kg)")
554+
ax.set_title("Propellant Mass Evolution")
555+
556+
# Create line and current point marker
557+
(line,) = ax.plot([], [], lw=2, color="blue", label="Propellant Mass")
558+
(point,) = ax.plot([], [], "ko")
559+
560+
ax.legend()
561+
562+
# Initialization
563+
def init():
564+
line.set_data([], [])
565+
point.set_data([], [])
566+
return line, point
567+
568+
# Update per frame
569+
def update(frame_index):
570+
line.set_data(times[: frame_index + 1], values[: frame_index + 1])
571+
point.set_data([times[frame_index]], [values[frame_index]])
572+
return line, point
573+
574+
# Build animation
575+
animation = FuncAnimation(
576+
fig,
577+
update,
578+
frames=len(times),
579+
init_func=init,
580+
interval=1000 / fps,
581+
blit=True,
582+
)
583+
584+
# Show or save animation
585+
show_or_save_animation(animation, filename, fps=fps)
586+
587+
return animation
588+
523589
def all(self):
524590
"""Prints out all graphs available about the Motor. It simply calls
525591
all the other plotter methods in this class.

rocketpy/plots/plot_helpers.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,37 @@ 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 ending 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+
animation.save(filename, fps=fps, writer="pillow")
100+
101+
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. Supported file
192+
ending is: .gif
193+
fps : int, optional
194+
Frames per second for the animation. Default is 30.
195+
196+
Returns
197+
-------
198+
matplotlib.animation.FuncAnimation
199+
The created animation object.
200+
"""
201+
202+
t_start, t_end = self.flux_time
203+
times = np.linspace(t_start, t_end, 200)
204+
205+
liquid_values = self.tank.liquid_volume.get_value(times)
206+
gas_values = self.tank.gas_volume.get_value(times)
207+
208+
fig, ax = plt.subplots()
209+
210+
ax.set_xlim(times[0], times[-1])
211+
ax.set_ylim(0, max(liquid_values.max(), gas_values.max()) * 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+
for item in (line_liquid, line_gas, point_liquid, point_gas):
226+
item.set_data([], [])
227+
return line_liquid, line_gas, point_liquid, point_gas
228+
229+
def update(frame_index):
230+
# Liquid part
231+
line_liquid.set_data(
232+
times[: frame_index + 1], liquid_values[: frame_index + 1]
233+
)
234+
point_liquid.set_data([times[frame_index]], [liquid_values[frame_index]])
235+
236+
# Gas part
237+
line_gas.set_data(times[: frame_index + 1], gas_values[: frame_index + 1])
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.

rocketpy/simulation/flight.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2584,6 +2584,15 @@ def acceleration(self):
25842584
"""Rocket acceleration magnitude as a Function of time."""
25852585
return (self.ax**2 + self.ay**2 + self.az**2) ** 0.5
25862586

2587+
@funcify_method("Time (s)", "Axial Acceleration (m/s²)", "spline", "zero")
2588+
def axial_acceleration(self):
2589+
"""Axial acceleration magnitude as a Function of time."""
2590+
return (
2591+
self.ax * self.attitude_vector_x
2592+
+ self.ay * self.attitude_vector_y
2593+
+ self.az * self.attitude_vector_z
2594+
)
2595+
25872596
@cached_property
25882597
def max_acceleration_power_on_time(self):
25892598
"""Time at which the rocket reaches its maximum acceleration during

tests/unit/simulation/test_flight.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,37 @@ def test_max_values(flight_calisto_robust):
413413
assert pytest.approx(285.94948, rel=rtol) == test.max_speed
414414

415415

416+
@pytest.mark.parametrize(
417+
"flight_time_attr",
418+
["t_initial", "out_of_rail_time", "apogee_time", "t_final"],
419+
)
420+
def test_axial_acceleration(flight_calisto_custom_wind, flight_time_attr):
421+
"""Tests the axial_acceleration property by manually calculating the
422+
dot product of the acceleration vector and the attitude vector at
423+
specific time steps.
424+
425+
Parameters
426+
----------
427+
flight_calisto_custom_wind : rocketpy.Flight
428+
Flight object to be tested.
429+
flight_time_attr : str
430+
The name of the attribute of the flight object that contains the time
431+
of the point to be tested.
432+
"""
433+
flight = flight_calisto_custom_wind
434+
t = getattr(flight, flight_time_attr)
435+
436+
calculated_axial_acc = flight.axial_acceleration(t)
437+
438+
expected_axial_acc = (
439+
flight.ax(t) * flight.attitude_vector_x(t)
440+
+ flight.ay(t) * flight.attitude_vector_y(t)
441+
+ flight.az(t) * flight.attitude_vector_z(t)
442+
)
443+
444+
assert pytest.approx(expected_axial_acc, abs=1e-9) == calculated_axial_acc
445+
446+
416447
def test_effective_rail_length(flight_calisto_robust, flight_calisto_nose_to_tail):
417448
"""Tests the effective rail length of the flight simulation. The expected
418449
values are calculated by hand, and should be valid as long as the rail

0 commit comments

Comments
 (0)