Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Attention: The newest changes should be on top -->

### Added

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

### Changed
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = false
docstring-code-line-length = "dynamic"
exclude = ["**/*.json", "**/*.rpy"]


[tool.ruff.lint.pydocstyle]
Expand Down
54 changes: 53 additions & 1 deletion rocketpy/_encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import numpy as np

from rocketpy.mathutils.function import Function
from rocketpy.prints.flight_prints import _FlightPrints
from rocketpy.plots.flight_plots import _FlightPlots


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

def __init__(self, *args, **kwargs):
self.resimulate = kwargs.pop("resimulate", False)
super().__init__(object_hook=self.object_hook, *args, **kwargs)

def object_hook(self, obj):
Expand All @@ -84,7 +87,56 @@ def object_hook(self, obj):
try:
class_ = get_class_from_signature(signature)

if hasattr(class_, "from_dict"):
if class_.__name__ == "Flight" and not self.resimulate:
new_flight = class_.__new__(class_)
new_flight.prints = _FlightPrints(new_flight)
new_flight.plots = _FlightPlots(new_flight)
attributes = (
"rocket",
"env",
"rail_length",
"inclination",
"heading",
"initial_solution",
"terminate_on_apogee",
"max_time",
"max_time_step",
"min_time_step",
"rtol",
"atol",
"time_overshoot",
"name",
"solution",
"out_of_rail_time",
"apogee_time",
"apogee",
"parachute_events",
"impact_state",
"impact_velocity",
"x_impact",
"y_impact",
"t_final",
"flight_phases",
"ax",
"ay",
"az",
"out_of_rail_time_index",
"function_evaluations",
"alpha1",
"alpha2",
"alpha3",
"R1",
"R2",
"R3",
"M1",
"M2",
"M3",
)
for attribute in attributes:
setattr(new_flight, attribute, obj[attribute])
new_flight.t_initial = new_flight.initial_solution[0]
return new_flight
elif hasattr(class_, "from_dict"):
return class_.from_dict(obj)
else:
# Filter keyword arguments
Expand Down
39 changes: 26 additions & 13 deletions rocketpy/simulation/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -3434,24 +3434,43 @@ def to_dict(self, include_outputs=False):
"time_overshoot": self.time_overshoot,
"name": self.name,
"equations_of_motion": self.equations_of_motion,
# The following outputs are essential to run all_info method
"solution": self.solution,
"out_of_rail_time": self.out_of_rail_time,
"out_of_rail_time_index": self.out_of_rail_time_index,
"apogee_time": self.apogee_time,
"apogee": self.apogee,
"parachute_events": self.parachute_events,
"impact_state": self.impact_state,
"impact_velocity": self.impact_velocity,
"x_impact": self.x_impact,
"y_impact": self.y_impact,
"t_final": self.t_final,
"flight_phases": self.flight_phases,
"function_evaluations": self.function_evaluations,
"ax": self.ax,
"ay": self.ay,
"az": self.az,
"alpha1": self.alpha1,
"alpha2": self.alpha2,
"alpha3": self.alpha3,
"R1": self.R1,
"R2": self.R2,
"R3": self.R3,
"M1": self.M1,
"M2": self.M2,
"M3": self.M3,
}

if include_outputs:
data.update(
{
"time": self.time,
"out_of_rail_time": self.out_of_rail_time,
"out_of_rail_velocity": self.out_of_rail_velocity,
"out_of_rail_state": self.out_of_rail_state,
"apogee": self.apogee,
"apogee_time": self.apogee_time,
"apogee_x": self.apogee_x,
"apogee_y": self.apogee_y,
"apogee_state": self.apogee_state,
"x_impact": self.x_impact,
"y_impact": self.y_impact,
"impact_velocity": self.impact_velocity,
"impact_state": self.impact_state,
"x": self.x,
"y": self.y,
"z": self.z,
Expand All @@ -3465,12 +3484,6 @@ def to_dict(self, include_outputs=False):
"w1": self.w1,
"w2": self.w2,
"w3": self.w3,
"ax": self.ax,
"ay": self.ay,
"az": self.az,
"alpha1": self.alpha1,
"alpha2": self.alpha2,
"alpha3": self.alpha3,
"altitude": self.altitude,
"mach_number": self.mach_number,
"stream_velocity_x": self.stream_velocity_x,
Expand Down
71 changes: 71 additions & 0 deletions rocketpy/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import inspect
import traceback
import warnings
import json
import os

from pathlib import Path
from importlib.metadata import version
from datetime import date
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import solve_ivp
Expand All @@ -12,6 +17,7 @@
from .plots.plot_helpers import show_or_save_plot
from .rocket.aero_surface import TrapezoidalFins
from .simulation.flight import Flight
from ._encoders import RocketPyEncoder, RocketPyDecoder


def compute_cd_s_from_drop_test(
Expand Down Expand Up @@ -688,3 +694,68 @@ def get_instance_attributes(instance):
if not inspect.ismethod(member[1]) and not member[0].startswith("__"):
attributes_dict[member[0]] = member[1]
return attributes_dict


def save_to_rpy(flight: Flight, filename: str, include_outputs=False):
"""Saves a .rpy file into the given path, containing key simulation
informations to reproduce the results.

Parameters
----------
flight : rocketpy.Flight
Flight object containing the rocket's flight data
filename : str
Path where the file will be saved in
include_outputs : bool, optional
If True, the function will include extra outputs into the file,
by default False

Returns
-------
None
"""
file = Path(filename).with_suffix(".rpy")

with open(file, "w") as f:
data = {"date": str(date.today()), "version": version("rocketpy")}
data["simulation"] = flight
json.dump(
data,
f,
cls=RocketPyEncoder,
indent=2,
include_outputs=include_outputs,
)


def load_from_rpy(filename: str, resimulate=False):
"""Loads the saved data from a .rpy file into a Flight object.

Parameters
----------
filename : str
Path where the file to be loaded is
resimulate : bool, optional
If True, the function will resimulate the Flight object,
by default False

Returns
-------
rocketpy.Flight
Flight object containing simulation information from the .rpy file
"""
ext = os.path.splitext(os.path.basename(filename))[1]
if ext != ".rpy": # pragma: no cover
raise ValueError(f"Invalid file extension: {ext}. Allowed: .rpy")

with open(filename, "r") as f:
data = json.load(f)
if data["version"] > version("rocketpy"):
warnings.warn(
"The file was saved in an updated version of",
f"RocketPy (v{data['version']}), the current",
f"imported module is v{version('rocketpy')}",
)
simulation = json.dumps(data["simulation"])
flight = json.loads(simulation, cls=RocketPyDecoder, resimulate=resimulate)
return flight
Loading