diff --git a/.gitignore b/.gitignore index b8c1794..ef1b4cc 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# VSCode config +.vscode/ \ No newline at end of file diff --git a/src/models/rocket.py b/src/models/rocket.py index c2f53a3..02a48f0 100644 --- a/src/models/rocket.py +++ b/src/models/rocket.py @@ -19,7 +19,7 @@ class RocketModel(ApiBaseModel): radius: float mass: float motor_position: float - center_of_mass_without_motor: int + center_of_mass_without_motor: float inertia: Union[ Tuple[float, float, float], Tuple[float, float, float, float, float, float], diff --git a/src/models/sub/aerosurfaces.py b/src/models/sub/aerosurfaces.py index 966770e..339066e 100644 --- a/src/models/sub/aerosurfaces.py +++ b/src/models/sub/aerosurfaces.py @@ -34,7 +34,6 @@ class Fins(BaseModel): root_chord: float span: float position: float - # Optional parameters tip_chord: Optional[float] = None cant_angle: Optional[float] = None @@ -42,6 +41,8 @@ class Fins(BaseModel): airfoil: Optional[ Tuple[List[Tuple[float, float]], Literal['radians', 'degrees']] ] = None + sweep_length: Optional[float] = None + sweep_angle: Optional[float] = None def get_additional_parameters(self): return { diff --git a/src/services/environment.py b/src/services/environment.py index a0c7656..991c865 100644 --- a/src/services/environment.py +++ b/src/services/environment.py @@ -5,7 +5,7 @@ from rocketpy.environment.environment import Environment as RocketPyEnvironment from src.models.environment import EnvironmentModel from src.views.environment import EnvironmentSimulation -from src.utils import rocketpy_encoder, DiscretizeConfig +from src.utils import collect_attributes class EnvironmentService: @@ -54,10 +54,11 @@ def get_environment_simulation(self) -> EnvironmentSimulation: EnvironmentSimulation """ - attributes = rocketpy_encoder( - self.environment, DiscretizeConfig.for_environment() + encoded_attributes = collect_attributes( + self.environment, + [EnvironmentSimulation], ) - env_simulation = EnvironmentSimulation(**attributes) + env_simulation = EnvironmentSimulation(**encoded_attributes) return env_simulation def get_environment_binary(self) -> bytes: diff --git a/src/services/flight.py b/src/services/flight.py index 564a4a9..e8f0fd1 100644 --- a/src/services/flight.py +++ b/src/services/flight.py @@ -8,7 +8,10 @@ from src.services.rocket import RocketService from src.models.flight import FlightModel from src.views.flight import FlightSimulation -from src.utils import rocketpy_encoder, DiscretizeConfig +from src.views.rocket import RocketSimulation +from src.views.motor import MotorSimulation +from src.views.environment import EnvironmentSimulation +from src.utils import collect_attributes class FlightService: @@ -55,10 +58,16 @@ def get_flight_simulation(self) -> FlightSimulation: Returns: FlightSimulation """ - attributes = rocketpy_encoder( - self.flight, DiscretizeConfig.for_flight() + encoded_attributes = collect_attributes( + self.flight, + [ + FlightSimulation, + RocketSimulation, + MotorSimulation, + EnvironmentSimulation, + ], ) - flight_simulation = FlightSimulation(**attributes) + flight_simulation = FlightSimulation(**encoded_attributes) return flight_simulation def get_flight_binary(self) -> bytes: diff --git a/src/services/motor.py b/src/services/motor.py index d96cccc..cc3ce69 100644 --- a/src/services/motor.py +++ b/src/services/motor.py @@ -17,7 +17,7 @@ from src.models.sub.tanks import TankKinds from src.models.motor import MotorKinds, MotorModel from src.views.motor import MotorSimulation -from src.utils import rocketpy_encoder, DiscretizeConfig +from src.utils import collect_attributes class MotorService: @@ -140,8 +140,11 @@ def get_motor_simulation(self) -> MotorSimulation: Returns: MotorSimulation """ - attributes = rocketpy_encoder(self.motor, DiscretizeConfig.for_motor()) - motor_simulation = MotorSimulation(**attributes) + encoded_attributes = collect_attributes( + self.motor, + [MotorSimulation], + ) + motor_simulation = MotorSimulation(**encoded_attributes) return motor_simulation def get_motor_binary(self) -> bytes: diff --git a/src/services/rocket.py b/src/services/rocket.py index 878e9e0..f67fbde 100644 --- a/src/services/rocket.py +++ b/src/services/rocket.py @@ -17,7 +17,8 @@ from src.models.sub.aerosurfaces import NoseCone, Tail, Fins from src.services.motor import MotorService from src.views.rocket import RocketSimulation -from src.utils import rocketpy_encoder, DiscretizeConfig +from src.views.motor import MotorSimulation +from src.utils import collect_attributes class RocketService: @@ -107,10 +108,10 @@ def get_rocket_simulation(self) -> RocketSimulation: Returns: RocketSimulation """ - attributes = rocketpy_encoder( - self.rocket, DiscretizeConfig.for_rocket() + encoded_attributes = collect_attributes( + self.rocket, [RocketSimulation, MotorSimulation] ) - rocket_simulation = RocketSimulation(**attributes) + rocket_simulation = RocketSimulation(**encoded_attributes) return rocket_simulation def get_rocket_binary(self) -> bytes: diff --git a/src/utils.py b/src/utils.py index 7f211b2..d24d724 100644 --- a/src/utils.py +++ b/src/utils.py @@ -2,16 +2,23 @@ import io import logging import json -import copy from datetime import datetime - from typing import NoReturn, Tuple -from rocketpy import Function +import numpy as np +from scipy.interpolate import interp1d + +from rocketpy import Function, Flight from rocketpy._encoders import RocketPyEncoder + from starlette.datastructures import Headers, MutableHeaders from starlette.types import ASGIApp, Message, Receive, Scope, Send +from src.views.environment import EnvironmentSimulation +from src.views.flight import FlightSimulation +from src.views.motor import MotorSimulation +from src.views.rocket import RocketSimulation + logger = logging.getLogger(__name__) @@ -46,78 +53,154 @@ def for_flight(cls) -> 'DiscretizeConfig': return cls(bounds=(0, 30), samples=200) -def rocketpy_encoder(obj, config: DiscretizeConfig = DiscretizeConfig()): - """ - Encode a RocketPy object using official RocketPy encoders. +class InfinityEncoder(RocketPyEncoder): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) - This function creates a copy of the object, discretizes callable Function - attributes on the copy, and then uses RocketPy's official RocketPyEncoder for - complete object serialization. The original object remains unchanged. + def default(self, obj): + if ( + isinstance(obj, Function) + and not callable(obj.source) + and obj.__dom_dim__ == 1 + ): + size = len(obj._domain) + reduction_factor = 1 + if size > 25: + reduction_factor = size // 25 + if reduction_factor > 1: + obj = obj.set_discrete( + obj.x_array[0], + obj.x_array[-1], + size // reduction_factor, + mutate_self=False, + ) + if isinstance(obj, Flight): + obj._Flight__evaluate_post_process + solution = np.array(obj.solution) + size = len(solution) + if size > 25: + reduction_factor = size // 25 + reduced_solution = np.zeros( + (size // reduction_factor, solution.shape[1]) + ) + reduced_scale = np.linspace( + solution[0, 0], solution[-1, 0], size // reduction_factor + ) + for i, col in enumerate(solution.T): + reduced_solution[:, i] = interp1d( + solution[:, 0], col, assume_sorted=True + )(reduced_scale) + obj.solution = reduced_solution.tolist() - Args: - obj: RocketPy object (Environment, Motor, Rocket, Flight) - config: DiscretizeConfig object with discretization parameters (optional) + obj.flight_phases = None + obj.function_evaluations = None - Returns: - Dictionary of encoded attributes - """ + return super().default(obj) - if config is None: - config = DiscretizeConfig() - try: - # Create a copy to avoid mutating the original object - obj_copy = copy.deepcopy(obj) - except Exception: - # Fall back to a shallow copy if deep copy is not supported - obj_copy = copy.copy(obj) - - for attr_name in dir(obj_copy): - if attr_name.startswith('_'): - continue - try: - attr_value = getattr(obj_copy, attr_name) - except Exception: - continue +def rocketpy_encoder(obj): + """ + Encode a RocketPy object using official RocketPy encoders. - if callable(attr_value) and isinstance(attr_value, Function): + Uses InfinityEncoder for serialization and reduction. + """ + json_str = json.dumps( + obj, + cls=InfinityEncoder, + include_outputs=True, + include_function_data=True, + discretize=True, + allow_pickle=False, + ) + encoded_result = json.loads(json_str) + return _fix_datetime_fields(encoded_result) + + +def collect_attributes(obj, attribute_classes=None): + """ + Collect attributes from various simulation classes and populate them from the flight object. + """ + if attribute_classes is None: + attribute_classes = [] + + attributes = rocketpy_encoder(obj) + + for attribute_class in attribute_classes: + if issubclass(attribute_class, FlightSimulation): + flight_attributes_list = [ + attr + for attr in attribute_class.__annotations__.keys() + if attr not in ["message", "rocket", "env"] + ] try: - discretized_func = Function(attr_value.source) - discretized_func.set_discrete( - lower=config.bounds[0], - upper=config.bounds[1], - samples=config.samples, - mutate_self=True, - ) - - setattr(obj_copy, attr_name, discretized_func) - - except Exception as e: - logger.warning(f"Failed to discretize {attr_name}: {e}") + for key in flight_attributes_list: + if key not in attributes: + try: + value = getattr(obj, key) + attributes[key] = value + except Exception: + pass + except Exception: + pass + + elif issubclass(attribute_class, RocketSimulation): + rocket_attributes_list = [ + attr + for attr in attribute_class.__annotations__.keys() + if attr not in ["message", "motor"] + ] + try: + for key in rocket_attributes_list: + if key not in attributes.get("rocket", {}): + try: + value = getattr(obj.rocket, key) + attributes.setdefault("rocket", {})[key] = value + except Exception: + pass + except Exception: + pass + + elif issubclass(attribute_class, MotorSimulation): + motor_attributes_list = [ + attr + for attr in attribute_class.__annotations__.keys() + if attr not in ["message"] + ] + try: + for key in motor_attributes_list: + if key not in attributes.get("rocket", {}).get( + "motor", {} + ): + try: + value = getattr(obj.rocket.motor, key) + attributes.setdefault("rocket", {}).setdefault( + "motor", {} + )[key] = value + except Exception: + pass + except Exception: + pass + + elif issubclass(attribute_class, EnvironmentSimulation): + environment_attributes_list = [ + attr + for attr in attribute_class.__annotations__.keys() + if attr not in ["message"] + ] + try: + for key in environment_attributes_list: + if key not in attributes.get("env", {}): + try: + value = getattr(obj.env, key) + attributes.setdefault("env", {})[key] = value + except Exception: + pass + except Exception: + pass + else: + continue - try: - json_str = json.dumps( - obj_copy, - cls=RocketPyEncoder, - include_outputs=True, - include_function_data=True, - ) - encoded_result = json.loads(json_str) - - # Post-process to fix datetime fields that got converted to lists - return _fix_datetime_fields(encoded_result) - except Exception as e: - logger.warning(f"Failed to encode with RocketPyEncoder: {e}") - attributes = {} - for attr_name in dir(obj_copy): - if not attr_name.startswith('_'): - try: - attr_value = getattr(obj_copy, attr_name) - if not callable(attr_value): - attributes[attr_name] = str(attr_value) - except Exception: - continue - return attributes + return rocketpy_encoder(attributes) def _fix_datetime_fields(data): diff --git a/src/views/flight.py b/src/views/flight.py index 4d8c211..cb3dd93 100644 --- a/src/views/flight.py +++ b/src/views/flight.py @@ -6,32 +6,32 @@ from src.views.environment import EnvironmentSimulation -class FlightSimulation(RocketSimulation, EnvironmentSimulation): +class FlightSimulation(ApiBaseView): """ - Flight simulation view that handles dynamically encoded - RocketPy Flight attributes. + Flight simulation view that handles dynamically encoded RocketPy Flight attributes. - Inherits from both RocketSimulation and EnvironmentSimulation, - and adds flight-specific attributes. Uses the new rocketpy_encoder - which may return different attributes based on the actual - RocketPy Flight object. The model allows extra fields to accommodate + Inherits from both RocketSimulation and EnvironmentSimulation, and adds flight-specific + attributes. Uses the new rocketpy_encoder which may return different attributes based + on the actual RocketPy Flight object. The model allows extra fields to accommodate any new attributes that might be encoded. """ - model_config = ConfigDict( - ser_json_exclude_none=True, extra='allow', arbitrary_types_allowed=True - ) + model_config = ConfigDict(extra='ignore', arbitrary_types_allowed=True) message: str = "Flight successfully simulated" + # Core Flight attributes (always present) + # Core Flight attributes (always present) rail_length: Optional[float] = None inclination: Optional[float] = None heading: Optional[float] = None + inclination: Optional[float] = None + heading: Optional[float] = None terminate_on_apogee: Optional[bool] = None initial_solution: Optional[list] = None rocket: Optional[RocketSimulation] = None - environment: Optional[EnvironmentSimulation] = None + env: Optional[EnvironmentSimulation] = None # Position and trajectory latitude: Optional[Any] = None @@ -46,12 +46,15 @@ class FlightSimulation(RocketSimulation, EnvironmentSimulation): vy: Optional[Any] = None vz: Optional[Any] = None speed: Optional[Any] = None + max_speed: Optional[Any] = None + max_speed_time: Optional[Any] = None # Key flight metrics apogee: Optional[Any] = None apogee_time: Optional[Any] = None apogee_x: Optional[Any] = None apogee_y: Optional[Any] = None + apogee_freestream_speed: Optional[Any] = None x_impact: Optional[Any] = None y_impact: Optional[Any] = None z_impact: Optional[Any] = None @@ -63,6 +66,10 @@ class FlightSimulation(RocketSimulation, EnvironmentSimulation): max_acceleration_time: Optional[Any] = None aerodynamic_drag: Optional[Any] = None aerodynamic_lift: Optional[Any] = None + max_acceleration_power_on: Optional[Any] = None + max_acceleration_power_on_time: Optional[Any] = None + max_acceleration_power_off: Optional[Any] = None + max_acceleration_power_off_time: Optional[Any] = None # Flight dynamics mach_number: Optional[Any] = None @@ -71,14 +78,34 @@ class FlightSimulation(RocketSimulation, EnvironmentSimulation): angle_of_attack: Optional[Any] = None dynamic_pressure: Optional[Any] = None max_dynamic_pressure: Optional[Any] = None + max_dynamic_pressure_time: Optional[Any] = None + reynolds_number: Optional[Any] = None + max_reynolds_number: Optional[Any] = None + max_reynolds_number_time: Optional[Any] = None # Time and simulation data time: Optional[Any] = None solution: Optional[Any] = None - - # Function attributes - # discretized by rocketpy_encoder - # serialized by RocketPyEncoder + t_final: Optional[Any] = None + max_time: Optional[Any] = None + max_time_step: Optional[Any] = None + min_time_step: Optional[Any] = None + rtol: Optional[Any] = None + atol: Optional[Any] = None + time_overshoot: Optional[Any] = None + out_of_rail_time: Optional[Any] = None + out_of_rail_time_index: Optional[Any] = None + out_of_rail_velocity: Optional[Any] = None + + # Stability margins + out_of_rail_stability_margin: Optional[Any] = None + initial_stability_margin: Optional[Any] = None + max_stability_margin: Optional[Any] = None + max_stability_margin_time: Optional[Any] = None + min_stability_margin: Optional[Any] = None + min_stability_margin_time: Optional[Any] = None + + # Function attributes (discretized by rocketpy_encoder) angular_position: Optional[Any] = None attitude_angle: Optional[Any] = None attitude_vector_x: Optional[Any] = None @@ -90,13 +117,15 @@ class FlightSimulation(RocketSimulation, EnvironmentSimulation): acceleration_power_off: Optional[Any] = None stream_velocity: Optional[Any] = None free_stream_speed: Optional[Any] = None - apogee_freestream_speed: Optional[Any] = None - reynolds_number: Optional[Any] = None total_pressure: Optional[Any] = None rail_button_normal_force: Optional[Any] = None max_rail_button_normal_force: Optional[Any] = None rail_button_shear_force: Optional[Any] = None max_rail_button_shear_force: Optional[Any] = None + max_rail_button1_normal_force: Optional[Any] = None + max_rail_button1_shear_force: Optional[Any] = None + max_rail_button2_normal_force: Optional[Any] = None + max_rail_button2_shear_force: Optional[Any] = None rotational_energy: Optional[Any] = None translational_energy: Optional[Any] = None kinetic_energy: Optional[Any] = None @@ -106,6 +135,10 @@ class FlightSimulation(RocketSimulation, EnvironmentSimulation): drag_power: Optional[Any] = None drift: Optional[Any] = None + # Environmental conditions + frontal_surface_wind: Optional[Any] = None + lateral_surface_wind: Optional[Any] = None + class FlightView(FlightModel): flight_id: str diff --git a/src/views/rocket.py b/src/views/rocket.py index 3107d3a..25d08fe 100644 --- a/src/views/rocket.py +++ b/src/views/rocket.py @@ -31,8 +31,8 @@ class RocketSimulation(MotorSimulation): tuple[float, float, float] | tuple[float, float, float, float, float, float] ] = None - power_off_drag: Optional[list[tuple[float, float]]] = None - power_on_drag: Optional[list[tuple[float, float]]] = None + power_off_drag: Optional[Any] = None + power_on_drag: Optional[Any] = None center_of_mass_without_motor: Optional[float] = None coordinate_system_orientation: Optional[str] = None parachutes: Optional[list] = None