From 40eaf3c46136d586b0090749514faca2f95701e7 Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Thu, 19 Sep 2024 18:18:36 -0300 Subject: [PATCH 1/7] ENH: provide from_dict classmethods for decoding basic classes. --- rocketpy/_encoders.py | 103 +++++++++++++++++- rocketpy/environment/environment.py | 64 +++++++++++ rocketpy/mathutils/function.py | 10 +- rocketpy/motors/motor.py | 24 ++++ rocketpy/motors/solid_motor.py | 29 +++++ .../aero_surface/fins/elliptical_fins.py | 12 ++ .../aero_surface/fins/trapezoidal_fins.py | 13 +++ rocketpy/rocket/aero_surface/nose_cone.py | 12 ++ rocketpy/rocket/aero_surface/tail.py | 10 ++ rocketpy/rocket/parachute.py | 46 ++++++++ rocketpy/rocket/rocket.py | 55 ++++++++++ rocketpy/simulation/flight.py | 39 +++++++ tests/integration/test_encoding.py | 102 +++++++++++++++++ tests/unit/test_encoding.py | 51 --------- 14 files changed, 510 insertions(+), 60 deletions(-) create mode 100644 tests/integration/test_encoding.py delete mode 100644 tests/unit/test_encoding.py diff --git a/rocketpy/_encoders.py b/rocketpy/_encoders.py index 189b7a5b5..7bf0c5f70 100644 --- a/rocketpy/_encoders.py +++ b/rocketpy/_encoders.py @@ -1,8 +1,11 @@ """Defines a custom JSON encoder for RocketPy objects.""" +import base64 import json from datetime import datetime +from importlib import import_module +import dill import numpy as np @@ -33,17 +36,111 @@ def default(self, o): elif isinstance(o, np.ndarray): return o.tolist() elif isinstance(o, datetime): - return o.isoformat() + return [o.year, o.month, o.day, o.hour] elif hasattr(o, "__iter__") and not isinstance(o, str): return list(o) elif hasattr(o, "to_dict"): - return o.to_dict() + encoding = o.to_dict() + + encoding["signature"] = get_class_signature(o) + + return encoding + elif hasattr(o, "__dict__"): exception_set = {"prints", "plots"} - return { + encoding = { key: value for key, value in o.__dict__.items() if key not in exception_set } + + if "rocketpy" in o.__class__.__module__ and not any( + subclass in o.__class__.__name__ + for subclass in ["FlightPhase", "TimeNode"] + ): + encoding["signature"] = get_class_signature(o) + + return encoding else: return super().default(o) + + +class RocketPyDecoder(json.JSONDecoder): + """Custom JSON decoder for RocketPy objects. It defines how to decode + different types of objects from a JSON supported format.""" + + def __init__(self, *args, **kwargs): + super().__init__(object_hook=self.object_hook, *args, **kwargs) + + def object_hook(self, obj): + if "signature" in obj: + signature = obj.pop("signature") + + try: + class_ = get_class_from_signature(signature) + + if hasattr(class_, "from_dict"): + return class_.from_dict(obj) + else: + # Filter keyword arguments + kwargs = { + key: value + for key, value in obj.items() + if key in class_.__init__.__code__.co_varnames + } + + return class_(**kwargs) + except ImportError: # AttributeException + return obj + else: + return obj + + +def get_class_signature(obj): + class_ = obj.__class__ + + return f"{class_.__module__}.{class_.__name__}" + + +def get_class_from_signature(signature): + module_name, class_name = signature.rsplit(".", 1) + + module = import_module(module_name) + + return getattr(module, class_name) + + +def to_hex_encode(obj, encoder=base64.b85encode): + """Converts an object to hex representation using dill. + + Parameters + ---------- + obj : object + Object to be converted to hex. + encoder : callable, optional + Function to encode the bytes. Default is base64.b85encode. + + Returns + ------- + bytes + Object converted to bytes. + """ + return encoder(dill.dumps(obj)).hex() + + +def from_hex_decode(obj_bytes, decoder=base64.b85decode): + """Converts an object from hex representation using dill. + + Parameters + ---------- + obj_bytes : str + Hex string to be converted to object. + decoder : callable, optional + Function to decode the bytes. Default is base64.b85decode. + + Returns + ------- + object + Object converted from bytes. + """ + return dill.loads(decoder(bytes.fromhex(obj_bytes))) diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index 265ee7d38..1e0a21ee6 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -2853,6 +2853,70 @@ def decimal_degrees_to_arc_seconds(angle): arc_seconds = (remainder * 60 - arc_minutes) * 60 return degrees, arc_minutes, arc_seconds + def to_dict(self): + return { + "gravity": self.gravity, + "date": self.date, + "latitude": self.latitude, + "longitude": self.longitude, + "elevation": self.elevation, + "datum": self.datum, + "timezone": self.timezone, + "_max_expected_height": self.max_expected_height, + "atmospheric_model_type": self.atmospheric_model_type, + "pressure": self.pressure, + "temperature": self.temperature, + "wind_velocity_x": self.wind_velocity_x, + "wind_velocity_y": self.wind_velocity_y, + "wind_heading": self.wind_heading, + "wind_direction": self.wind_direction, + "wind_speed": self.wind_speed, + } + + @classmethod + def from_dict(cls, data): + environment = cls( + gravity=data["gravity"], + date=data["date"], + latitude=data["latitude"], + longitude=data["longitude"], + elevation=data["elevation"], + datum=data["datum"], + timezone=data["timezone"], + max_expected_height=data["_max_expected_height"], + ) + + atmospheric_model = data["atmospheric_model_type"] + + if atmospheric_model == "standard_atmosphere": + environment.set_atmospheric_model("standard_atmosphere") + elif atmospheric_model == "custom_atmosphere": + environment.set_atmospheric_model( + type="custom_atmosphere", + pressure=data["pressure"], + temperature=data["temperature"], + wind_u=data["wind_velocity_x"], + wind_v=data["wind_velocity_y"], + ) + else: + environment.__set_pressure_function(data["pressure"]) + environment.__set_barometric_height_function(data["temperature"]) + environment.__set_temperature_function(data["temperature"]) + environment.__set_wind_velocity_x_function(data["wind_velocity_x"]) + environment.__set_wind_velocity_y_function(data["wind_velocity_y"]) + environment.__set_wind_heading_function(data["wind_heading"]) + environment.__set_wind_direction_function(data["wind_direction"]) + environment.__set_wind_speed_function(data["wind_speed"]) + environment.elevation = data["elevation"] + environment.max_expected_height = data["_max_expected_height"] + + if atmospheric_model != "ensemble": + environment.calculate_density_profile() + environment.calculate_speed_of_sound_profile() + environment.calculate_dynamic_viscosity() + + return environment + if __name__ == "__main__": import doctest diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index 638ea1750..a42534e89 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -5,9 +5,7 @@ carefully as it may impact all the rest of the project. """ -import base64 import warnings -import zlib from bisect import bisect_left from collections.abc import Iterable from copy import deepcopy @@ -25,6 +23,8 @@ RBFInterpolator, ) +from rocketpy._encoders import from_hex_decode, to_hex_encode + # Numpy 1.x compatibility, # TODO: remove these lines when all dependencies support numpy>=2.0.0 if np.lib.NumpyVersion(np.__version__) >= "2.0.0b1": @@ -3401,7 +3401,7 @@ def to_dict(self): source = self.source if callable(source): - source = zlib.compress(base64.b85encode(dill.dumps(source))).hex() + source = to_hex_encode(source) return { "source": source, @@ -3423,9 +3423,7 @@ def from_dict(cls, func_dict): """ source = func_dict["source"] if func_dict["interpolation"] is None and func_dict["extrapolation"] is None: - source = dill.loads( - base64.b85decode(zlib.decompress(bytes.fromhex(source))) - ) + source = from_hex_decode(source) return cls( source=source, diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index eae7931b4..d1ab085e9 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -1481,6 +1481,30 @@ def all_info(self): self.prints.all() self.plots.all() + @classmethod + def from_dict(cls, data): + return cls( + thrust_source=data["thrust_source"], + burn_time=data["_burn_time"], + chamber_radius=data["chamber_radius"], + chamber_height=data["chamber_height"], + chamber_position=data["chamber_position"], + propellant_initial_mass=data["propellant_initial_mass"], + nozzle_radius=data["nozzle_radius"], + dry_mass=data["dry_mass"], + center_of_dry_mass_position=data["center_of_dry_mass_position"], + dry_inertia=( + data["dry_I_11"], + data["dry_I_22"], + data["dry_I_33"], + data["dry_I_12"], + data["dry_I_13"], + data["dry_I_23"], + ), + nozzle_position=data["nozzle_position"], + interpolation_method=data["interpolate"], + ) + class EmptyMotor: """Class that represents an empty motor with no mass and no thrust.""" diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index 81faf453f..12b7f5a9d 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -738,3 +738,32 @@ def all_info(self): """Prints out all data and graphs available about the SolidMotor.""" self.prints.all() self.plots.all() + + @classmethod + def from_dict(cls, data): + return cls( + thrust_source=data["thrust_source"], + dry_mass=data["dry_mass"], + dry_inertia=( + data["dry_I_11"], + data["dry_I_22"], + data["dry_I_33"], + data["dry_I_12"], + data["dry_I_13"], + data["dry_I_23"], + ), + nozzle_radius=data["nozzle_radius"], + grain_number=data["grain_number"], + grain_density=data["grain_density"], + grain_outer_radius=data["grain_outer_radius"], + grain_initial_inner_radius=data["grain_initial_inner_radius"], + grain_initial_height=data["grain_initial_height"], + grain_separation=data["grain_separation"], + grains_center_of_mass_position=data["grains_center_of_mass_position"], + center_of_dry_mass_position=data["center_of_dry_mass_position"], + nozzle_position=data["nozzle_position"], + burn_time=data["_burn_time"], + throat_radius=data["throat_radius"], + interpolation_method=data["interpolate"], + coordinate_system_orientation=data["coordinate_system_orientation"], + ) diff --git a/rocketpy/rocket/aero_surface/fins/elliptical_fins.py b/rocketpy/rocket/aero_surface/fins/elliptical_fins.py index d30cf9aa1..69a9ee7fa 100644 --- a/rocketpy/rocket/aero_surface/fins/elliptical_fins.py +++ b/rocketpy/rocket/aero_surface/fins/elliptical_fins.py @@ -316,3 +316,15 @@ def info(self): def all_info(self): self.prints.all() self.plots.all() + + @classmethod + def from_dict(cls, data): + return cls( + n=data["_n"], + root_chord=data["_root_chord"], + span=data["_span"], + rocket_radius=data["_rocket_radius"], + cant_angle=data["_cant_angle"], + airfoil=data["_airfoil"], + name=data["name"], + ) diff --git a/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py b/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py index 3040e21c9..b2edb8206 100644 --- a/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py +++ b/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py @@ -347,3 +347,16 @@ def info(self): def all_info(self): self.prints.all() self.plots.all() + + @classmethod + def from_dict(cls, data): + return cls( + n=data["_n"], + root_chord=data["_root_chord"], + tip_chord=data["_tip_chord"], + span=data["_span"], + rocket_radius=data["_rocket_radius"], + cant_angle=data["_cant_angle"], + airfoil=data["_airfoil"], + name=data["name"], + ) diff --git a/rocketpy/rocket/aero_surface/nose_cone.py b/rocketpy/rocket/aero_surface/nose_cone.py index 7d59473e3..d5ee33151 100644 --- a/rocketpy/rocket/aero_surface/nose_cone.py +++ b/rocketpy/rocket/aero_surface/nose_cone.py @@ -515,3 +515,15 @@ def all_info(self): """ self.prints.all() self.plots.all() + + @classmethod + def from_dict(cls, data): + return cls( + length=data["_length"], + kind=data["_kind"], + base_radius=data["_base_radius"], + bluffness=data["_bluffness"], + rocket_radius=data["_rocket_radius"], + power=data["_power"], + name=data["name"], + ) diff --git a/rocketpy/rocket/aero_surface/tail.py b/rocketpy/rocket/aero_surface/tail.py index 4f2783e02..acd055f2e 100644 --- a/rocketpy/rocket/aero_surface/tail.py +++ b/rocketpy/rocket/aero_surface/tail.py @@ -204,3 +204,13 @@ def info(self): def all_info(self): self.prints.all() self.plots.all() + + @classmethod + def from_dict(cls, data): + return cls( + top_radius=data["_top_radius"], + bottom_radius=data["_bottom_radius"], + length=data["_length"], + rocket_radius=data["_rocket_radius"], + name=data["name"], + ) diff --git a/rocketpy/rocket/parachute.py b/rocketpy/rocket/parachute.py index c465c4367..227538230 100644 --- a/rocketpy/rocket/parachute.py +++ b/rocketpy/rocket/parachute.py @@ -2,6 +2,8 @@ import numpy as np +from rocketpy._encoders import from_hex_decode, to_hex_encode + from ..mathutils.function import Function from ..prints.parachute_prints import _ParachutePrints @@ -248,3 +250,47 @@ def all_info(self): """Prints all information about the Parachute class.""" self.info() # self.plots.all() # Parachutes still doesn't have plots + + def to_dict(self): + trigger = self.trigger + + if callable(self.trigger): + trigger = to_hex_encode(trigger) + + return { + "name": self.name, + "cd_s": self.cd_s, + "trigger": trigger, + "sampling_rate": self.sampling_rate, + "lag": self.lag, + "noise": self.noise, + "noise_signal": [ + [self.noise_signal[0][0], to_hex_encode(self.noise_signal[-1][1])] + ], + "noise_function": to_hex_encode(self.noise_function), + } + + @classmethod + def from_dict(cls, data): + trigger = data["trigger"] + + try: + trigger = from_hex_decode(trigger) + except (TypeError, ValueError): + pass + + parachute = cls( + name=data["name"], + cd_s=data["cd_s"], + trigger=trigger, + sampling_rate=data["sampling_rate"], + lag=data["lag"], + noise=data["noise"], + ) + + parachute.noise_signal = [ + [data["noise_signal"][0][0], from_hex_decode(data["noise_signal"][-1][1])] + ] + parachute.noise_function = from_hex_decode(data["noise_function"]) + + return parachute diff --git a/rocketpy/rocket/rocket.py b/rocketpy/rocket/rocket.py index ec3bfc0ec..85def6285 100644 --- a/rocketpy/rocket/rocket.py +++ b/rocketpy/rocket/rocket.py @@ -1856,3 +1856,58 @@ def all_info(self): """ self.info() self.plots.all() + + @classmethod + def from_dict(cls, data): + rocket = cls( + radius=data["radius"], + mass=data["mass"], + inertia=( + data["I_11_without_motor"], + data["I_22_without_motor"], + data["I_33_without_motor"], + data["I_12_without_motor"], + data["I_13_without_motor"], + data["I_23_without_motor"], + ), + power_off_drag=data["power_off_drag"], + power_on_drag=data["power_on_drag"], + center_of_mass_without_motor=data["center_of_mass_without_motor"], + coordinate_system_orientation=data["coordinate_system_orientation"], + ) + + if (motor := data["motor"]) is not None: + rocket.add_motor( + motor, + data["motor_position"], + ) + + for surface, position in data["aerodynamic_surfaces"]: + rocket.add_surfaces(surface, position) + + for button, position in data["rail_buttons"]: + rocket.set_rail_buttons( + position + button.buttons_distance, + position, + button.angular_position, + button.rocket_radius, + ) + + for parachute in data["parachutes"]: + rocket.parachutes.append(parachute) + + for air_brakes in data["air_brakes"]: + rocket.add_air_brakes( + air_brakes["drag_coefficient_curve"], + air_brakes["controller_function"], + air_brakes["sampling_rate"], + air_brakes["clamp"], + air_brakes["reference_area"], + air_brakes["initial_observed_variables"], + air_brakes["override_rocket_drag"], + air_brakes["return_controller"], + air_brakes["name"], + air_brakes["controller_name"], + ) + + return rocket diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index 70ddff6c1..6be93a57c 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -3374,6 +3374,45 @@ def time_iterator(self, node_list): yield i, node_list[i] i += 1 + def to_dict(self): + return { + "rocket": self.rocket, + "env": self.env, + "rail_length": self.rail_length, + "inclination": self.inclination, + "heading": self.heading, + "initial_solution": self.initial_solution, + "terminate_on_apogee": self.terminate_on_apogee, + "max_time": self.max_time, + "max_time_step": self.max_time_step, + "min_time_step": self.min_time_step, + "rtol": self.rtol, + "atol": self.atol, + "time_overshoot": self.time_overshoot, + "name": self.name, + "equations_of_motion": self.equations_of_motion, + } + + @classmethod + def from_dict(cls, data): + return cls( + rocket=data["rocket"], + environment=data["env"], + rail_length=data["rail_length"], + inclination=data["inclination"], + heading=data["heading"], + initial_solution=None, + terminate_on_apogee=data["terminate_on_apogee"], + max_time=data["max_time"], + max_time_step=data["max_time_step"], + min_time_step=data["min_time_step"], + rtol=data["rtol"], + atol=data["atol"], + time_overshoot=data["time_overshoot"], + name=data["name"], + equations_of_motion=data["equations_of_motion"], + ) + class FlightPhases: """Class to handle flight phases. It is used to store the derivatives and callbacks for each flight phase. It is also used to handle the diff --git a/tests/integration/test_encoding.py b/tests/integration/test_encoding.py new file mode 100644 index 000000000..80a9601ce --- /dev/null +++ b/tests/integration/test_encoding.py @@ -0,0 +1,102 @@ +import json +import os + +import numpy as np +import pytest + +from rocketpy._encoders import RocketPyDecoder, RocketPyEncoder + + +@pytest.mark.slow +@pytest.mark.parametrize("flight_name", ["flight_calisto", "flight_calisto_robust"]) +def test_flight_save_load(flight_name, request): + """Test encoding a ``rocketpy.Flight``. + + Parameters + ---------- + flight_name : str + Name flight fixture to encode. + request : pytest.FixtureRequest + Pytest request object. + """ + flight_to_save = request.getfixturevalue(flight_name) + + with open("flight.json", "w") as f: + json.dump(flight_to_save, f, cls=RocketPyEncoder, indent=2) + + with open("flight.json", "r") as f: + flight_loaded = json.load(f, cls=RocketPyDecoder) + + assert np.isclose(flight_to_save.t_initial, flight_loaded.t_initial) + assert np.isclose(flight_to_save.out_of_rail_time, flight_loaded.out_of_rail_time) + assert np.isclose(flight_to_save.apogee_time, flight_loaded.apogee_time) + assert np.isclose(flight_to_save.t_final, flight_loaded.t_final, rtol=1e-2) + + os.remove("flight.json") + + +@pytest.mark.parametrize( + "function_name", ["lambda_quad_func", "spline_interpolated_func"] +) +def test_function_encoder(function_name, request): + """Test encoding a ``rocketpy.Function``. + + Parameters + ---------- + function_name : str + Name of the function to encode. + request : pytest.FixtureRequest + Pytest request object. + """ + function_to_encode = request.getfixturevalue(function_name) + + json_encoded = json.dumps(function_to_encode, cls=RocketPyEncoder) + + function_loaded = json.loads(json_encoded, cls=RocketPyDecoder) + + assert isinstance(function_loaded, type(function_to_encode)) + assert np.isclose(function_to_encode(0), function_loaded(0)) + + +@pytest.mark.parametrize( + "environment_name", ["example_plain_env", "environment_spaceport_america_2023"] +) +def test_environment_encoder(environment_name, request): + """Test encoding a ``rocketpy.Environment``. + + Parameters + ---------- + environment_name : str + Name of the environment fixture to encode. + request : pytest.FixtureRequest + Pytest request object. + """ + env_to_encode = request.getfixturevalue(environment_name) + + json_encoded = json.dumps(env_to_encode, cls=RocketPyEncoder) + + env_loaded = json.loads(json_encoded, cls=RocketPyDecoder) + + test_heights = np.linspace(0, 10000, 4) + + assert np.isclose(env_to_encode.elevation, env_loaded.elevation) + assert np.isclose(env_to_encode.latitude, env_loaded.latitude) + assert np.isclose(env_to_encode.longitude, env_loaded.longitude) + assert env_to_encode.datum == env_loaded.datum + assert np.allclose( + env_to_encode.wind_velocity_x(test_heights), + env_loaded.wind_velocity_x(test_heights), + ) + assert np.allclose( + env_to_encode.wind_velocity_y(test_heights), + env_loaded.wind_velocity_y(test_heights), + ) + assert np.allclose( + env_to_encode.temperature(test_heights), env_loaded.temperature(test_heights) + ) + assert np.allclose( + env_to_encode.pressure(test_heights), env_loaded.pressure(test_heights) + ) + assert np.allclose( + env_to_encode.density(test_heights), env_loaded.density(test_heights) + ) diff --git a/tests/unit/test_encoding.py b/tests/unit/test_encoding.py deleted file mode 100644 index 8b58e71a3..000000000 --- a/tests/unit/test_encoding.py +++ /dev/null @@ -1,51 +0,0 @@ -import json - -import pytest - -from rocketpy._encoders import RocketPyEncoder - -# TODO: this tests should be improved with better validation and decoding - - -@pytest.mark.parametrize("flight_name", ["flight_calisto", "flight_calisto_robust"]) -def test_encode_flight(flight_name, request): - """Test encoding a ``rocketpy.Flight``. - - Parameters - ---------- - flight_name : str - Name flight fixture to encode. - request : pytest.FixtureRequest - Pytest request object. - """ - flight = request.getfixturevalue(flight_name) - - json_encoded = json.dumps(flight, cls=RocketPyEncoder) - - flight_dict = json.loads(json_encoded) - - assert json_encoded is not None - assert flight_dict is not None - - -@pytest.mark.parametrize( - "function_name", ["lambda_quad_func", "spline_interpolated_func"] -) -def test_encode_function(function_name, request): - """Test encoding a ``rocketpy.Function``. - - Parameters - ---------- - function_name : str - Name of the function to encode. - request : pytest.FixtureRequest - Pytest request object. - """ - function = request.getfixturevalue(function_name) - - json_encoded = json.dumps(function, cls=RocketPyEncoder) - - function_dict = json.loads(json_encoded) - - assert json_encoded is not None - assert function_dict is not None From 48e3237e14fa08c9fcbe0a7b1afd69442bb3d4f9 Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Thu, 19 Sep 2024 23:21:10 -0300 Subject: [PATCH 2/7] ENH: extend encoding and decoding to Liquid and Hybrid. --- rocketpy/environment/environment.py | 41 +++++++++++++-- rocketpy/motors/hybrid_motor.py | 32 ++++++++++++ rocketpy/motors/liquid_motor.py | 23 +++++++++ rocketpy/motors/motor.py | 2 +- rocketpy/motors/solid_motor.py | 2 +- rocketpy/motors/tank.py | 79 +++++++++++++++++++++++++++++ rocketpy/motors/tank_geometry.py | 27 ++++++++++ tests/integration/test_encoding.py | 26 +++++++++- 8 files changed, 225 insertions(+), 7 deletions(-) diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index 1e0a21ee6..c90b2f611 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -2865,6 +2865,7 @@ def to_dict(self): "_max_expected_height": self.max_expected_height, "atmospheric_model_type": self.atmospheric_model_type, "pressure": self.pressure, + "barometric_height": self.barometric_height, "temperature": self.temperature, "wind_velocity_x": self.wind_velocity_x, "wind_velocity_y": self.wind_velocity_y, @@ -2874,7 +2875,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, data): + def from_dict(cls, data): # pylint: disable=too-many-statements environment = cls( gravity=data["gravity"], date=data["date"], @@ -2885,7 +2886,6 @@ def from_dict(cls, data): timezone=data["timezone"], max_expected_height=data["_max_expected_height"], ) - atmospheric_model = data["atmospheric_model_type"] if atmospheric_model == "standard_atmosphere": @@ -2900,7 +2900,7 @@ def from_dict(cls, data): ) else: environment.__set_pressure_function(data["pressure"]) - environment.__set_barometric_height_function(data["temperature"]) + environment.__set_barometric_height_function(data["barometric_height"]) environment.__set_temperature_function(data["temperature"]) environment.__set_wind_velocity_x_function(data["wind_velocity_x"]) environment.__set_wind_velocity_y_function(data["wind_velocity_y"]) @@ -2910,7 +2910,40 @@ def from_dict(cls, data): environment.elevation = data["elevation"] environment.max_expected_height = data["_max_expected_height"] - if atmospheric_model != "ensemble": + if atmospheric_model in ["windy", "forecast", "reanalysis", "ensemble"]: + environment.atmospheric_model_init_date = data[ + "atmospheric_model_init_date" + ] + environment.atmospheric_model_end_date = data[ + "atmospheric_model_end_date" + ] + environment.atmospheric_model_interval = data[ + "atmospheric_model_interval" + ] + environment.atmospheric_model_init_lat = data[ + "atmospheric_model_init_lat" + ] + environment.atmospheric_model_end_lat = data[ + "atmospheric_model_end_lat" + ] + environment.atmospheric_model_init_lon = data[ + "atmospheric_model_init_lon" + ] + environment.atmospheric_model_end_lon = data[ + "atmospheric_model_end_lon" + ] + + if atmospheric_model == "ensemble": + environment.level_ensemble = data["level_ensemble"] + environment.height_ensemble = data["height_ensemble"] + environment.temperature_ensemble = data["temperature_ensemble"] + environment.wind_u_ensemble = data["wind_u_ensemble"] + environment.wind_v_ensemble = data["wind_v_ensemble"] + environment.wind_heading_ensemble = data["wind_heading_ensemble"] + environment.wind_direction_ensemble = data["wind_direction_ensemble"] + environment.wind_speed_ensemble = data["wind_speed_ensemble"] + environment.num_ensemble_members = data["num_ensemble_members"] + environment.calculate_density_profile() environment.calculate_speed_of_sound_profile() environment.calculate_dynamic_viscosity() diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index 1a1bbb3db..05c029835 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -612,3 +612,35 @@ def all_info(self): """Prints out all data and graphs available about the Motor.""" self.prints.all() self.plots.all() + + @classmethod + def from_dict(cls, data): + motor = cls( + thrust_source=data["thrust"], + burn_time=data["_burn_time"], + nozzle_radius=data["nozzle_radius"], + dry_mass=data["dry_mass"], + center_of_dry_mass_position=data["center_of_dry_mass_position"], + dry_inertia=( + data["dry_I_11"], + data["dry_I_22"], + data["dry_I_33"], + data["dry_I_12"], + data["dry_I_13"], + data["dry_I_23"], + ), + interpolation_method=data["interpolate"], + coordinate_system_orientation=data["coordinate_system_orientation"], + grain_number=data["grain_number"], + grain_density=data["grain_density"], + grain_outer_radius=data["grain_outer_radius"], + grain_initial_inner_radius=data["grain_initial_inner_radius"], + grain_initial_height=data["grain_initial_height"], + grain_separation=data["grain_separation"], + grains_center_of_mass_position=data["grains_center_of_mass_position"], + nozzle_position=data["nozzle_position"], + throat_radius=data["throat_radius"], + ) + + for tank in data["positioned_tanks"]: + motor.add_tank(tank["tank"], tank["position"]) diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index 9ec3d1130..ecfff4d35 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -479,3 +479,26 @@ def all_info(self): """ self.prints.all() self.plots.all() + + @classmethod + def from_dict(cls, data): + motor = cls( + thrust_source=data["thrust"], + burn_time=data["_burn_time"], + nozzle_radius=data["nozzle_radius"], + dry_mass=data["dry_mass"], + center_of_dry_mass_position=data["center_of_dry_mass_position"], + dry_inertia=( + data["dry_I_11"], + data["dry_I_22"], + data["dry_I_33"], + data["dry_I_12"], + data["dry_I_13"], + data["dry_I_23"], + ), + interpolation_method=data["interpolate"], + coordinate_system_orientation=data["coordinate_system_orientation"], + ) + + for tank in data["positioned_tanks"]: + motor.add_tank(tank["tank"], tank["position"]) diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index d1ab085e9..890f36367 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -1484,7 +1484,7 @@ def all_info(self): @classmethod def from_dict(cls, data): return cls( - thrust_source=data["thrust_source"], + thrust_source=data["thrust"], burn_time=data["_burn_time"], chamber_radius=data["chamber_radius"], chamber_height=data["chamber_height"], diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index 12b7f5a9d..91f0d2a4d 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -742,7 +742,7 @@ def all_info(self): @classmethod def from_dict(cls, data): return cls( - thrust_source=data["thrust_source"], + thrust_source=data["thrust"], dry_mass=data["dry_mass"], dry_inertia=( data["dry_I_11"], diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index a3b21f434..f3dc5ee14 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -480,6 +480,16 @@ def draw(self): """Draws the tank geometry.""" self.plots.draw() + def to_dict(self): + return { + "name": self.name, + "geometry": self.geometry, + "flux_time": self.flux_time, + "liquid": self.liquid, + "gas": self.gas, + "discretize": self.discretize, + } + class MassFlowRateBasedTank(Tank): """Class to define a tank based on mass flow rates inputs. This class @@ -821,6 +831,32 @@ def discretize_flow(self): self.liquid_mass_flow_rate_out.set_discrete(*self.flux_time, self.discretize) self.gas_mass_flow_rate_out.set_discrete(*self.flux_time, self.discretize) + def to_dict(self): + return { + **super().to_dict(), + "initial_liquid_mass": self.initial_liquid_mass, + "initial_gas_mass": self.initial_gas_mass, + "liquid_mass_flow_rate_in": self.liquid_mass_flow_rate_in, + "gas_mass_flow_rate_in": self.gas_mass_flow_rate_in, + } + + @classmethod + def from_dict(cls, data): + return cls( + name=data["name"], + geometry=data["geometry"], + flux_time=data["flux_time"], + liquid=data["liquid"], + gas=data["gas"], + initial_liquid_mass=data["initial_liquid_mass"], + initial_gas_mass=data["initial_gas_mass"], + liquid_mass_flow_rate_in=data["liquid_mass_flow_rate_in"], + gas_mass_flow_rate_in=data["gas_mass_flow_rate_in"], + liquid_mass_flow_rate_out=data["liquid_mass_flow_rate_out"], + gas_mass_flow_rate_out=data["gas_mass_flow_rate_out"], + discretize=data["discretize"], + ) + class UllageBasedTank(Tank): """Class to define a tank whose flow is described by ullage volume, i.e., @@ -1016,6 +1052,24 @@ def discretize_ullage(self): discretize parameter.""" self.ullage.set_discrete(*self.flux_time, self.discretize) + def to_dict(self): + return { + **super().to_dict(), + "ullage": self.ullage, + } + + @classmethod + def from_dict(cls, data): + return cls( + name=data["name"], + geometry=data["geometry"], + flux_time=data["flux_time"], + liquid=data["liquid"], + gas=data["gas"], + ullage=data["ullage"], + discretize=data["discretize"], + ) + class LevelBasedTank(Tank): """Class to define a tank whose flow is described by liquid level, i.e., @@ -1226,6 +1280,24 @@ def discretize_liquid_height(self): """ self.liquid_level.set_discrete(*self.flux_time, self.discretize) + def to_dict(self): + return { + **super().to_dict(), + "liquid_height": self.liquid_level, + } + + @classmethod + def from_dict(cls, data): + return cls( + name=data["name"], + geometry=data["geometry"], + flux_time=data["flux_time"], + liquid=data["liquid"], + gas=data["gas"], + liquid_height=data["liquid_height"], + discretize=data["discretize"], + ) + class MassBasedTank(Tank): """Class to define a tank whose flow is described by liquid and gas masses. @@ -1466,3 +1538,10 @@ def discretize_masses(self): """ self.liquid_mass.set_discrete(*self.flux_time, self.discretize) self.gas_mass.set_discrete(*self.flux_time, self.discretize) + + def to_dict(self): + return { + **super().to_dict(), + "liquid_mass": self.liquid_mass, + "gas_mass": self.gas_mass, + } diff --git a/rocketpy/motors/tank_geometry.py b/rocketpy/motors/tank_geometry.py index 2471079d7..bd9d9e4e3 100644 --- a/rocketpy/motors/tank_geometry.py +++ b/rocketpy/motors/tank_geometry.py @@ -361,6 +361,15 @@ def to_dict(self): } } + @classmethod + def from_dict(cls, data): + geometry_dict = {} + # Reconstruct tuple keys + for domain, radius_function in data["geometry"].items(): + domain = tuple(map(float, domain.strip("()").split(", "))) + geometry_dict[domain] = radius_function + return cls(geometry_dict) + class CylindricalTank(TankGeometry): """Class to define the geometry of a cylindrical tank. The cylinder has @@ -429,6 +438,17 @@ def upper_cap_radius(h): else: raise ValueError("Tank already has caps.") + def to_dict(self): + return { + "radius": self.radius(0), + "height": self.height, + "spherical_caps": self.has_caps, + } + + @classmethod + def from_dict(cls, data): + return cls(data["radius"], data["height"], data["spherical_caps"]) + class SphericalTank(TankGeometry): """Class to define the geometry of a spherical tank. The sphere zero @@ -451,3 +471,10 @@ def __init__(self, radius, geometry_dict=None): geometry_dict = geometry_dict or {} super().__init__(geometry_dict) self.add_geometry((-radius, radius), lambda h: (radius**2 - h**2) ** 0.5) + + def to_dict(self): + return {"radius": self.radius(0)} + + @classmethod + def from_dict(cls, data): + return cls(data["radius"]) diff --git a/tests/integration/test_encoding.py b/tests/integration/test_encoding.py index 80a9601ce..2568c4632 100644 --- a/tests/integration/test_encoding.py +++ b/tests/integration/test_encoding.py @@ -77,7 +77,7 @@ def test_environment_encoder(environment_name, request): env_loaded = json.loads(json_encoded, cls=RocketPyDecoder) - test_heights = np.linspace(0, 10000, 4) + test_heights = np.linspace(0, 10000, 100) assert np.isclose(env_to_encode.elevation, env_loaded.elevation) assert np.isclose(env_to_encode.latitude, env_loaded.latitude) @@ -100,3 +100,27 @@ def test_environment_encoder(environment_name, request): assert np.allclose( env_to_encode.density(test_heights), env_loaded.density(test_heights) ) + + +@pytest.mark.parametrize( + "rocket_name", ["calisto_robust", "calisto_liquid_modded", "calisto_hybrid_modded"] +) +def test_rocket_encoder(rocket_name, request): + """Test encoding a ``rocketpy.Rocket``. + + Parameters + ---------- + rocket_name : str + Name of the rocket fixture to encode. + request : pytest.FixtureRequest + Pytest request object. + """ + rocket_to_encode = request.getfixturevalue(rocket_name) + + json_encoded = json.dumps(rocket_to_encode, cls=RocketPyEncoder) + + rocket_loaded = json.loads(json_encoded, cls=RocketPyDecoder) + + # assert np.isclose(rocket_to_encode.rocket_mass, rocket_loaded.rocket_mass) + # assert np.isclose(rocket_to_encode.propellant_mass, rocket_loaded.propellant_mass) + # assert np.isclose(rocket_to_encode.dry_mass, rocket_loaded.dry_mass) From 3afc71a92dd596995c5bb961b30ed68ce477ac0b Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Sun, 22 Sep 2024 11:58:33 -0300 Subject: [PATCH 3/7] MNT: correct decoding of liquid and hybrid motors. --- rocketpy/motors/hybrid_motor.py | 2 + rocketpy/motors/liquid_motor.py | 2 + rocketpy/motors/tank.py | 2 +- tests/fixtures/flight/flight_fixtures.py | 56 ++++++++++++++++++++++++ tests/fixtures/hybrid/hybrid_fixtures.py | 5 ++- tests/integration/test_encoding.py | 33 +++++++++++--- tests/integration/test_flight.py | 34 ++++---------- 7 files changed, 98 insertions(+), 36 deletions(-) diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index 05c029835..7af812645 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -644,3 +644,5 @@ def from_dict(cls, data): for tank in data["positioned_tanks"]: motor.add_tank(tank["tank"], tank["position"]) + + return motor diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index ecfff4d35..995f8ef1b 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -502,3 +502,5 @@ def from_dict(cls, data): for tank in data["positioned_tanks"]: motor.add_tank(tank["tank"], tank["position"]) + + return motor diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index f3dc5ee14..49b7992c8 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -1283,7 +1283,7 @@ def discretize_liquid_height(self): def to_dict(self): return { **super().to_dict(), - "liquid_height": self.liquid_level, + "liquid_height": self.liquid_height, } @classmethod diff --git a/tests/fixtures/flight/flight_fixtures.py b/tests/fixtures/flight/flight_fixtures.py index c8fe437ca..30c93822e 100644 --- a/tests/fixtures/flight/flight_fixtures.py +++ b/tests/fixtures/flight/flight_fixtures.py @@ -93,6 +93,62 @@ def flight_calisto_robust(calisto_robust, example_spaceport_env): ) +@pytest.fixture +def flight_calisto_liquid_modded(calisto_liquid_modded, example_plain_env): + """A rocketpy.Flight object of the Calisto rocket modded for a liquid + motor. The environment is the simplest possible, with no parameters set. + + Parameters + ---------- + calisto_liquid_modded : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + example_plain_env : rocketpy.Environment + An object of the Environment class. This is a pytest fixture too. + + Returns + ------- + rocketpy.Flight + A rocketpy.Flight object. + """ + return Flight( + rocket=calisto_liquid_modded, + environment=example_plain_env, + rail_length=5.2, + inclination=85, + heading=0, + time_overshoot=False, + terminate_on_apogee=True, + ) + + +@pytest.fixture +def flight_calisto_hybrid_modded(calisto_hybrid_modded, example_plain_env): + """A rocketpy.Flight object of the Calisto rocket modded for a hybrid + motor. The environment is the simplest possible, with no parameters set. + + Parameters + ---------- + calisto_hybrid_modded : rocketpy.Rocket + An object of the Rocket class. This is a pytest fixture too. + example_plain_env : rocketpy.Environment + An object of the Environment class. This is a pytest fixture too. + + Returns + ------- + rocketpy.Flight + A rocketpy.Flight object. + """ + return Flight( + rocket=calisto_hybrid_modded, + environment=example_plain_env, + rail_length=5.2, + inclination=85, + heading=0, + time_overshoot=False, + terminate_on_apogee=True, + ) + + @pytest.fixture def flight_calisto_custom_wind(calisto_robust, example_spaceport_env): """A rocketpy.Flight object of the Calisto rocket. This uses the calisto diff --git a/tests/fixtures/hybrid/hybrid_fixtures.py b/tests/fixtures/hybrid/hybrid_fixtures.py index 9c3d8c8dc..7c89442c2 100644 --- a/tests/fixtures/hybrid/hybrid_fixtures.py +++ b/tests/fixtures/hybrid/hybrid_fixtures.py @@ -1,3 +1,5 @@ +from math import exp + import numpy as np import pytest @@ -225,14 +227,13 @@ def spherical_oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): rocketpy.UllageBasedTank """ geometry = SphericalTank(0.05) - liquid_level = Function(lambda t: 0.1 * np.exp(-t / 2) - 0.05) oxidizer_tank = LevelBasedTank( name="Lox Tank", flux_time=10, geometry=geometry, liquid=oxidizer_fluid, gas=oxidizer_pressurant, - liquid_height=liquid_level, + liquid_height=lambda t: 0.1 * np.exp(-t / 2) - 0.05, ) return oxidizer_tank diff --git a/tests/integration/test_encoding.py b/tests/integration/test_encoding.py index 2568c4632..54d73f81d 100644 --- a/tests/integration/test_encoding.py +++ b/tests/integration/test_encoding.py @@ -8,7 +8,15 @@ @pytest.mark.slow -@pytest.mark.parametrize("flight_name", ["flight_calisto", "flight_calisto_robust"]) +@pytest.mark.parametrize( + "flight_name", + [ + "flight_calisto", + "flight_calisto_robust", + "flight_calisto_liquid_modded", + "flight_calisto_hybrid_modded", + ], +) def test_flight_save_load(flight_name, request): """Test encoding a ``rocketpy.Flight``. @@ -27,9 +35,13 @@ def test_flight_save_load(flight_name, request): with open("flight.json", "r") as f: flight_loaded = json.load(f, cls=RocketPyDecoder) - assert np.isclose(flight_to_save.t_initial, flight_loaded.t_initial) - assert np.isclose(flight_to_save.out_of_rail_time, flight_loaded.out_of_rail_time) - assert np.isclose(flight_to_save.apogee_time, flight_loaded.apogee_time) + # TODO: Investigate why hybrid motor needs a higher tolerance + + assert np.isclose(flight_to_save.t_initial, flight_loaded.t_initial, rtol=1e-3) + assert np.isclose( + flight_to_save.out_of_rail_time, flight_loaded.out_of_rail_time, rtol=1e-3 + ) + assert np.isclose(flight_to_save.apogee_time, flight_loaded.apogee_time, rtol=1e-3) assert np.isclose(flight_to_save.t_final, flight_loaded.t_final, rtol=1e-2) os.remove("flight.json") @@ -121,6 +133,13 @@ def test_rocket_encoder(rocket_name, request): rocket_loaded = json.loads(json_encoded, cls=RocketPyDecoder) - # assert np.isclose(rocket_to_encode.rocket_mass, rocket_loaded.rocket_mass) - # assert np.isclose(rocket_to_encode.propellant_mass, rocket_loaded.propellant_mass) - # assert np.isclose(rocket_to_encode.dry_mass, rocket_loaded.dry_mass) + sample_times = np.linspace(0, 3.9, 100) + + assert np.allclose( + rocket_to_encode.evaluate_total_mass()(sample_times), + rocket_loaded.evaluate_total_mass()(sample_times), + ) + assert np.allclose( + rocket_to_encode.static_margin(sample_times), + rocket_loaded.static_margin(sample_times), + ) diff --git a/tests/integration/test_flight.py b/tests/integration/test_flight.py index fc1dd1956..62938ecdb 100644 --- a/tests/integration/test_flight.py +++ b/tests/integration/test_flight.py @@ -154,7 +154,7 @@ def test_export_pressures(flight_calisto_robust): @patch("matplotlib.pyplot.show") def test_hybrid_motor_flight( - mock_show, calisto_hybrid_modded + mock_show, flight_calisto_hybrid_modded ): # pylint: disable=unused-argument """Test the flight of a rocket with a hybrid motor. This test only validates that a flight simulation can be performed with a hybrid motor; it does not @@ -164,24 +164,15 @@ def test_hybrid_motor_flight( ---------- mock_show : unittest.mock.MagicMock Mock object to replace matplotlib.pyplot.show - calisto_hybrid_modded : rocketpy.Rocket - Sample rocket to be simulated. See the conftest.py file for more info. + flight_calisto_hybrid_modded : rocketpy.Flight + Sample Flight to be tested. See the conftest.py file for more info. """ - test_flight = Flight( - rocket=calisto_hybrid_modded, - environment=Environment(), - rail_length=5, - inclination=85, - heading=0, - max_time_step=0.25, - ) - - assert test_flight.all_info() is None + assert flight_calisto_hybrid_modded.all_info() is None @patch("matplotlib.pyplot.show") def test_liquid_motor_flight( - mock_show, calisto_liquid_modded + mock_show, flight_calisto_liquid_modded ): # pylint: disable=unused-argument """Test the flight of a rocket with a liquid motor. This test only validates that a flight simulation can be performed with a liquid motor; it does not @@ -191,19 +182,10 @@ def test_liquid_motor_flight( ---------- mock_show : unittest.mock.MagicMock Mock object to replace matplotlib.pyplot.show - calisto_liquid_modded : rocketpy.Rocket - Sample Rocket to be simulated. See the conftest.py file for more info. + flight_calisto_liquid_modded : rocketpy.Flight + Sample Flight to be tested. See the conftest.py file for more info. """ - test_flight = Flight( - rocket=calisto_liquid_modded, - environment=Environment(), - rail_length=5, - inclination=85, - heading=0, - max_time_step=0.25, - ) - - assert test_flight.all_info() is None + assert flight_calisto_liquid_modded.all_info() is None @pytest.mark.slow From d61f2372d88b618922c15a95d45fd5fad20ffaba Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Sun, 22 Sep 2024 12:18:46 -0300 Subject: [PATCH 4/7] STY: solve pylint remarks. --- rocketpy/mathutils/function.py | 1 - tests/fixtures/hybrid/hybrid_fixtures.py | 2 -- tests/integration/test_flight.py | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index a42534e89..fd251ba66 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -13,7 +13,6 @@ from inspect import signature from pathlib import Path -import dill import matplotlib.pyplot as plt import numpy as np from scipy import integrate, linalg, optimize diff --git a/tests/fixtures/hybrid/hybrid_fixtures.py b/tests/fixtures/hybrid/hybrid_fixtures.py index 7c89442c2..745887315 100644 --- a/tests/fixtures/hybrid/hybrid_fixtures.py +++ b/tests/fixtures/hybrid/hybrid_fixtures.py @@ -1,5 +1,3 @@ -from math import exp - import numpy as np import pytest diff --git a/tests/integration/test_flight.py b/tests/integration/test_flight.py index 62938ecdb..17a2f4b0b 100644 --- a/tests/integration/test_flight.py +++ b/tests/integration/test_flight.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from rocketpy import Environment, Flight +from rocketpy import Flight plt.rcParams.update({"figure.max_open_warning": 0}) From 4c908f8129497296d8c8a9d98becf9a88ca5a449 Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Sun, 22 Sep 2024 13:13:27 -0300 Subject: [PATCH 5/7] MNT: adapt encoding to new post merge attributes. --- rocketpy/motors/hybrid_motor.py | 2 +- rocketpy/motors/liquid_motor.py | 2 +- rocketpy/motors/motor.py | 2 +- rocketpy/motors/solid_motor.py | 2 +- rocketpy/rocket/rocket.py | 15 +++++++++++++-- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index 7af812645..85a413767 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -619,7 +619,7 @@ def from_dict(cls, data): thrust_source=data["thrust"], burn_time=data["_burn_time"], nozzle_radius=data["nozzle_radius"], - dry_mass=data["dry_mass"], + dry_mass=data["_dry_mass"], center_of_dry_mass_position=data["center_of_dry_mass_position"], dry_inertia=( data["dry_I_11"], diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index 995f8ef1b..1727105a8 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -486,7 +486,7 @@ def from_dict(cls, data): thrust_source=data["thrust"], burn_time=data["_burn_time"], nozzle_radius=data["nozzle_radius"], - dry_mass=data["dry_mass"], + dry_mass=data["_dry_mass"], center_of_dry_mass_position=data["center_of_dry_mass_position"], dry_inertia=( data["dry_I_11"], diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index 890f36367..30826e9f0 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -1491,7 +1491,7 @@ def from_dict(cls, data): chamber_position=data["chamber_position"], propellant_initial_mass=data["propellant_initial_mass"], nozzle_radius=data["nozzle_radius"], - dry_mass=data["dry_mass"], + dry_mass=data["_dry_mass"], center_of_dry_mass_position=data["center_of_dry_mass_position"], dry_inertia=( data["dry_I_11"], diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index 91f0d2a4d..7e96fe547 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -743,7 +743,7 @@ def all_info(self): def from_dict(cls, data): return cls( thrust_source=data["thrust"], - dry_mass=data["dry_mass"], + dry_mass=data["_dry_mass"], dry_inertia=( data["dry_I_11"], data["dry_I_22"], diff --git a/rocketpy/rocket/rocket.py b/rocketpy/rocket/rocket.py index 85def6285..b6e78cf48 100644 --- a/rocketpy/rocket/rocket.py +++ b/rocketpy/rocket/rocket.py @@ -1857,6 +1857,17 @@ def all_info(self): self.info() self.plots.all() + def to_dict(self): + data = vars(self) + + data = {**data} + + data.pop("prints") + data.pop("plots") + data.pop("surfaces_cp_to_cdm") + + return data + @classmethod def from_dict(cls, data): rocket = cls( @@ -1887,8 +1898,8 @@ def from_dict(cls, data): for button, position in data["rail_buttons"]: rocket.set_rail_buttons( - position + button.buttons_distance, - position, + position[2] + button.buttons_distance, + position[2], button.angular_position, button.rocket_radius, ) From 022fa575bfb7f317d7a8f92223f67bde9c351dfd Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Sun, 22 Sep 2024 13:22:57 -0300 Subject: [PATCH 6/7] MNT: restore typo to correct values on flight test. --- tests/fixtures/flight/flight_fixtures.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/fixtures/flight/flight_fixtures.py b/tests/fixtures/flight/flight_fixtures.py index 30c93822e..0a9a39ff9 100644 --- a/tests/fixtures/flight/flight_fixtures.py +++ b/tests/fixtures/flight/flight_fixtures.py @@ -113,11 +113,10 @@ def flight_calisto_liquid_modded(calisto_liquid_modded, example_plain_env): return Flight( rocket=calisto_liquid_modded, environment=example_plain_env, - rail_length=5.2, + rail_length=5, inclination=85, heading=0, - time_overshoot=False, - terminate_on_apogee=True, + max_time_step=0.25, ) From 74ba83645f42975b9e9ce81b093edc76a0726c9d Mon Sep 17 00:00:00 2001 From: Pedro Bressan Date: Fri, 8 Nov 2024 21:46:19 +0100 Subject: [PATCH 7/7] ENH: add option for including outputs on JSON export. --- rocketpy/_encoders.py | 79 +++++++++--- rocketpy/environment/environment.py | 95 +++++++------- rocketpy/environment/environment_analysis.py | 5 +- rocketpy/mathutils/function.py | 8 +- rocketpy/mathutils/vector_matrix.py | 18 ++- rocketpy/motors/fluid.py | 7 ++ rocketpy/motors/hybrid_motor.py | 37 +++++- rocketpy/motors/liquid_motor.py | 19 ++- rocketpy/motors/motor.py | 72 ++++++++++- rocketpy/motors/solid_motor.py | 35 +++++- rocketpy/motors/tank.py | 117 ++++++++++++------ rocketpy/motors/tank_geometry.py | 52 +++++--- .../aero_surface/fins/elliptical_fins.py | 30 ++++- rocketpy/rocket/aero_surface/fins/fins.py | 24 ++++ .../aero_surface/fins/free_form_fins.py | 33 +++++ .../aero_surface/fins/trapezoidal_fins.py | 37 ++++-- rocketpy/rocket/aero_surface/nose_cone.py | 17 +++ rocketpy/rocket/aero_surface/rail_buttons.py | 17 +++ rocketpy/rocket/aero_surface/tail.py | 29 ++++- rocketpy/rocket/components.py | 16 +-- rocketpy/rocket/parachute.py | 23 ++-- rocketpy/rocket/rocket.py | 99 +++++++++++---- rocketpy/simulation/flight.py | 55 +++++++- rocketpy/simulation/monte_carlo.py | 4 +- tests/fixtures/hybrid/hybrid_fixtures.py | 2 +- tests/fixtures/motor/tanks_fixtures.py | 2 +- tests/integration/test_encoding.py | 33 ++--- 27 files changed, 739 insertions(+), 226 deletions(-) diff --git a/rocketpy/_encoders.py b/rocketpy/_encoders.py index 7bf0c5f70..d3d7005ca 100644 --- a/rocketpy/_encoders.py +++ b/rocketpy/_encoders.py @@ -13,6 +13,10 @@ class RocketPyEncoder(json.JSONEncoder): """Custom JSON encoder for RocketPy objects. It defines how to encode different types of objects to a JSON supported format.""" + def __init__(self, *args, **kwargs): + self.include_outputs = kwargs.pop("include_outputs", True) + super().__init__(*args, **kwargs) + def default(self, o): if isinstance( o, @@ -40,24 +44,17 @@ def default(self, o): elif hasattr(o, "__iter__") and not isinstance(o, str): return list(o) elif hasattr(o, "to_dict"): - encoding = o.to_dict() + encoding = o.to_dict(self.include_outputs) + encoding = remove_circular_references(encoding) encoding["signature"] = get_class_signature(o) return encoding elif hasattr(o, "__dict__"): - exception_set = {"prints", "plots"} - encoding = { - key: value - for key, value in o.__dict__.items() - if key not in exception_set - } - - if "rocketpy" in o.__class__.__module__ and not any( - subclass in o.__class__.__name__ - for subclass in ["FlightPhase", "TimeNode"] - ): + encoding = remove_circular_references(o.__dict__) + + if "rocketpy" in o.__class__.__module__: encoding["signature"] = get_class_signature(o) return encoding @@ -90,24 +87,72 @@ def object_hook(self, obj): } return class_(**kwargs) - except ImportError: # AttributeException + except (ImportError, AttributeError): return obj else: return obj def get_class_signature(obj): + """Returns the signature of a class in the form of a string. + The signature is an importable string that can be used to import + the class by its module. + + Parameters + ---------- + obj : object + Object to get the signature from. + + Returns + ------- + str + Signature of the class. + """ class_ = obj.__class__ + name = getattr(class_, '__qualname__', class_.__name__) - return f"{class_.__module__}.{class_.__name__}" + return {"module": class_.__module__, "name": name} def get_class_from_signature(signature): - module_name, class_name = signature.rsplit(".", 1) + """Returns the class by importing its signature. + + Parameters + ---------- + signature : str + Signature of the class. - module = import_module(module_name) + Returns + ------- + type + Class defined by the signature. + """ + module = import_module(signature["module"]) + inner_class = None + + for class_ in signature["name"].split("."): + inner_class = getattr(module, class_) + + return inner_class + + +def remove_circular_references(obj_dict): + """Removes circular references from a dictionary. + + Parameters + ---------- + obj_dict : dict + Dictionary to remove circular references from. + + Returns + ------- + dict + Dictionary without circular references. + """ + obj_dict.pop("prints", None) + obj_dict.pop("plots", None) - return getattr(module, class_name) + return obj_dict def to_hex_encode(obj, encoder=base64.b85encode): diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index 1eab32ac9..795d42a93 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -2743,8 +2743,8 @@ def decimal_degrees_to_arc_seconds(angle): arc_seconds = (remainder * 60 - arc_minutes) * 60 return degrees, arc_minutes, arc_seconds - def to_dict(self): - return { + def to_dict(self, include_outputs=True): + env_dict = { "gravity": self.gravity, "date": self.date, "latitude": self.latitude, @@ -2764,9 +2764,16 @@ def to_dict(self): "wind_speed": self.wind_speed, } + if include_outputs: + env_dict["density"] = self.density + env_dict["speed_of_sound"] = self.speed_of_sound + env_dict["dynamic_viscosity"] = self.dynamic_viscosity + + return env_dict + @classmethod def from_dict(cls, data): # pylint: disable=too-many-statements - environment = cls( + env = cls( gravity=data["gravity"], date=data["date"], latitude=data["latitude"], @@ -2779,9 +2786,9 @@ def from_dict(cls, data): # pylint: disable=too-many-statements atmospheric_model = data["atmospheric_model_type"] if atmospheric_model == "standard_atmosphere": - environment.set_atmospheric_model("standard_atmosphere") + env.set_atmospheric_model("standard_atmosphere") elif atmospheric_model == "custom_atmosphere": - environment.set_atmospheric_model( + env.set_atmospheric_model( type="custom_atmosphere", pressure=data["pressure"], temperature=data["temperature"], @@ -2789,56 +2796,42 @@ def from_dict(cls, data): # pylint: disable=too-many-statements wind_v=data["wind_velocity_y"], ) else: - environment.__set_pressure_function(data["pressure"]) - environment.__set_barometric_height_function(data["barometric_height"]) - environment.__set_temperature_function(data["temperature"]) - environment.__set_wind_velocity_x_function(data["wind_velocity_x"]) - environment.__set_wind_velocity_y_function(data["wind_velocity_y"]) - environment.__set_wind_heading_function(data["wind_heading"]) - environment.__set_wind_direction_function(data["wind_direction"]) - environment.__set_wind_speed_function(data["wind_speed"]) - environment.elevation = data["elevation"] - environment.max_expected_height = data["_max_expected_height"] + env.__set_pressure_function(data["pressure"]) + env.__set_barometric_height_function(data["barometric_height"]) + env.__set_temperature_function(data["temperature"]) + env.__set_wind_velocity_x_function(data["wind_velocity_x"]) + env.__set_wind_velocity_y_function(data["wind_velocity_y"]) + env.__set_wind_heading_function(data["wind_heading"]) + env.__set_wind_direction_function(data["wind_direction"]) + env.__set_wind_speed_function(data["wind_speed"]) + env.elevation = data["elevation"] + env.max_expected_height = data["_max_expected_height"] if atmospheric_model in ["windy", "forecast", "reanalysis", "ensemble"]: - environment.atmospheric_model_init_date = data[ - "atmospheric_model_init_date" - ] - environment.atmospheric_model_end_date = data[ - "atmospheric_model_end_date" - ] - environment.atmospheric_model_interval = data[ - "atmospheric_model_interval" - ] - environment.atmospheric_model_init_lat = data[ - "atmospheric_model_init_lat" - ] - environment.atmospheric_model_end_lat = data[ - "atmospheric_model_end_lat" - ] - environment.atmospheric_model_init_lon = data[ - "atmospheric_model_init_lon" - ] - environment.atmospheric_model_end_lon = data[ - "atmospheric_model_end_lon" - ] + env.atmospheric_model_init_date = data["atmospheric_model_init_date"] + env.atmospheric_model_end_date = data["atmospheric_model_end_date"] + env.atmospheric_model_interval = data["atmospheric_model_interval"] + env.atmospheric_model_init_lat = data["atmospheric_model_init_lat"] + env.atmospheric_model_end_lat = data["atmospheric_model_end_lat"] + env.atmospheric_model_init_lon = data["atmospheric_model_init_lon"] + env.atmospheric_model_end_lon = data["atmospheric_model_end_lon"] if atmospheric_model == "ensemble": - environment.level_ensemble = data["level_ensemble"] - environment.height_ensemble = data["height_ensemble"] - environment.temperature_ensemble = data["temperature_ensemble"] - environment.wind_u_ensemble = data["wind_u_ensemble"] - environment.wind_v_ensemble = data["wind_v_ensemble"] - environment.wind_heading_ensemble = data["wind_heading_ensemble"] - environment.wind_direction_ensemble = data["wind_direction_ensemble"] - environment.wind_speed_ensemble = data["wind_speed_ensemble"] - environment.num_ensemble_members = data["num_ensemble_members"] - - environment.calculate_density_profile() - environment.calculate_speed_of_sound_profile() - environment.calculate_dynamic_viscosity() - - return environment + env.level_ensemble = data["level_ensemble"] + env.height_ensemble = data["height_ensemble"] + env.temperature_ensemble = data["temperature_ensemble"] + env.wind_u_ensemble = data["wind_u_ensemble"] + env.wind_v_ensemble = data["wind_v_ensemble"] + env.wind_heading_ensemble = data["wind_heading_ensemble"] + env.wind_direction_ensemble = data["wind_direction_ensemble"] + env.wind_speed_ensemble = data["wind_speed_ensemble"] + env.num_ensemble_members = data["num_ensemble_members"] + + env.calculate_density_profile() + env.calculate_speed_of_sound_profile() + env.calculate_dynamic_viscosity() + + return env if __name__ == "__main__": diff --git a/rocketpy/environment/environment_analysis.py b/rocketpy/environment/environment_analysis.py index 6b917d88a..35d2ac7f2 100644 --- a/rocketpy/environment/environment_analysis.py +++ b/rocketpy/environment/environment_analysis.py @@ -423,7 +423,10 @@ def __check_coordinates_inside_grid( or lat_index > len(lat_array) - 1 ): raise ValueError( - f"Latitude and longitude pair {(self.latitude, self.longitude)} is outside the grid available in the given file, which is defined by {(lat_array[0], lon_array[0])} and {(lat_array[-1], lon_array[-1])}." + f"Latitude and longitude pair {(self.latitude, self.longitude)} " + "is outside the grid available in the given file, which " + f"is defined by {(lat_array[0], lon_array[0])} and " + f"{(lat_array[-1], lon_array[-1])}." ) def __localize_input_dates(self): diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index e31ad9f6f..2ad3c385e 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -711,9 +711,9 @@ def set_discrete( if func.__dom_dim__ == 1: xs = np.linspace(lower, upper, samples) ys = func.get_value(xs.tolist()) if one_by_one else func.get_value(xs) - func.set_source(np.concatenate(([xs], [ys])).transpose()) - func.set_interpolation(interpolation) - func.set_extrapolation(extrapolation) + func.__interpolation__ = interpolation + func.__extrapolation__ = extrapolation + func.set_source(np.column_stack((xs, ys))) elif func.__dom_dim__ == 2: lower = 2 * [lower] if isinstance(lower, NUMERICAL_TYPES) else lower upper = 2 * [upper] if isinstance(upper, NUMERICAL_TYPES) else upper @@ -3389,7 +3389,7 @@ def __validate_extrapolation(self, extrapolation): extrapolation = "natural" return extrapolation - def to_dict(self): + def to_dict(self, _): """Serializes the Function instance to a dictionary. Returns diff --git a/rocketpy/mathutils/vector_matrix.py b/rocketpy/mathutils/vector_matrix.py index 0da44935d..08aa03cac 100644 --- a/rocketpy/mathutils/vector_matrix.py +++ b/rocketpy/mathutils/vector_matrix.py @@ -403,6 +403,10 @@ def zeros(): """Returns the zero vector.""" return Vector([0, 0, 0]) + def to_dict(self, _): + """Returns the vector as a JSON compatible element.""" + return list(self.components) + @staticmethod def i(): """Returns the i vector, [1, 0, 0].""" @@ -418,9 +422,10 @@ def k(): """Returns the k vector, [0, 0, 1].""" return Vector([0, 0, 1]) - def to_dict(self): - """Returns the vector as a JSON compatible element.""" - return list(self.components) + @classmethod + def from_dict(cls, data): + """Creates a Vector instance from a dictionary.""" + return cls(data) class Matrix: @@ -1002,7 +1007,7 @@ def __repr__(self): + f" [{self.zx}, {self.zy}, {self.zz}])" ) - def to_dict(self): + def to_dict(self, _): """Returns the matrix as a JSON compatible element.""" return [list(row) for row in self.components] @@ -1092,6 +1097,11 @@ def transformation_euler_angles(roll, pitch, roll2): """ return Matrix.transformation(euler313_to_quaternions(roll, pitch, roll2)) + @classmethod + def from_dict(cls, data): + """Creates a Matrix instance from a dictionary.""" + return cls(data) + if __name__ == "__main__": import doctest diff --git a/rocketpy/motors/fluid.py b/rocketpy/motors/fluid.py index 4be124ec3..3d6efca48 100644 --- a/rocketpy/motors/fluid.py +++ b/rocketpy/motors/fluid.py @@ -60,3 +60,10 @@ def __str__(self): """ return f"Fluid: {self.name}" + + def to_dict(self, _): + return {"name": self.name, "density": self.density} + + @classmethod + def from_dict(cls, data): + return cls(data["name"], data["density"]) diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index 1f0359424..0fef8d06c 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -615,13 +615,44 @@ def all_info(self): self.prints.all() self.plots.all() + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data.update( + { + "grain_number": self.grain_number, + "grain_density": self.grain_density, + "grain_outer_radius": self.grain_outer_radius, + "grain_initial_inner_radius": self.grain_initial_inner_radius, + "grain_initial_height": self.grain_initial_height, + "grain_separation": self.grain_separation, + "grains_center_of_mass_position": self.grains_center_of_mass_position, + "throat_radius": self.throat_radius, + "positioned_tanks": [ + {"tank": tank["tank"], "position": tank["position"]} + for tank in self.positioned_tanks + ], + } + ) + + if include_outputs: + data.update( + { + "grain_inner_radius": self.solid.grain_inner_radius, + "grain_height": self.solid.grain_height, + "burn_area": self.solid.burn_area, + "burn_rate": self.solid.burn_rate, + } + ) + + return data + @classmethod def from_dict(cls, data): motor = cls( - thrust_source=data["thrust"], - burn_time=data["_burn_time"], + thrust_source=data["thrust_source"], + burn_time=data["burn_time"], nozzle_radius=data["nozzle_radius"], - dry_mass=data["_dry_mass"], + dry_mass=data["dry_mass"], center_of_dry_mass_position=data["center_of_dry_mass_position"], dry_inertia=( data["dry_I_11"], diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index 8fdd98e95..d90a6e26d 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -482,13 +482,25 @@ def all_info(self): self.prints.all() self.plots.all() + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data.update( + { + "positioned_tanks": [ + {"tank": tank["tank"], "position": tank["position"]} + for tank in self.positioned_tanks + ], + } + ) + return data + @classmethod def from_dict(cls, data): motor = cls( - thrust_source=data["thrust"], - burn_time=data["_burn_time"], + thrust_source=data["thrust_source"], + burn_time=data["burn_time"], nozzle_radius=data["nozzle_radius"], - dry_mass=data["_dry_mass"], + dry_mass=data["dry_mass"], center_of_dry_mass_position=data["center_of_dry_mass_position"], dry_inertia=( data["dry_I_11"], @@ -498,6 +510,7 @@ def from_dict(cls, data): data["dry_I_13"], data["dry_I_23"], ), + nozzle_position=data["nozzle_position"], interpolation_method=data["interpolate"], coordinate_system_orientation=data["coordinate_system_orientation"], ) diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index ac002ecce..5e68b2e10 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -1083,6 +1083,60 @@ def get_attr_value(obj, attr_name, multiplier=1): # Write last line file.write(f"{self.thrust.source[-1, 0]:.4f} {0:.3f}\n") + def to_dict(self, include_outputs=True): + thrust_source = self.thrust_source + + if isinstance(thrust_source, str): + thrust_source = self.thrust.source + elif callable(thrust_source) and not isinstance(thrust_source, Function): + thrust_source = Function(thrust_source) + + data = { + "thrust_source": self.thrust, + "dry_I_11": self.dry_I_11, + "dry_I_22": self.dry_I_22, + "dry_I_33": self.dry_I_33, + "dry_I_12": self.dry_I_12, + "dry_I_13": self.dry_I_13, + "dry_I_23": self.dry_I_23, + "nozzle_radius": self.nozzle_radius, + "center_of_dry_mass_position": self.center_of_dry_mass_position, + "dry_mass": self.dry_mass, + "nozzle_position": self.nozzle_position, + "burn_time": self.burn_time, + "interpolate": self.interpolate, + "coordinate_system_orientation": self.coordinate_system_orientation, + } + + if include_outputs: + data.update( + { + "total_mass": self.total_mass, + "propellant_mass": self.propellant_mass, + "mass_flow_rate": self.mass_flow_rate, + "center_of_mass": self.center_of_mass, + "center_of_propellant_mass": self.center_of_propellant_mass, + "total_impulse": self.total_impulse, + "exhaust_velocity": self.exhaust_velocity, + "propellant_initial_mass": self.propellant_initial_mass, + "structural_mass_ratio": self.structural_mass_ratio, + "I_11": self.I_11, + "I_22": self.I_22, + "I_33": self.I_33, + "I_12": self.I_12, + "I_13": self.I_13, + "I_23": self.I_23, + "propellant_I_11": self.propellant_I_11, + "propellant_I_22": self.propellant_I_22, + "propellant_I_33": self.propellant_I_33, + "propellant_I_12": self.propellant_I_12, + "propellant_I_13": self.propellant_I_13, + "propellant_I_23": self.propellant_I_23, + } + ) + + return data + def info(self): """Prints out a summary of the data and graphs available about the Motor. @@ -1501,17 +1555,29 @@ def all_info(self): self.prints.all() self.plots.all() + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data.update( + { + "chamber_radius": self.chamber_radius, + "chamber_height": self.chamber_height, + "chamber_position": self.chamber_position, + "propellant_initial_mass": self.propellant_initial_mass, + } + ) + return data + @classmethod def from_dict(cls, data): return cls( - thrust_source=data["thrust"], - burn_time=data["_burn_time"], + thrust_source=data["thrust_source"], + burn_time=data["burn_time"], chamber_radius=data["chamber_radius"], chamber_height=data["chamber_height"], chamber_position=data["chamber_position"], propellant_initial_mass=data["propellant_initial_mass"], nozzle_radius=data["nozzle_radius"], - dry_mass=data["_dry_mass"], + dry_mass=data["dry_mass"], center_of_dry_mass_position=data["center_of_dry_mass_position"], dry_inertia=( data["dry_I_11"], diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index d87e235cb..32dd0d3f7 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -741,11 +741,40 @@ def all_info(self): self.prints.all() self.plots.all() + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data.update( + { + "nozzle_radius": self.nozzle_radius, + "throat_radius": self.throat_radius, + "grain_number": self.grain_number, + "grain_density": self.grain_density, + "grain_outer_radius": self.grain_outer_radius, + "grain_initial_inner_radius": self.grain_initial_inner_radius, + "grain_initial_height": self.grain_initial_height, + "grain_separation": self.grain_separation, + "grains_center_of_mass_position": self.grains_center_of_mass_position, + } + ) + + if include_outputs: + data.update( + { + "grain_inner_radius": self.grain_inner_radius, + "grain_height": self.grain_height, + "burn_area": self.burn_area, + "burn_rate": self.burn_rate, + "Kn": self.Kn, + } + ) + + return data + @classmethod def from_dict(cls, data): return cls( - thrust_source=data["thrust"], - dry_mass=data["_dry_mass"], + thrust_source=data["thrust_source"], + dry_mass=data["dry_mass"], dry_inertia=( data["dry_I_11"], data["dry_I_22"], @@ -764,7 +793,7 @@ def from_dict(cls, data): grains_center_of_mass_position=data["grains_center_of_mass_position"], center_of_dry_mass_position=data["center_of_dry_mass_position"], nozzle_position=data["nozzle_position"], - burn_time=data["_burn_time"], + burn_time=data["burn_time"], throat_radius=data["throat_radius"], interpolation_method=data["interpolate"], coordinate_system_orientation=data["coordinate_system_orientation"], diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index 49b7992c8..69870d3a9 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -480,8 +480,8 @@ def draw(self): """Draws the tank geometry.""" self.plots.draw() - def to_dict(self): - return { + def to_dict(self, include_outputs=True): + data = { "name": self.name, "geometry": self.geometry, "flux_time": self.flux_time, @@ -489,6 +489,26 @@ def to_dict(self): "gas": self.gas, "discretize": self.discretize, } + if include_outputs: + data.update( + { + "fluid_mass": self.fluid_mass, + "net_mass_flow_rate": self.net_mass_flow_rate, + "liquid_volume": self.liquid_volume, + "gas_volume": self.gas_volume, + "liquid_height": self.liquid_height, + "gas_height": self.gas_height, + "liquid_mass": self.liquid_mass, + "gas_mass": self.gas_mass, + "liquid_center_of_mass": self.liquid_center_of_mass, + "gas_center_of_mass": self.gas_center_of_mass, + "center_of_mass": self.center_of_mass, + "liquid_inertia": self.liquid_inertia, + "gas_inertia": self.gas_inertia, + "inertia": self.inertia, + } + ) + return data class MassFlowRateBasedTank(Tank): @@ -826,19 +846,32 @@ def discretize_flow(self): """Discretizes the mass flow rate inputs according to the flux time and the discretize parameter. """ - self.liquid_mass_flow_rate_in.set_discrete(*self.flux_time, self.discretize) - self.gas_mass_flow_rate_in.set_discrete(*self.flux_time, self.discretize) - self.liquid_mass_flow_rate_out.set_discrete(*self.flux_time, self.discretize) - self.gas_mass_flow_rate_out.set_discrete(*self.flux_time, self.discretize) - - def to_dict(self): - return { - **super().to_dict(), - "initial_liquid_mass": self.initial_liquid_mass, - "initial_gas_mass": self.initial_gas_mass, - "liquid_mass_flow_rate_in": self.liquid_mass_flow_rate_in, - "gas_mass_flow_rate_in": self.gas_mass_flow_rate_in, - } + self.liquid_mass_flow_rate_in.set_discrete( + *self.flux_time, self.discretize, "linear" + ) + self.gas_mass_flow_rate_in.set_discrete( + *self.flux_time, self.discretize, "linear" + ) + self.liquid_mass_flow_rate_out.set_discrete( + *self.flux_time, self.discretize, "linear" + ) + self.gas_mass_flow_rate_out.set_discrete( + *self.flux_time, self.discretize, "linear" + ) + + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data.update( + { + "initial_liquid_mass": self.initial_liquid_mass, + "initial_gas_mass": self.initial_gas_mass, + "liquid_mass_flow_rate_in": self.liquid_mass_flow_rate_in, + "gas_mass_flow_rate_in": self.gas_mass_flow_rate_in, + "liquid_mass_flow_rate_out": self.liquid_mass_flow_rate_out, + "gas_mass_flow_rate_out": self.gas_mass_flow_rate_out, + } + ) + return data @classmethod def from_dict(cls, data): @@ -1050,13 +1083,12 @@ def gas_height(self): def discretize_ullage(self): """Discretizes the ullage input according to the flux time and the discretize parameter.""" - self.ullage.set_discrete(*self.flux_time, self.discretize) + self.ullage.set_discrete(*self.flux_time, self.discretize, "linear") - def to_dict(self): - return { - **super().to_dict(), - "ullage": self.ullage, - } + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data.update({"ullage": self.ullage}) + return data @classmethod def from_dict(cls, data): @@ -1278,13 +1310,12 @@ def discretize_liquid_height(self): """Discretizes the liquid height input according to the flux time and the discretize parameter. """ - self.liquid_level.set_discrete(*self.flux_time, self.discretize) + self.liquid_level.set_discrete(*self.flux_time, self.discretize, "linear") - def to_dict(self): - return { - **super().to_dict(), - "liquid_height": self.liquid_height, - } + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data.update({"liquid_height": self.liquid_level}) + return data @classmethod def from_dict(cls, data): @@ -1536,12 +1567,28 @@ def discretize_masses(self): """Discretizes the fluid mass inputs according to the flux time and the discretize parameter. """ - self.liquid_mass.set_discrete(*self.flux_time, self.discretize) - self.gas_mass.set_discrete(*self.flux_time, self.discretize) + self.liquid_mass.set_discrete(*self.flux_time, self.discretize, "linear") + self.gas_mass.set_discrete(*self.flux_time, self.discretize, "linear") - def to_dict(self): - return { - **super().to_dict(), - "liquid_mass": self.liquid_mass, - "gas_mass": self.gas_mass, - } + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data.update( + { + "liquid_mass": self.liquid_mass, + "gas_mass": self.gas_mass, + } + ) + return data + + @classmethod + def from_dict(cls, data): + return cls( + name=data["name"], + geometry=data["geometry"], + flux_time=data["flux_time"], + liquid=data["liquid"], + gas=data["gas"], + liquid_mass=data["liquid_mass"], + gas_mass=data["gas_mass"], + discretize=data["discretize"], + ) diff --git a/rocketpy/motors/tank_geometry.py b/rocketpy/motors/tank_geometry.py index bd9d9e4e3..a26eb7fbf 100644 --- a/rocketpy/motors/tank_geometry.py +++ b/rocketpy/motors/tank_geometry.py @@ -345,22 +345,24 @@ def add_geometry(self, domain, radius_function): self._geometry[domain] = Function(radius_function) self.radius = PiecewiseFunction(self._geometry, "Height (m)", "radius (m)") - def to_dict(self): - """ - Returns a dictionary representation of the TankGeometry object. - - Returns - ------- - dict - Dictionary representation of the TankGeometry object. - """ - return { + def to_dict(self, include_outputs=True): + data = { "geometry": { - str(domain): function.to_dict() - for domain, function in self._geometry.items() + str(domain): function for domain, function in self._geometry.items() } } + if include_outputs: + data["outputs"] = { + "average_radius": self.average_radius, + "bottom": self.bottom, + "top": self.top, + "total_height": self.total_height, + "total_volume": self.total_volume, + } + + return data + @classmethod def from_dict(cls, data): geometry_dict = {} @@ -398,6 +400,7 @@ def __init__(self, radius, height, spherical_caps=False, geometry_dict=None): """ geometry_dict = geometry_dict or {} super().__init__(geometry_dict) + self.__input_radius = radius self.height = height self.has_caps = False if spherical_caps: @@ -417,11 +420,11 @@ def add_spherical_caps(self): "Warning: Adding spherical caps to the tank will not modify the " + f"total height of the tank {self.height} m. " + "Its cylindrical portion height will be reduced to " - + f"{self.height - 2*self.radius(0)} m." + + f"{self.height - 2*self.__input_radius} m." ) if not self.has_caps: - radius = self.radius(0) + radius = self.__input_radius height = self.height bottom_cap_range = (-height / 2, -height / 2 + radius) upper_cap_range = (height / 2 - radius, height / 2) @@ -438,13 +441,18 @@ def upper_cap_radius(h): else: raise ValueError("Tank already has caps.") - def to_dict(self): - return { - "radius": self.radius(0), + def to_dict(self, include_outputs=True): + data = { + "radius": self.__input_radius, "height": self.height, "spherical_caps": self.has_caps, } + if include_outputs: + data.update(super().to_dict(include_outputs)) + + return data + @classmethod def from_dict(cls, data): return cls(data["radius"], data["height"], data["spherical_caps"]) @@ -470,10 +478,16 @@ def __init__(self, radius, geometry_dict=None): """ geometry_dict = geometry_dict or {} super().__init__(geometry_dict) + self.__input_radius = radius self.add_geometry((-radius, radius), lambda h: (radius**2 - h**2) ** 0.5) - def to_dict(self): - return {"radius": self.radius(0)} + def to_dict(self, include_outputs=True): + data = {"radius": self.__input_radius} + + if include_outputs: + data.update(super().to_dict(include_outputs)) + + return data @classmethod def from_dict(cls, data): diff --git a/rocketpy/rocket/aero_surface/fins/elliptical_fins.py b/rocketpy/rocket/aero_surface/fins/elliptical_fins.py index 86ca71602..961418c38 100644 --- a/rocketpy/rocket/aero_surface/fins/elliptical_fins.py +++ b/rocketpy/rocket/aero_surface/fins/elliptical_fins.py @@ -317,14 +317,32 @@ def all_info(self): self.prints.all() self.plots.all() + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + if include_outputs: + data.update( + { + "Af": self.Af, + "AR": self.AR, + "gamma_c": self.gamma_c, + "Yma": self.Yma, + "roll_geometrical_constant": self.roll_geometrical_constant, + "tau": self.tau, + "lift_interference_factor": self.lift_interference_factor, + "roll_damping_interference_factor": self.roll_damping_interference_factor, + "roll_forcing_interference_factor": self.roll_forcing_interference_factor, + } + ) + return data + @classmethod def from_dict(cls, data): return cls( - n=data["_n"], - root_chord=data["_root_chord"], - span=data["_span"], - rocket_radius=data["_rocket_radius"], - cant_angle=data["_cant_angle"], - airfoil=data["_airfoil"], + n=data["n"], + root_chord=data["root_chord"], + span=data["span"], + rocket_radius=data["rocket_radius"], + cant_angle=data["cant_angle"], + airfoil=data["airfoil"], name=data["name"], ) diff --git a/rocketpy/rocket/aero_surface/fins/fins.py b/rocketpy/rocket/aero_surface/fins/fins.py index 29f4e8cff..bfdd82d71 100644 --- a/rocketpy/rocket/aero_surface/fins/fins.py +++ b/rocketpy/rocket/aero_surface/fins/fins.py @@ -426,6 +426,30 @@ def compute_forces_and_moments( M3 = M3_forcing - M3_damping return R1, R2, R3, M1, M2, M3 + def to_dict(self, include_outputs=True): + data = { + "n": self.n, + "root_chord": self.root_chord, + "span": self.span, + "rocket_radius": self.rocket_radius, + "cant_angle": self.cant_angle, + "airfoil": self.airfoil, + "name": self.name, + } + + if include_outputs: + data.update( + { + "cp": self.cp, + "cl": self.cl, + "roll_parameters": self.roll_parameters, + "d": self.d, + "ref_area": self.ref_area, + } + ) + + return data + def draw(self): """Draw the fin shape along with some important information, including the center line, the quarter line and the center of pressure position. diff --git a/rocketpy/rocket/aero_surface/fins/free_form_fins.py b/rocketpy/rocket/aero_surface/fins/free_form_fins.py index 3abdb84ad..40efb86a4 100644 --- a/rocketpy/rocket/aero_surface/fins/free_form_fins.py +++ b/rocketpy/rocket/aero_surface/fins/free_form_fins.py @@ -359,6 +359,39 @@ def evaluate_shape(self): x_array, y_array = zip(*self.shape_points) self.shape_vec = [np.array(x_array), np.array(y_array)] + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data["shape_points"] = self.shape_points + + if include_outputs: + data.update( + { + "Af": self.Af, + "AR": self.AR, + "gamma_c": self.gamma_c, + "Yma": self.Yma, + "mac_length": self.mac_length, + "mac_lead": self.mac_lead, + "roll_geometrical_constant": self.roll_geometrical_constant, + "tau": self.tau, + "lift_interference_factor": self.lift_interference_factor, + "roll_forcing_interference_factor": self.roll_forcing_interference_factor, + "roll_damping_interference_factor": self.roll_damping_interference_factor, + } + ) + return data + + @classmethod + def from_dict(cls, data): + return cls( + data["n"], + data["shape_points"], + data["rocket_radius"], + data["cant_angle"], + data["airfoil"], + data["name"], + ) + def info(self): self.prints.geometry() self.prints.lift() diff --git a/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py b/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py index 606e397a1..42fe4bb21 100644 --- a/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py +++ b/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py @@ -348,15 +348,38 @@ def all_info(self): self.prints.all() self.plots.all() + def to_dict(self, include_outputs=True): + data = super().to_dict(include_outputs) + data["tip_chord"] = self.tip_chord + + if include_outputs: + data.update( + { + "sweep_length": self.sweep_length, + "sweep_angle": self.sweep_angle, + "shape_vec": self.shape_vec, + "Af": self.Af, + "AR": self.AR, + "gamma_c": self.gamma_c, + "Yma": self.Yma, + "roll_geometrical_constant": self.roll_geometrical_constant, + "tau": self.tau, + "lift_interference_factor": self.lift_interference_factor, + "roll_damping_interference_factor": self.roll_damping_interference_factor, + "roll_forcing_interference_factor": self.roll_forcing_interference_factor, + } + ) + return data + @classmethod def from_dict(cls, data): return cls( - n=data["_n"], - root_chord=data["_root_chord"], - tip_chord=data["_tip_chord"], - span=data["_span"], - rocket_radius=data["_rocket_radius"], - cant_angle=data["_cant_angle"], - airfoil=data["_airfoil"], + n=data["n"], + root_chord=data["root_chord"], + tip_chord=data["tip_chord"], + span=data["span"], + rocket_radius=data["rocket_radius"], + cant_angle=data["cant_angle"], + airfoil=data["airfoil"], name=data["name"], ) diff --git a/rocketpy/rocket/aero_surface/nose_cone.py b/rocketpy/rocket/aero_surface/nose_cone.py index d5ee33151..9ea3754cb 100644 --- a/rocketpy/rocket/aero_surface/nose_cone.py +++ b/rocketpy/rocket/aero_surface/nose_cone.py @@ -516,6 +516,23 @@ def all_info(self): self.prints.all() self.plots.all() + def to_dict(self, include_outputs=True): + data = { + "_length": self._length, + "_kind": self._kind, + "_base_radius": self._base_radius, + "_bluffness": self._bluffness, + "_rocket_radius": self._rocket_radius, + "_power": self._power, + "name": self.name, + } + if include_outputs: + data["cp"] = self.cp + data["clalpha"] = self.clalpha + data["cl"] = self.cl + + return data + @classmethod def from_dict(cls, data): return cls( diff --git a/rocketpy/rocket/aero_surface/rail_buttons.py b/rocketpy/rocket/aero_surface/rail_buttons.py index facd8b157..06ddea73e 100644 --- a/rocketpy/rocket/aero_surface/rail_buttons.py +++ b/rocketpy/rocket/aero_surface/rail_buttons.py @@ -100,6 +100,23 @@ def evaluate_geometrical_parameters(self): None """ + def to_dict(self, _): + return { + "buttons_distance": self.buttons_distance, + "angular_position": self.angular_position, + "name": self.name, + "rocket_radius": self.rocket_radius, + } + + @classmethod + def from_dict(cls, data): + return cls( + data["buttons_distance"], + data["angular_position"], + data["name"], + data["rocket_radius"], + ) + def info(self): """Prints out all the information about the Rail Buttons. diff --git a/rocketpy/rocket/aero_surface/tail.py b/rocketpy/rocket/aero_surface/tail.py index acd055f2e..5c9308783 100644 --- a/rocketpy/rocket/aero_surface/tail.py +++ b/rocketpy/rocket/aero_surface/tail.py @@ -205,12 +205,33 @@ def all_info(self): self.prints.all() self.plots.all() + def to_dict(self, include_outputs=True): + data = { + "top_radius": self._top_radius, + "bottom_radius": self._bottom_radius, + "length": self._length, + "rocket_radius": self._rocket_radius, + "name": self.name, + } + + if include_outputs: + data.update( + { + "cp": self.cp, + "cl": self.clalpha, + "slant_length": self.slant_length, + "surface_area": self.surface_area, + } + ) + + return data + @classmethod def from_dict(cls, data): return cls( - top_radius=data["_top_radius"], - bottom_radius=data["_bottom_radius"], - length=data["_length"], - rocket_radius=data["_rocket_radius"], + top_radius=data["top_radius"], + bottom_radius=data["bottom_radius"], + length=data["length"], + rocket_radius=data["rocket_radius"], name=data["name"], ) diff --git a/rocketpy/rocket/components.py b/rocketpy/rocket/components.py index 1bf36411a..3641aab3e 100644 --- a/rocketpy/rocket/components.py +++ b/rocketpy/rocket/components.py @@ -194,17 +194,17 @@ def sort_by_position(self, reverse=False): """ self._components.sort(key=lambda x: x.position.z, reverse=reverse) - def to_dict(self): - """Return a dictionary representation of the components. - - Returns - ------- - dict - A dictionary representation of the components. - """ + def to_dict(self, _): return { "components": [ {"component": c.component, "position": c.position} for c in self._components ] } + + @classmethod + def from_dict(cls, data): + components = cls() + for component in data["components"]: + components.add(component["component"], component["position"]) + return components diff --git a/rocketpy/rocket/parachute.py b/rocketpy/rocket/parachute.py index 227538230..4abc37d1c 100644 --- a/rocketpy/rocket/parachute.py +++ b/rocketpy/rocket/parachute.py @@ -251,25 +251,29 @@ def all_info(self): self.info() # self.plots.all() # Parachutes still doesn't have plots - def to_dict(self): + def to_dict(self, include_outputs=True): trigger = self.trigger - if callable(self.trigger): + if callable(self.trigger) and not isinstance(self.trigger, Function): trigger = to_hex_encode(trigger) - return { + data = { "name": self.name, "cd_s": self.cd_s, "trigger": trigger, "sampling_rate": self.sampling_rate, "lag": self.lag, "noise": self.noise, - "noise_signal": [ - [self.noise_signal[0][0], to_hex_encode(self.noise_signal[-1][1])] - ], - "noise_function": to_hex_encode(self.noise_function), } + if include_outputs: + data["noise_signal"] = self.noise_signal + data["noise_function"] = to_hex_encode(self.noise_function) + data["noisy_pressure_signal"] = self.noisy_pressure_signal + data["clean_pressure_signal"] = self.clean_pressure_signal + + return data + @classmethod def from_dict(cls, data): trigger = data["trigger"] @@ -288,9 +292,4 @@ def from_dict(cls, data): noise=data["noise"], ) - parachute.noise_signal = [ - [data["noise_signal"][0][0], from_hex_decode(data["noise_signal"][-1][1])] - ] - parachute.noise_function = from_hex_decode(data["noise_function"]) - return parachute diff --git a/rocketpy/rocket/rocket.py b/rocketpy/rocket/rocket.py index 447c5ea8b..511d8c21f 100644 --- a/rocketpy/rocket/rocket.py +++ b/rocketpy/rocket/rocket.py @@ -1889,16 +1889,64 @@ def all_info(self): self.info() self.plots.all() - def to_dict(self): - data = vars(self) - - data = {**data} - - data.pop("prints") - data.pop("plots") - data.pop("surfaces_cp_to_cdm") + def to_dict(self, include_outputs=True): + rocket_dict = { + "radius": self.radius, + "mass": self.mass, + "I_11_without_motor": self.I_11_without_motor, + "I_22_without_motor": self.I_22_without_motor, + "I_33_without_motor": self.I_33_without_motor, + "I_12_without_motor": self.I_12_without_motor, + "I_13_without_motor": self.I_13_without_motor, + "I_23_without_motor": self.I_23_without_motor, + "power_off_drag": self.power_off_drag, + "power_on_drag": self.power_on_drag, + "center_of_mass_without_motor": self.center_of_mass_without_motor, + "coordinate_system_orientation": self.coordinate_system_orientation, + "motor": self.motor, + "motor_position": self.motor_position, + "aerodynamic_surfaces": self.aerodynamic_surfaces, + "rail_buttons": self.rail_buttons, + "parachutes": self.parachutes, + "air_brakes": self.air_brakes, + "_controllers": self._controllers, + "sensors": self.sensors, + } + + if include_outputs: + rocket_dict["area"] = self.area + rocket_dict["center_of_dry_mass_position"] = ( + self.center_of_dry_mass_position + ) + rocket_dict["center_of_mass_without_motor"] = ( + self.center_of_mass_without_motor + ) + rocket_dict["motor_center_of_mass_position"] = ( + self.motor_center_of_mass_position + ) + rocket_dict["motor_center_of_dry_mass_position"] = ( + self.motor_center_of_dry_mass_position + ) + rocket_dict["center_of_mass"] = self.center_of_mass + rocket_dict["reduced_mass"] = self.reduced_mass + rocket_dict["total_mass"] = self.total_mass + rocket_dict["total_mass_flow_rate"] = self.total_mass_flow_rate + rocket_dict["thrust_to_weight"] = self.thrust_to_weight + rocket_dict["cp_eccentricity_x"] = self.cp_eccentricity_x + rocket_dict["cp_eccentricity_y"] = self.cp_eccentricity_y + rocket_dict["thrust_eccentricity_x"] = self.thrust_eccentricity_x + rocket_dict["thrust_eccentricity_y"] = self.thrust_eccentricity_y + rocket_dict["cp_position"] = self.cp_position + rocket_dict["stability_margin"] = self.stability_margin + rocket_dict["static_margin"] = self.static_margin + rocket_dict["nozzle_position"] = self.nozzle_position + rocket_dict["nozzle_to_cdm"] = self.nozzle_to_cdm + rocket_dict["nozzle_gyration_tensor"] = self.nozzle_gyration_tensor + rocket_dict["center_of_propellant_position"] = ( + self.center_of_propellant_position + ) - return data + return rocket_dict @classmethod def from_dict(cls, data): @@ -1921,19 +1969,19 @@ def from_dict(cls, data): if (motor := data["motor"]) is not None: rocket.add_motor( - motor, - data["motor_position"], + motor=motor, + position=data["motor_position"], ) for surface, position in data["aerodynamic_surfaces"]: - rocket.add_surfaces(surface, position) + rocket.add_surfaces(surfaces=surface, positions=position) for button, position in data["rail_buttons"]: rocket.set_rail_buttons( - position[2] + button.buttons_distance, - position[2], - button.angular_position, - button.rocket_radius, + upper_button_position=position[2] + button.buttons_distance, + lower_button_position=position[2], + angular_position=button.angular_position, + radius=button.rocket_radius, ) for parachute in data["parachutes"]: @@ -1941,16 +1989,15 @@ def from_dict(cls, data): for air_brakes in data["air_brakes"]: rocket.add_air_brakes( - air_brakes["drag_coefficient_curve"], - air_brakes["controller_function"], - air_brakes["sampling_rate"], - air_brakes["clamp"], - air_brakes["reference_area"], - air_brakes["initial_observed_variables"], - air_brakes["override_rocket_drag"], - air_brakes["return_controller"], - air_brakes["name"], - air_brakes["controller_name"], + drag_coefficient_curve=air_brakes["drag_coefficient_curve"], + controller_function=air_brakes["controller_function"], + sampling_rate=air_brakes["sampling_rate"], + clamp=air_brakes["clamp"], + reference_area=air_brakes["reference_area"], + initial_observed_variables=air_brakes["initial_observed_variables"], + override_rocket_drag=air_brakes["override_rocket_drag"], + name=air_brakes["name"], + controller_name=air_brakes["controller_name"], ) return rocket diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index 936dd570f..42ca0f897 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -3364,8 +3364,8 @@ def time_iterator(self, node_list): yield i, node_list[i] i += 1 - def to_dict(self): - return { + def to_dict(self, include_outputs=True): + data = { "rocket": self.rocket, "env": self.env, "rail_length": self.rail_length, @@ -3383,6 +3383,57 @@ def to_dict(self): "equations_of_motion": self.equations_of_motion, } + 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, + "vx": self.vx, + "vy": self.vy, + "vz": self.vz, + "e0": self.e0, + "e1": self.e1, + "e2": self.e2, + "e3": self.e3, + "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, + "stream_velocity_y": self.stream_velocity_y, + "stream_velocity_z": self.stream_velocity_z, + "free_stream_speed": self.free_stream_speed, + "angle_of_attack": self.angle_of_attack, + "static_margin": self.static_margin, + "stability_margin": self.stability_margin, + "latitude": self.latitude, + "longitude": self.longitude, + } + ) + + return data + @classmethod def from_dict(cls, data): return cls( diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index c6cd1bed4..4ed051a4f 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -870,10 +870,10 @@ def export_ellipses_to_kml( # pylint: disable=too-many-statements for i, points in enumerate(kml_data): if i < len(impact_ellipses): - name = f"Impact Ellipse {i+1}" + name = f"Impact Ellipse {i + 1}" ellipse_color = colors[0] # default is blue else: - name = f"Apogee Ellipse {i +1- len(impact_ellipses)}" + name = f"Apogee Ellipse {i + 1 - len(impact_ellipses)}" ellipse_color = colors[1] # default is green mult_ell = kml.newmultigeometry(name=name) diff --git a/tests/fixtures/hybrid/hybrid_fixtures.py b/tests/fixtures/hybrid/hybrid_fixtures.py index 745887315..833f62978 100644 --- a/tests/fixtures/hybrid/hybrid_fixtures.py +++ b/tests/fixtures/hybrid/hybrid_fixtures.py @@ -224,7 +224,7 @@ def spherical_oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): ------- rocketpy.UllageBasedTank """ - geometry = SphericalTank(0.05) + geometry = SphericalTank(0.051) oxidizer_tank = LevelBasedTank( name="Lox Tank", flux_time=10, diff --git a/tests/fixtures/motor/tanks_fixtures.py b/tests/fixtures/motor/tanks_fixtures.py index 4238e1e1e..6dd1334f3 100644 --- a/tests/fixtures/motor/tanks_fixtures.py +++ b/tests/fixtures/motor/tanks_fixtures.py @@ -123,7 +123,7 @@ def spherical_oxidizer_tank(oxidizer_fluid, oxidizer_pressurant): ------- rocketpy.UllageBasedTank """ - geometry = SphericalTank(0.05) + geometry = SphericalTank(0.051) liquid_level = Function(lambda t: 0.1 * np.exp(-t / 2) - 0.05) oxidizer_tank = LevelBasedTank( name="Lox Tank", diff --git a/tests/integration/test_encoding.py b/tests/integration/test_encoding.py index 54d73f81d..ebe305b61 100644 --- a/tests/integration/test_encoding.py +++ b/tests/integration/test_encoding.py @@ -9,15 +9,16 @@ @pytest.mark.slow @pytest.mark.parametrize( - "flight_name", + ["flight_name", "include_outputs"], [ - "flight_calisto", - "flight_calisto_robust", - "flight_calisto_liquid_modded", - "flight_calisto_hybrid_modded", + ("flight_calisto", False), + ("flight_calisto", True), + ("flight_calisto_robust", True), + ("flight_calisto_liquid_modded", False), + ("flight_calisto_hybrid_modded", False), ], ) -def test_flight_save_load(flight_name, request): +def test_flight_save_load(flight_name, include_outputs, request): """Test encoding a ``rocketpy.Flight``. Parameters @@ -30,19 +31,23 @@ def test_flight_save_load(flight_name, request): flight_to_save = request.getfixturevalue(flight_name) with open("flight.json", "w") as f: - json.dump(flight_to_save, f, cls=RocketPyEncoder, indent=2) + json.dump( + flight_to_save, + f, + cls=RocketPyEncoder, + indent=2, + include_outputs=include_outputs, + ) with open("flight.json", "r") as f: flight_loaded = json.load(f, cls=RocketPyDecoder) - # TODO: Investigate why hybrid motor needs a higher tolerance + assert np.isclose(flight_to_save.t_initial, flight_loaded.t_initial) + assert np.isclose(flight_to_save.out_of_rail_time, flight_loaded.out_of_rail_time) + assert np.isclose(flight_to_save.apogee_time, flight_loaded.apogee_time) - assert np.isclose(flight_to_save.t_initial, flight_loaded.t_initial, rtol=1e-3) - assert np.isclose( - flight_to_save.out_of_rail_time, flight_loaded.out_of_rail_time, rtol=1e-3 - ) - assert np.isclose(flight_to_save.apogee_time, flight_loaded.apogee_time, rtol=1e-3) - assert np.isclose(flight_to_save.t_final, flight_loaded.t_final, rtol=1e-2) + # Higher tolerance due to random parachute trigger + assert np.isclose(flight_to_save.t_final, flight_loaded.t_final, rtol=1e-3) os.remove("flight.json")