Skip to content

Commit d49c40e

Browse files
ENH: Create a rocketpy file to store flight simulations (#800)
* ENH: added .rpy file functionality (see issue 668) This commit add 'save_to_rpy' and 'load_from_rpy' functions, that allows saving and loading flights. * MNT: adjusting minor changes to .rpy functions and tests. Formatted docstrings correctly. Reverted duplication of `test_encoding.py` files. Version warning will be called when loaded version is more recent. * MNT: incorporating previous comments Change file management from os to Path Adjust docstrings * DOC: Added comment about outputs in `to_dict` method * MNT: Refactoring `RocketPyDecoder` unpacking operation and other small adjustments * DOC: update changelog * STY: formatted according to ruff * MNT: changing `str | Path` operation to support Python 3.9 * MNT: fixed trailing commas on .rpy and added shield against `ruff` formatting .rpy and .json files * MNT: fixing error related to `test_flight_save_load_no_resimulate` When `include_outputs` were set to `True`, it would try to include the additional data into the flight, breaking the test * MNT: fixing a typo and adding comment on test coverage --------- Co-authored-by: Gui-FernandesBR <[email protected]>
1 parent 6bf70f3 commit d49c40e

File tree

8 files changed

+43655
-16
lines changed

8 files changed

+43655
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Attention: The newest changes should be on top -->
3232

3333
### Added
3434

35+
- ENH: Create a rocketpy file to store flight simulations [#800](https://github.com/RocketPy-Team/RocketPy/pull/800)
3536
- ENH: Support for the RSE file format has been added to the library [#798](https://github.com/RocketPy-Team/RocketPy/pull/798)
3637

3738
### Changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ skip-magic-trailing-comma = false
101101
line-ending = "auto"
102102
docstring-code-format = false
103103
docstring-code-line-length = "dynamic"
104+
exclude = ["**/*.json", "**/*.rpy"]
104105

105106

106107
[tool.ruff.lint.pydocstyle]

rocketpy/_encoders.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import numpy as np
88

99
from rocketpy.mathutils.function import Function
10+
from rocketpy.prints.flight_prints import _FlightPrints
11+
from rocketpy.plots.flight_plots import _FlightPlots
1012

1113

1214
class RocketPyEncoder(json.JSONEncoder):
@@ -75,6 +77,7 @@ class RocketPyDecoder(json.JSONDecoder):
7577
different types of objects from a JSON supported format."""
7678

7779
def __init__(self, *args, **kwargs):
80+
self.resimulate = kwargs.pop("resimulate", False)
7881
super().__init__(object_hook=self.object_hook, *args, **kwargs)
7982

8083
def object_hook(self, obj):
@@ -84,7 +87,56 @@ def object_hook(self, obj):
8487
try:
8588
class_ = get_class_from_signature(signature)
8689

87-
if hasattr(class_, "from_dict"):
90+
if class_.__name__ == "Flight" and not self.resimulate:
91+
new_flight = class_.__new__(class_)
92+
new_flight.prints = _FlightPrints(new_flight)
93+
new_flight.plots = _FlightPlots(new_flight)
94+
attributes = (
95+
"rocket",
96+
"env",
97+
"rail_length",
98+
"inclination",
99+
"heading",
100+
"initial_solution",
101+
"terminate_on_apogee",
102+
"max_time",
103+
"max_time_step",
104+
"min_time_step",
105+
"rtol",
106+
"atol",
107+
"time_overshoot",
108+
"name",
109+
"solution",
110+
"out_of_rail_time",
111+
"apogee_time",
112+
"apogee",
113+
"parachute_events",
114+
"impact_state",
115+
"impact_velocity",
116+
"x_impact",
117+
"y_impact",
118+
"t_final",
119+
"flight_phases",
120+
"ax",
121+
"ay",
122+
"az",
123+
"out_of_rail_time_index",
124+
"function_evaluations",
125+
"alpha1",
126+
"alpha2",
127+
"alpha3",
128+
"R1",
129+
"R2",
130+
"R3",
131+
"M1",
132+
"M2",
133+
"M3",
134+
)
135+
for attribute in attributes:
136+
setattr(new_flight, attribute, obj[attribute])
137+
new_flight.t_initial = new_flight.initial_solution[0]
138+
return new_flight
139+
elif hasattr(class_, "from_dict"):
88140
return class_.from_dict(obj)
89141
else:
90142
# Filter keyword arguments

rocketpy/simulation/flight.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3434,24 +3434,43 @@ def to_dict(self, include_outputs=False):
34343434
"time_overshoot": self.time_overshoot,
34353435
"name": self.name,
34363436
"equations_of_motion": self.equations_of_motion,
3437+
# The following outputs are essential to run all_info method
3438+
"solution": self.solution,
3439+
"out_of_rail_time": self.out_of_rail_time,
3440+
"out_of_rail_time_index": self.out_of_rail_time_index,
3441+
"apogee_time": self.apogee_time,
3442+
"apogee": self.apogee,
3443+
"parachute_events": self.parachute_events,
3444+
"impact_state": self.impact_state,
3445+
"impact_velocity": self.impact_velocity,
3446+
"x_impact": self.x_impact,
3447+
"y_impact": self.y_impact,
3448+
"t_final": self.t_final,
3449+
"flight_phases": self.flight_phases,
3450+
"function_evaluations": self.function_evaluations,
3451+
"ax": self.ax,
3452+
"ay": self.ay,
3453+
"az": self.az,
3454+
"alpha1": self.alpha1,
3455+
"alpha2": self.alpha2,
3456+
"alpha3": self.alpha3,
3457+
"R1": self.R1,
3458+
"R2": self.R2,
3459+
"R3": self.R3,
3460+
"M1": self.M1,
3461+
"M2": self.M2,
3462+
"M3": self.M3,
34373463
}
34383464

34393465
if include_outputs:
34403466
data.update(
34413467
{
34423468
"time": self.time,
3443-
"out_of_rail_time": self.out_of_rail_time,
34443469
"out_of_rail_velocity": self.out_of_rail_velocity,
34453470
"out_of_rail_state": self.out_of_rail_state,
3446-
"apogee": self.apogee,
3447-
"apogee_time": self.apogee_time,
34483471
"apogee_x": self.apogee_x,
34493472
"apogee_y": self.apogee_y,
34503473
"apogee_state": self.apogee_state,
3451-
"x_impact": self.x_impact,
3452-
"y_impact": self.y_impact,
3453-
"impact_velocity": self.impact_velocity,
3454-
"impact_state": self.impact_state,
34553474
"x": self.x,
34563475
"y": self.y,
34573476
"z": self.z,
@@ -3465,12 +3484,6 @@ def to_dict(self, include_outputs=False):
34653484
"w1": self.w1,
34663485
"w2": self.w2,
34673486
"w3": self.w3,
3468-
"ax": self.ax,
3469-
"ay": self.ay,
3470-
"az": self.az,
3471-
"alpha1": self.alpha1,
3472-
"alpha2": self.alpha2,
3473-
"alpha3": self.alpha3,
34743487
"altitude": self.altitude,
34753488
"mach_number": self.mach_number,
34763489
"stream_velocity_x": self.stream_velocity_x,

rocketpy/utilities.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import inspect
33
import traceback
44
import warnings
5+
import json
6+
import os
57

8+
from pathlib import Path
9+
from importlib.metadata import version
10+
from datetime import date
611
import matplotlib.pyplot as plt
712
import numpy as np
813
from scipy.integrate import solve_ivp
@@ -12,6 +17,7 @@
1217
from .plots.plot_helpers import show_or_save_plot
1318
from .rocket.aero_surface import TrapezoidalFins
1419
from .simulation.flight import Flight
20+
from ._encoders import RocketPyEncoder, RocketPyDecoder
1521

1622

1723
def compute_cd_s_from_drop_test(
@@ -688,3 +694,68 @@ def get_instance_attributes(instance):
688694
if not inspect.ismethod(member[1]) and not member[0].startswith("__"):
689695
attributes_dict[member[0]] = member[1]
690696
return attributes_dict
697+
698+
699+
def save_to_rpy(flight: Flight, filename: str, include_outputs=False):
700+
"""Saves a .rpy file into the given path, containing key simulation
701+
informations to reproduce the results.
702+
703+
Parameters
704+
----------
705+
flight : rocketpy.Flight
706+
Flight object containing the rocket's flight data
707+
filename : str
708+
Path where the file will be saved in
709+
include_outputs : bool, optional
710+
If True, the function will include extra outputs into the file,
711+
by default False
712+
713+
Returns
714+
-------
715+
None
716+
"""
717+
file = Path(filename).with_suffix(".rpy")
718+
719+
with open(file, "w") as f:
720+
data = {"date": str(date.today()), "version": version("rocketpy")}
721+
data["simulation"] = flight
722+
json.dump(
723+
data,
724+
f,
725+
cls=RocketPyEncoder,
726+
indent=2,
727+
include_outputs=include_outputs,
728+
)
729+
730+
731+
def load_from_rpy(filename: str, resimulate=False):
732+
"""Loads the saved data from a .rpy file into a Flight object.
733+
734+
Parameters
735+
----------
736+
filename : str
737+
Path where the file to be loaded is
738+
resimulate : bool, optional
739+
If True, the function will resimulate the Flight object,
740+
by default False
741+
742+
Returns
743+
-------
744+
rocketpy.Flight
745+
Flight object containing simulation information from the .rpy file
746+
"""
747+
ext = os.path.splitext(os.path.basename(filename))[1]
748+
if ext != ".rpy": # pragma: no cover
749+
raise ValueError(f"Invalid file extension: {ext}. Allowed: .rpy")
750+
751+
with open(filename, "r") as f:
752+
data = json.load(f)
753+
if data["version"] > version("rocketpy"):
754+
warnings.warn(
755+
"The file was saved in an updated version of",
756+
f"RocketPy (v{data['version']}), the current",
757+
f"imported module is v{version('rocketpy')}",
758+
)
759+
simulation = json.dumps(data["simulation"])
760+
flight = json.loads(simulation, cls=RocketPyDecoder, resimulate=resimulate)
761+
return flight

0 commit comments

Comments
 (0)