diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e3670606..e71671961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,8 @@ Attention: The newest changes should be on top --> ### Added -- ENH: Add the Coriolis Force to the Flight class [#799](https://github.com/RocketPy-Team/RocketPy/pull/799) +- ENH: Discretized and No-Pickle Encoding Options [#827] (https://github.com/RocketPy-Team/RocketPy/pull/827) +- ENH: Add the Coriolis Force to the Flight class [#799](https://github.com/RocketPy-Team/RocketPy/pull/799) ### Changed diff --git a/rocketpy/_encoders.py b/rocketpy/_encoders.py index 141ff6baa..b68e36fda 100644 --- a/rocketpy/_encoders.py +++ b/rocketpy/_encoders.py @@ -13,33 +13,46 @@ class RocketPyEncoder(json.JSONEncoder): """Custom JSON encoder for RocketPy objects. It defines how to encode - different types of objects to a JSON supported format.""" + different types of objects to a JSON supported format. + """ def __init__(self, *args, **kwargs): + """Initializes the encoder with parameter options. + + Parameters + ---------- + *args : tuple + Positional arguments to pass to the parent class. + **kwargs : dict + Keyword arguments to configure the encoder. The following + options are available: + - include_outputs: bool, whether to include simulation outputs. + Default is False. + - include_function_data: bool, whether to include Function + data in the encoding. If False, Functions will be encoded by their + ``__repr__``. This is useful for reducing the size of the outputs, + but it prevents full restoration of the object upon decoding. + Default is True. + - discretize: bool, whether to discretize Functions whose source + are callables. If True, the accuracy of the decoding may be reduced. + Default is False. + - allow_pickle: bool, whether to pickle callable objects. If + False, callable sources (such as user-defined functions, parachute + triggers or simulation callable outputs) will have their name + stored instead of the function itself. This is useful for + reducing the size of the outputs, but it prevents full restoration + of the object upon decoding. + Default is True. + """ self.include_outputs = kwargs.pop("include_outputs", False) self.include_function_data = kwargs.pop("include_function_data", True) + self.discretize = kwargs.pop("discretize", False) + self.allow_pickle = kwargs.pop("allow_pickle", True) super().__init__(*args, **kwargs) def default(self, o): - if isinstance( - o, - ( - np.int_, - np.intc, - np.intp, - np.int8, - np.int16, - np.int32, - np.int64, - np.uint8, - np.uint16, - np.uint32, - np.uint64, - ), - ): - return int(o) - elif isinstance(o, (np.float16, np.float32, np.float64)): - return float(o) + if isinstance(o, np.generic): + return o.item() elif isinstance(o, np.ndarray): return o.tolist() elif isinstance(o, datetime): @@ -50,11 +63,19 @@ def default(self, o): if not self.include_function_data: return str(o) else: - encoding = o.to_dict(self.include_outputs) + encoding = o.to_dict( + include_outputs=self.include_outputs, + discretize=self.discretize, + allow_pickle=self.allow_pickle, + ) encoding["signature"] = get_class_signature(o) return encoding elif hasattr(o, "to_dict"): - encoding = o.to_dict(self.include_outputs) + encoding = o.to_dict( + include_outputs=self.include_outputs, + discretize=self.discretize, + allow_pickle=self.allow_pickle, + ) encoding = remove_circular_references(encoding) encoding["signature"] = get_class_signature(o) diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index 18abcf1df..eb2eacd5a 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -2630,7 +2630,21 @@ def decimal_degrees_to_arc_seconds(angle): arc_seconds = (remainder * 60 - arc_minutes) * 60 return degrees, arc_minutes, arc_seconds - def to_dict(self, include_outputs=False): + def to_dict(self, **kwargs): + 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 + density = self.density + if kwargs.get("discretize", False): + wind_velocity_x = wind_velocity_x.set_discrete(0, self.max_expected_height) + wind_velocity_y = wind_velocity_y.set_discrete(0, self.max_expected_height) + wind_heading = wind_heading.set_discrete(0, self.max_expected_height) + wind_direction = wind_direction.set_discrete(0, self.max_expected_height) + wind_speed = wind_speed.set_discrete(0, self.max_expected_height) + density = density.set_discrete(0, self.max_expected_height) + env_dict = { "gravity": self.gravity, "date": self.date, @@ -2643,15 +2657,15 @@ def to_dict(self, include_outputs=False): "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, + "wind_velocity_x": wind_velocity_x, + "wind_velocity_y": wind_velocity_y, + "wind_heading": wind_heading, + "wind_direction": wind_direction, + "wind_speed": wind_speed, } - if include_outputs: - env_dict["density"] = self.density + if kwargs.get("include_outputs", False): + env_dict["density"] = density env_dict["barometric_height"] = self.barometric_height env_dict["speed_of_sound"] = self.speed_of_sound env_dict["dynamic_viscosity"] = self.dynamic_viscosity diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index a034ba6f8..681aa68f5 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -3565,7 +3565,7 @@ def __validate_extrapolation(self, extrapolation): extrapolation = "natural" return extrapolation - def to_dict(self, include_outputs=False): # pylint: disable=unused-argument + def to_dict(self, **kwargs): # pylint: disable=unused-argument """Serializes the Function instance to a dictionary. Returns @@ -3576,7 +3576,10 @@ def to_dict(self, include_outputs=False): # pylint: disable=unused-argument source = self.source if callable(source): - source = to_hex_encode(source) + if kwargs.get("allow_pickle", True): + source = to_hex_encode(source) + else: + source = source.__name__ return { "source": source, diff --git a/rocketpy/mathutils/vector_matrix.py b/rocketpy/mathutils/vector_matrix.py index ceac9a08b..9c8bde144 100644 --- a/rocketpy/mathutils/vector_matrix.py +++ b/rocketpy/mathutils/vector_matrix.py @@ -403,7 +403,7 @@ def zeros(): """Returns the zero vector.""" return Vector([0, 0, 0]) - def to_dict(self, include_outputs=False): # pylint: disable=unused-argument + def to_dict(self, **kwargs): # pylint: disable=unused-argument """Returns the vector as a JSON compatible element.""" return list(self.components) @@ -1007,7 +1007,7 @@ def __repr__(self): + f" [{self.zx}, {self.zy}, {self.zz}])" ) - def to_dict(self, include_outputs=False): # pylint: disable=unused-argument + def to_dict(self, **kwargs): # pylint: disable=unused-argument """Returns the matrix as a JSON compatible element.""" return [list(row) for row in self.components] diff --git a/rocketpy/motors/fluid.py b/rocketpy/motors/fluid.py index e027702d0..2b66076b0 100644 --- a/rocketpy/motors/fluid.py +++ b/rocketpy/motors/fluid.py @@ -61,7 +61,7 @@ def __str__(self): return f"Fluid: {self.name}" - def to_dict(self, include_outputs=False): # pylint: disable=unused-argument + def to_dict(self, **kwargs): # pylint: disable=unused-argument return {"name": self.name, "density": self.density} @classmethod diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index e227e4550..7cb28670c 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -641,8 +641,8 @@ def draw(self, *, filename=None): """ self.plots.draw(filename=filename) - def to_dict(self, include_outputs=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data.update( { "grain_number": self.grain_number, @@ -660,13 +660,18 @@ def to_dict(self, include_outputs=False): } ) - if include_outputs: + if kwargs.get("include_outputs", False): + burn_rate = self.solid.burn_rate + if kwargs.get("discretize", False): + burn_rate = burn_rate.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) 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, + "burn_rate": burn_rate, } ) diff --git a/rocketpy/motors/liquid_motor.py b/rocketpy/motors/liquid_motor.py index acce874af..1cd76a52f 100644 --- a/rocketpy/motors/liquid_motor.py +++ b/rocketpy/motors/liquid_motor.py @@ -497,8 +497,8 @@ def draw(self, *, filename=None): """ self.plots.draw(filename=filename) - def to_dict(self, include_outputs=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data.update( { "positioned_tanks": [ diff --git a/rocketpy/motors/motor.py b/rocketpy/motors/motor.py index 1cb659d4f..7930ed52b 100644 --- a/rocketpy/motors/motor.py +++ b/rocketpy/motors/motor.py @@ -1231,14 +1231,7 @@ 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=False): - 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) - + def to_dict(self, **kwargs): data = { "thrust_source": self.thrust, "dry_I_11": self.dry_I_11, @@ -1258,31 +1251,94 @@ def to_dict(self, include_outputs=False): "reference_pressure": self.reference_pressure, } - if include_outputs: + if kwargs.get("include_outputs", False): + total_mass = self.total_mass + propellant_mass = self.propellant_mass + mass_flow_rate = self.total_mass_flow_rate + center_of_mass = self.center_of_mass + center_of_propellant_mass = self.center_of_propellant_mass + exhaust_velocity = self.exhaust_velocity + 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 + if kwargs.get("discretize", False): + total_mass = total_mass.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + propellant_mass = propellant_mass.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + mass_flow_rate = mass_flow_rate.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + center_of_mass = center_of_mass.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + center_of_propellant_mass = ( + center_of_propellant_mass.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + ) + exhaust_velocity = exhaust_velocity.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + I_11 = I_11.set_discrete_based_on_model(self.thrust, mutate_self=False) + I_22 = I_22.set_discrete_based_on_model(self.thrust, mutate_self=False) + I_33 = I_33.set_discrete_based_on_model(self.thrust, mutate_self=False) + I_12 = I_12.set_discrete_based_on_model(self.thrust, mutate_self=False) + I_13 = I_13.set_discrete_based_on_model(self.thrust, mutate_self=False) + I_23 = I_23.set_discrete_based_on_model(self.thrust, mutate_self=False) + propellant_I_11 = propellant_I_11.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + propellant_I_22 = propellant_I_22.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + propellant_I_33 = propellant_I_33.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + propellant_I_12 = propellant_I_12.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + propellant_I_13 = propellant_I_13.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) + propellant_I_23 = propellant_I_23.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) data.update( { "vacuum_thrust": self.vacuum_thrust, - "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_mass": total_mass, + "propellant_mass": propellant_mass, + "mass_flow_rate": mass_flow_rate, + "center_of_mass": center_of_mass, + "center_of_propellant_mass": center_of_propellant_mass, "total_impulse": self.total_impulse, - "exhaust_velocity": self.exhaust_velocity, + "exhaust_velocity": 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, + "I_11": I_11, + "I_22": I_22, + "I_33": I_33, + "I_12": I_12, + "I_13": I_13, + "I_23": I_23, + "propellant_I_11": propellant_I_11, + "propellant_I_22": propellant_I_22, + "propellant_I_33": propellant_I_33, + "propellant_I_12": propellant_I_12, + "propellant_I_13": propellant_I_13, + "propellant_I_23": propellant_I_23, } ) @@ -1510,7 +1566,7 @@ def center_of_propellant_mass(self): Function Function representing the center of mass of the motor. """ - return self.chamber_position + return Function(self.chamber_position).set_discrete_based_on_model(self.thrust) @funcify_method("Time (s)", "Inertia I_11 (kg m²)") def propellant_I_11(self): @@ -1532,11 +1588,11 @@ def propellant_I_11(self): ---------- https://en.wikipedia.org/wiki/Moment_of_inertia#Inertia_tensor """ - return ( + return Function( self.propellant_mass * (3 * self.chamber_radius**2 + self.chamber_height**2) / 12 - ) + ).set_discrete_based_on_model(self.thrust) @funcify_method("Time (s)", "Inertia I_22 (kg m²)") def propellant_I_22(self): @@ -1580,19 +1636,21 @@ def propellant_I_33(self): ---------- https://en.wikipedia.org/wiki/Moment_of_inertia#Inertia_tensor """ - return self.propellant_mass * self.chamber_radius**2 / 2 + return Function( + self.propellant_mass * self.chamber_radius**2 / 2 + ).set_discrete_based_on_model(self.thrust) @funcify_method("Time (s)", "Inertia I_12 (kg m²)") def propellant_I_12(self): - return Function(0) + return Function(0).set_discrete_based_on_model(self.thrust) @funcify_method("Time (s)", "Inertia I_13 (kg m²)") def propellant_I_13(self): - return Function(0) + return Function(0).set_discrete_based_on_model(self.thrust) @funcify_method("Time (s)", "Inertia I_23 (kg m²)") def propellant_I_23(self): - return Function(0) + return Function(0).set_discrete_based_on_model(self.thrust) @staticmethod def load_from_eng_file( @@ -1862,8 +1920,8 @@ def all_info(self): self.prints.all() self.plots.all() - def to_dict(self, include_outputs=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data.update( { "chamber_radius": self.chamber_radius, diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index a8d823966..8a00eeec9 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -765,8 +765,8 @@ def draw(self, *, filename=None): """ self.plots.draw(filename=filename) - def to_dict(self, include_outputs=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data.update( { "nozzle_radius": self.nozzle_radius, @@ -781,13 +781,18 @@ def to_dict(self, include_outputs=False): } ) - if include_outputs: + if kwargs.get("include_outputs", False): + burn_rate = self.burn_rate + if kwargs.get("discretize", False): + burn_rate = burn_rate.set_discrete_based_on_model( + self.thrust, mutate_self=False + ) data.update( { "grain_inner_radius": self.grain_inner_radius, "grain_height": self.grain_height, "burn_area": self.burn_area, - "burn_rate": self.burn_rate, + "burn_rate": burn_rate, "Kn": self.Kn, } ) diff --git a/rocketpy/motors/tank.py b/rocketpy/motors/tank.py index 18060f1a5..98eafa045 100644 --- a/rocketpy/motors/tank.py +++ b/rocketpy/motors/tank.py @@ -505,7 +505,7 @@ def all_info(self): self.prints.all() self.plots.all() - def to_dict(self, include_outputs=False): + def to_dict(self, **kwargs): data = { "name": self.name, "geometry": self.geometry, @@ -514,7 +514,7 @@ def to_dict(self, include_outputs=False): "gas": self.gas, "discretize": self.discretize, } - if include_outputs: + if kwargs.get("include_outputs", False): data.update( { "fluid_mass": self.fluid_mass, @@ -886,8 +886,8 @@ def discretize_flow(self): *self.flux_time, self.discretize, "linear" ) - def to_dict(self, include_outputs=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data.update( { "initial_liquid_mass": self.initial_liquid_mass, @@ -1113,8 +1113,8 @@ def discretize_ullage(self): discretize parameter.""" self.ullage.set_discrete(*self.flux_time, self.discretize, "linear") - def to_dict(self, include_outputs=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data.update({"ullage": self.ullage}) return data @@ -1341,8 +1341,8 @@ def discretize_liquid_height(self): """ self.liquid_level.set_discrete(*self.flux_time, self.discretize, "linear") - def to_dict(self, include_outputs=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data.update({"liquid_height": self.liquid_level}) return data @@ -1600,8 +1600,8 @@ def discretize_masses(self): 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, include_outputs=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data.update( { "liquid_mass": self.liquid_mass, diff --git a/rocketpy/motors/tank_geometry.py b/rocketpy/motors/tank_geometry.py index 59644a537..485f57b09 100644 --- a/rocketpy/motors/tank_geometry.py +++ b/rocketpy/motors/tank_geometry.py @@ -346,14 +346,17 @@ 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, include_outputs=False): + def to_dict(self, **kwargs): data = { "geometry": { - str(domain): function for domain, function in self._geometry.items() + str(domain): function.set_discrete(*domain, 50, mutate_self=False) + if kwargs.get("discretize", False) + else function + for domain, function in self._geometry.items() } } - if include_outputs: + if kwargs.get("include_outputs", False): data["outputs"] = { "average_radius": self.average_radius, "bottom": self.bottom, @@ -442,15 +445,15 @@ def upper_cap_radius(h): else: raise ValueError("Tank already has caps.") - def to_dict(self, include_outputs=False): + def to_dict(self, **kwargs): data = { "radius": self.__input_radius, "height": self.height, "spherical_caps": self.has_caps, } - if include_outputs: - data.update(super().to_dict(include_outputs)) + if kwargs.get("include_outputs", False): + data.update(super().to_dict(**kwargs)) return data @@ -482,11 +485,11 @@ def __init__(self, radius, geometry_dict=None): self.__input_radius = radius self.add_geometry((-radius, radius), lambda h: (radius**2 - h**2) ** 0.5) - def to_dict(self, include_outputs=False): + def to_dict(self, **kwargs): data = {"radius": self.__input_radius} - if include_outputs: - data.update(super().to_dict(include_outputs)) + if kwargs.get("include_outputs", False): + data.update(super().to_dict(**kwargs)) return data diff --git a/rocketpy/rocket/aero_surface/fins/elliptical_fins.py b/rocketpy/rocket/aero_surface/fins/elliptical_fins.py index 08c51ab64..61d98bc0d 100644 --- a/rocketpy/rocket/aero_surface/fins/elliptical_fins.py +++ b/rocketpy/rocket/aero_surface/fins/elliptical_fins.py @@ -317,9 +317,9 @@ def all_info(self): self.prints.all() self.plots.all() - def to_dict(self, include_outputs=False): - data = super().to_dict(include_outputs) - if include_outputs: + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) + if kwargs.get("include_outputs", False): data.update( { "Af": self.Af, diff --git a/rocketpy/rocket/aero_surface/fins/fins.py b/rocketpy/rocket/aero_surface/fins/fins.py index 26a4a7111..1b0d36114 100644 --- a/rocketpy/rocket/aero_surface/fins/fins.py +++ b/rocketpy/rocket/aero_surface/fins/fins.py @@ -426,7 +426,7 @@ def compute_forces_and_moments( M3 = M3_forcing - M3_damping return R1, R2, R3, M1, M2, M3 - def to_dict(self, include_outputs=False): + def to_dict(self, **kwargs): data = { "n": self.n, "root_chord": self.root_chord, @@ -437,11 +437,17 @@ def to_dict(self, include_outputs=False): "name": self.name, } - if include_outputs: + if kwargs.get("include_outputs", False): + cl = self.cl + if kwargs.get("discretize", False): + cl = cl.set_discrete( + (-np.pi / 6, 0), (np.pi / 6, 2), (10, 10), mutate_self=False + ) + data.update( { "cp": self.cp, - "cl": self.cl, + "cl": cl, "roll_parameters": self.roll_parameters, "d": self.d, "ref_area": self.ref_area, diff --git a/rocketpy/rocket/aero_surface/fins/free_form_fins.py b/rocketpy/rocket/aero_surface/fins/free_form_fins.py index 7cae4e556..72758171e 100644 --- a/rocketpy/rocket/aero_surface/fins/free_form_fins.py +++ b/rocketpy/rocket/aero_surface/fins/free_form_fins.py @@ -359,11 +359,11 @@ 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=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data["shape_points"] = self.shape_points - if include_outputs: + if kwargs.get("include_outputs", False): data.update( { "Af": self.Af, diff --git a/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py b/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py index 61e2b78fc..58b68799f 100644 --- a/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py +++ b/rocketpy/rocket/aero_surface/fins/trapezoidal_fins.py @@ -348,11 +348,11 @@ def all_info(self): self.prints.all() self.plots.all() - def to_dict(self, include_outputs=False): - data = super().to_dict(include_outputs) + def to_dict(self, **kwargs): + data = super().to_dict(**kwargs) data["tip_chord"] = self.tip_chord - if include_outputs: + if kwargs.get("include_outputs", False): data.update( { "sweep_length": self.sweep_length, diff --git a/rocketpy/rocket/aero_surface/nose_cone.py b/rocketpy/rocket/aero_surface/nose_cone.py index 954ce1ef8..4e6ea8e95 100644 --- a/rocketpy/rocket/aero_surface/nose_cone.py +++ b/rocketpy/rocket/aero_surface/nose_cone.py @@ -523,7 +523,7 @@ def all_info(self): self.prints.all() self.plots.all() - def to_dict(self, include_outputs=False): + def to_dict(self, **kwargs): data = { "_length": self._length, "_kind": self._kind, @@ -533,10 +533,17 @@ def to_dict(self, include_outputs=False): "_power": self._power, "name": self.name, } - if include_outputs: + if kwargs.get("include_outputs", False): + clalpha = self.clalpha + cl = self.cl + if kwargs.get("discretize", False): + clalpha = clalpha.set_discrete(0, 4, 50) + cl = cl.set_discrete( + (-np.pi / 6, 0), (np.pi / 6, 2), (10, 10), mutate_self=False + ) data["cp"] = self.cp - data["clalpha"] = self.clalpha - data["cl"] = self.cl + data["clalpha"] = clalpha + data["cl"] = cl return data diff --git a/rocketpy/rocket/aero_surface/rail_buttons.py b/rocketpy/rocket/aero_surface/rail_buttons.py index 879f013d7..c27e2949a 100644 --- a/rocketpy/rocket/aero_surface/rail_buttons.py +++ b/rocketpy/rocket/aero_surface/rail_buttons.py @@ -100,7 +100,7 @@ def evaluate_geometrical_parameters(self): None """ - def to_dict(self, include_outputs=False): # pylint: disable=unused-argument + def to_dict(self, **kwargs): # pylint: disable=unused-argument return { "buttons_distance": self.buttons_distance, "angular_position": self.angular_position, diff --git a/rocketpy/rocket/aero_surface/tail.py b/rocketpy/rocket/aero_surface/tail.py index d32836e18..b5e4b878a 100644 --- a/rocketpy/rocket/aero_surface/tail.py +++ b/rocketpy/rocket/aero_surface/tail.py @@ -205,7 +205,7 @@ def all_info(self): self.prints.all() self.plots.all() - def to_dict(self, include_outputs=False): + def to_dict(self, **kwargs): data = { "top_radius": self._top_radius, "bottom_radius": self._bottom_radius, @@ -214,11 +214,20 @@ def to_dict(self, include_outputs=False): "name": self.name, } - if include_outputs: + if kwargs.get("include_outputs", False): + clalpha = self.clalpha + cl = self.cl + if kwargs.get("discretize", False): + clalpha = clalpha.set_discrete(0, 4, 50) + cl = cl.set_discrete( + (-np.pi / 6, 0), (np.pi / 6, 2), (10, 10), mutate_self=False + ) + data.update( { "cp": self.cp, - "cl": self.clalpha, + "clalpha": clalpha, + "cl": cl, "slant_length": self.slant_length, "surface_area": self.surface_area, } diff --git a/rocketpy/rocket/components.py b/rocketpy/rocket/components.py index 66448eb69..9ce8595b3 100644 --- a/rocketpy/rocket/components.py +++ b/rocketpy/rocket/components.py @@ -200,7 +200,7 @@ def sort_by_position(self, reverse=False): """ self._components.sort(key=lambda x: x.position.z, reverse=reverse) - def to_dict(self, include_outputs=False): # pylint: disable=unused-argument + def to_dict(self, **kwargs): # pylint: disable=unused-argument return { "components": [ {"component": c.component, "position": c.position} diff --git a/rocketpy/rocket/parachute.py b/rocketpy/rocket/parachute.py index a18cb4793..3f00f9972 100644 --- a/rocketpy/rocket/parachute.py +++ b/rocketpy/rocket/parachute.py @@ -251,11 +251,15 @@ def all_info(self): self.info() # self.plots.all() # Parachutes still doesn't have plots - def to_dict(self, include_outputs=False): + def to_dict(self, **kwargs): + allow_pickle = kwargs.get("allow_pickle", True) trigger = self.trigger if callable(self.trigger) and not isinstance(self.trigger, Function): - trigger = to_hex_encode(trigger) + if allow_pickle: + trigger = to_hex_encode(trigger) + else: + trigger = trigger.__name__ data = { "name": self.name, @@ -266,9 +270,13 @@ def to_dict(self, include_outputs=False): "noise": self.noise, } - if include_outputs: + if kwargs.get("include_outputs", False): data["noise_signal"] = self.noise_signal - data["noise_function"] = to_hex_encode(self.noise_function) + data["noise_function"] = ( + to_hex_encode(self.noise_function) + if allow_pickle + else self.noise_function.__name__ + ) data["noisy_pressure_signal"] = self.noisy_pressure_signal data["clean_pressure_signal"] = self.clean_pressure_signal diff --git a/rocketpy/rocket/rocket.py b/rocketpy/rocket/rocket.py index 31ef9dd3f..1cab142ee 100644 --- a/rocketpy/rocket/rocket.py +++ b/rocketpy/rocket/rocket.py @@ -1891,7 +1891,16 @@ def all_info(self): self.info() self.plots.all() - def to_dict(self, include_outputs=False): + # pylint: disable=too-many-statements + def to_dict(self, **kwargs): + discretize = kwargs.get("discretize", False) + + power_off_drag = self.power_off_drag + power_on_drag = self.power_on_drag + if discretize: + power_off_drag = power_off_drag.set_discrete(0, 4, 50, mutate_self=False) + power_on_drag = power_on_drag.set_discrete(0, 4, 50, mutate_self=False) + rocket_dict = { "radius": self.radius, "mass": self.mass, @@ -1901,8 +1910,8 @@ def to_dict(self, include_outputs=False): "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, + "power_off_drag": power_off_drag, + "power_on_drag": power_on_drag, "center_of_mass_without_motor": self.center_of_mass_without_motor, "coordinate_system_orientation": self.coordinate_system_orientation, "motor": self.motor, @@ -1915,7 +1924,51 @@ def to_dict(self, include_outputs=False): "sensors": self.sensors, } - if include_outputs: + if kwargs.get("include_outputs", False): + thrust_to_weight = self.thrust_to_weight + cp_position = self.cp_position + stability_margin = self.stability_margin + center_of_mass = self.center_of_mass + motor_center_of_mass_position = self.motor_center_of_mass_position + reduced_mass = self.reduced_mass + total_mass = self.total_mass + total_mass_flow_rate = self.total_mass_flow_rate + center_of_propellant_position = self.center_of_propellant_position + + if discretize: + thrust_to_weight = thrust_to_weight.set_discrete_based_on_model( + self.motor.thrust, mutate_self=False + ) + cp_position = cp_position.set_discrete(0, 4, 25, mutate_self=False) + stability_margin = stability_margin.set_discrete( + (0, self.motor.burn_time[0]), + (2, self.motor.burn_time[1]), + (10, 10), + mutate_self=False, + ) + center_of_mass = center_of_mass.set_discrete_based_on_model( + self.motor.thrust, mutate_self=False + ) + motor_center_of_mass_position = ( + motor_center_of_mass_position.set_discrete_based_on_model( + self.motor.thrust, mutate_self=False + ) + ) + reduced_mass = reduced_mass.set_discrete_based_on_model( + self.motor.thrust, mutate_self=False + ) + total_mass = total_mass.set_discrete_based_on_model( + self.motor.thrust, mutate_self=False + ) + total_mass_flow_rate = total_mass_flow_rate.set_discrete_based_on_model( + self.motor.thrust, mutate_self=False + ) + center_of_propellant_position = ( + center_of_propellant_position.set_discrete_based_on_model( + self.motor.thrust, mutate_self=False + ) + ) + rocket_dict["area"] = self.area rocket_dict["center_of_dry_mass_position"] = ( self.center_of_dry_mass_position @@ -1923,30 +1976,26 @@ def to_dict(self, include_outputs=False): 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_mass_position"] = 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["center_of_mass"] = center_of_mass + rocket_dict["reduced_mass"] = reduced_mass + rocket_dict["total_mass"] = total_mass + rocket_dict["total_mass_flow_rate"] = total_mass_flow_rate + rocket_dict["thrust_to_weight"] = 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["cp_position"] = cp_position + rocket_dict["stability_margin"] = 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 - ) + rocket_dict["center_of_propellant_position"] = center_of_propellant_position return rocket_dict diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index e1db71ba7..5283fd12a 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -3505,7 +3505,7 @@ def time_iterator(self, node_list): yield i, node_list[i] i += 1 - def to_dict(self, include_outputs=False): + def to_dict(self, **kwargs): data = { "rocket": self.rocket, "env": self.env, @@ -3550,7 +3550,7 @@ def to_dict(self, include_outputs=False): "M3": self.M3, } - if include_outputs: + if kwargs.get("include_outputs", False): data.update( { "time": self.time, diff --git a/tests/integration/test_encoding.py b/tests/integration/test_encoding.py index 3ef71252c..c2c0474cb 100644 --- a/tests/integration/test_encoding.py +++ b/tests/integration/test_encoding.py @@ -6,6 +6,8 @@ from rocketpy._encoders import RocketPyDecoder, RocketPyEncoder +from rocketpy.tools import from_hex_decode + @pytest.mark.parametrize( ["flight_name", "include_outputs"], @@ -228,3 +230,55 @@ def test_rocket_encoder(rocket_name, request): rocket_to_encode.static_margin(sample_times), rocket_loaded.static_margin(sample_times), ) + + +@pytest.mark.parametrize("rocket_name", ["calisto_robust"]) +def test_encoder_discretize(rocket_name, request): + """Test encoding the total mass of ``rocketpy.Rocket`` with + discretized encoding. + + 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, discretize=True, include_outputs=True + ) + + mass_loaded = json.loads( + json.dumps(json.loads(json_encoded)["total_mass"]), cls=RocketPyDecoder + ) + + sample_times = np.linspace(*rocket_to_encode.motor.burn_time, 100) + + np.testing.assert_allclose( + mass_loaded(sample_times), + rocket_to_encode.total_mass(sample_times), + rtol=1e-3, + atol=1e-1, + ) + assert isinstance(mass_loaded.source, np.ndarray) + + +@pytest.mark.parametrize("parachute_name", ["calisto_main_chute"]) +def test_encoder_no_pickle(parachute_name, request): + """Test encoding of a ``rocketpy.Parachute`` disallowing + pickle usage. + """ + parachute_to_encode = request.getfixturevalue(parachute_name) + + json_encoded = json.dumps( + parachute_to_encode, + cls=RocketPyEncoder, + allow_pickle=False, + ) + + trigger_loaded = json.loads(json_encoded)["trigger"] + + with pytest.raises(ValueError, match=r"non-hexadecimal number found"): + from_hex_decode(trigger_loaded)