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/services/environment.py b/src/services/environment.py index d0cfa95..991c865 100644 --- a/src/services/environment.py +++ b/src/services/environment.py @@ -3,9 +3,9 @@ import dill from rocketpy.environment.environment import Environment as RocketPyEnvironment -from rocketpy.utilities import get_instance_attributes from src.models.environment import EnvironmentModel from src.views.environment import EnvironmentSimulation +from src.utils import collect_attributes class EnvironmentService: @@ -54,8 +54,11 @@ def get_environment_simulation(self) -> EnvironmentSimulation: EnvironmentSimulation """ - attributes = get_instance_attributes(self.environment) - env_simulation = EnvironmentSimulation(**attributes) + encoded_attributes = collect_attributes( + self.environment, + [EnvironmentSimulation], + ) + 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 fdce768..1a8f559 100644 --- a/src/services/flight.py +++ b/src/services/flight.py @@ -3,13 +3,15 @@ import dill from rocketpy.simulation.flight import Flight as RocketPyFlight -from rocketpy.utilities import get_instance_attributes from src.services.environment import EnvironmentService from src.services.rocket import RocketService from src.models.flight import FlightModel from src.views.flight import FlightSimulation - +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: _flight: RocketPyFlight @@ -55,8 +57,11 @@ def get_flight_simulation(self) -> FlightSimulation: Returns: FlightSimulation """ - attributes = get_instance_attributes(self.flight) - flight_simulation = FlightSimulation(**attributes) + encoded_attributes = collect_attributes( + self.flight, + [FlightSimulation, RocketSimulation, MotorSimulation, EnvironmentSimulation] + ) + 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 5b2acde..cc3ce69 100644 --- a/src/services/motor.py +++ b/src/services/motor.py @@ -6,7 +6,6 @@ from rocketpy.motors.solid_motor import SolidMotor from rocketpy.motors.liquid_motor import LiquidMotor from rocketpy.motors.hybrid_motor import HybridMotor -from rocketpy.utilities import get_instance_attributes from rocketpy import ( LevelBasedTank, MassBasedTank, @@ -18,6 +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 collect_attributes class MotorService: @@ -140,8 +140,11 @@ def get_motor_simulation(self) -> MotorSimulation: Returns: MotorSimulation """ - attributes = get_instance_attributes(self.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 65bbfe3..1b3f645 100644 --- a/src/services/rocket.py +++ b/src/services/rocket.py @@ -11,14 +11,14 @@ Fins as RocketPyFins, Tail as RocketPyTail, ) -from rocketpy.utilities import get_instance_attributes from src import logger from src.models.rocket import RocketModel, Parachute from src.models.sub.aerosurfaces import NoseCone, Tail, Fins from src.services.motor import MotorService from src.views.rocket import RocketSimulation - +from src.views.motor import MotorSimulation +from src.utils import collect_attributes class RocketService: _rocket: RocketPyRocket @@ -107,8 +107,11 @@ def get_rocket_simulation(self) -> RocketSimulation: Returns: RocketSimulation """ - attributes = get_instance_attributes(self.rocket) - rocket_simulation = RocketSimulation(**attributes) + encoded_attributes = collect_attributes( + self.rocket, + [RocketSimulation, MotorSimulation] + ) + 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 d31d747..ff26be4 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,49 +1,147 @@ -# fork of https://github.com/encode/starlette/blob/master/starlette/middleware/gzip.py import gzip import io +import logging +import json -from typing import Annotated, NoReturn, Any -import numpy as np +from typing import NoReturn -from pydantic import PlainSerializer +from views.environment import EnvironmentSimulation +from views.flight import FlightSimulation +from views.motor import MotorSimulation +from views.rocket import RocketSimulation + +from rocketpy._encoders import RocketPyEncoder from starlette.datastructures import Headers, MutableHeaders from starlette.types import ASGIApp, Message, Receive, Scope, Send +logger = logging.getLogger(__name__) + -def to_python_primitive(v: Any) -> Any: +def rocketpy_encoder(obj): """ - Convert complex types to Python primitives. + Encode a RocketPy object using official RocketPy encoders. + + This function uses RocketPy's official RocketPyEncoder for complete + object serialization. Args: - v: Any value, particularly those with a 'source' attribute - containing numpy arrays or generic types. + obj: RocketPy object (Environment, Motor, Rocket, Flight) Returns: - The primitive representation of the input value. + Dictionary of encoded attributes """ - if hasattr(v, "source"): - if isinstance(v.source, np.ndarray): - return v.source.tolist() - - if isinstance(v.source, (np.generic,)): - return v.source.item() - return str(v.source) + json_str = json.dumps( + obj, + cls=RocketPyEncoder, + include_outputs=True, + include_function_data=True, + discretize=True, + allow_pickle=False, + ) + return json.loads(json_str) - if isinstance(v, (np.generic,)): - return v.item() - if isinstance(v, (np.ndarray,)): - return v.tolist() +def collect_attributes(obj, attribute_classes=None): + """ + Collect attributes from various simulation classes and populate them from the flight object. + + Args: + obj: RocketPy Flight object + attribute_classes: List of attribute classes to collect from - return str(v) + Returns: + Dictionary with all collected attributes + """ + if attribute_classes is None: + attribute_classes = [] + attributes = rocketpy_encoder(obj) -AnyToPrimitive = Annotated[ - Any, - PlainSerializer(to_python_primitive), -] + 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: + for key in flight_attributes_list: + if key not in attributes: + try: + value = getattr(obj, key) + attributes[key] = value + except AttributeError: + pass + 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) + if "rocket" not in attributes: + attributes["rocket"] = {} + attributes["rocket"][key] = value + except AttributeError: + pass + 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) + if "rocket" not in attributes: + attributes["rocket"] = {} + if "motor" not in attributes["rocket"]: + attributes["rocket"]["motor"] = {} + attributes["rocket"]["motor"][key] = value + except AttributeError: + pass + 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) + if "env" not in attributes: + attributes["env"] = {} + attributes["env"][key] = value + except AttributeError: + pass + except Exception: + pass + except Exception: + pass + else: + continue + return rocketpy_encoder(attributes) class RocketPyGZipMiddleware: def __init__( @@ -70,6 +168,7 @@ async def __call__( class GZipResponder: + # fork of https://github.com/encode/starlette/blob/master/starlette/middleware/gzip.py def __init__( self, app: ASGIApp, minimum_size: int, compresslevel: int = 9 ) -> None: diff --git a/src/views/environment.py b/src/views/environment.py index 53322b3..4dca240 100644 --- a/src/views/environment.py +++ b/src/views/environment.py @@ -1,12 +1,24 @@ -from typing import Optional +from typing import Optional, Any from datetime import datetime, timedelta +from pydantic import ConfigDict from src.views.interface import ApiBaseView from src.models.environment import EnvironmentModel -from src.utils import AnyToPrimitive class EnvironmentSimulation(ApiBaseView): + """ + Environment simulation view that handles dynamically encoded RocketPy Environment attributes. + + Uses the new rocketpy_encoder which may return different attributes based on the + actual RocketPy Environment object. The model allows extra fields to accommodate + any new attributes that might be encoded. + """ + + model_config = ConfigDict(extra='ignore', arbitrary_types_allowed=True) + message: str = "Environment successfully simulated" + + # Core Environment attributes (always present) latitude: Optional[float] = None longitude: Optional[float] = None elevation: Optional[float] = 1 @@ -23,30 +35,32 @@ class EnvironmentSimulation(ApiBaseView): initial_hemisphere: Optional[str] = None initial_ew: Optional[str] = None max_expected_height: Optional[float] = None - date: Optional[datetime] = datetime.today() + timedelta(days=1) - local_date: Optional[datetime] = datetime.today() + timedelta(days=1) - datetime_date: Optional[datetime] = datetime.today() + timedelta(days=1) - ellipsoid: Optional[AnyToPrimitive] = None - barometric_height: Optional[AnyToPrimitive] = None - barometric_height_ISA: Optional[AnyToPrimitive] = None - pressure: Optional[AnyToPrimitive] = None - pressure_ISA: Optional[AnyToPrimitive] = None - temperature: Optional[AnyToPrimitive] = None - temperature_ISA: Optional[AnyToPrimitive] = None - density: Optional[AnyToPrimitive] = None - speed_of_sound: Optional[AnyToPrimitive] = None - dynamic_viscosity: Optional[AnyToPrimitive] = None - gravity: Optional[AnyToPrimitive] = None - somigliana_gravity: Optional[AnyToPrimitive] = None - wind_speed: Optional[AnyToPrimitive] = None - wind_direction: Optional[AnyToPrimitive] = None - wind_heading: Optional[AnyToPrimitive] = None - wind_velocity_x: Optional[AnyToPrimitive] = None - wind_velocity_y: Optional[AnyToPrimitive] = None - calculate_earth_radius: Optional[AnyToPrimitive] = None - decimal_degrees_to_arc_seconds: Optional[AnyToPrimitive] = None - geodesic_to_utm: Optional[AnyToPrimitive] = None - utm_to_geodesic: Optional[AnyToPrimitive] = None + date: Optional[Any] = datetime.today() + timedelta(days=1) + local_date: Optional[Any] = datetime.today() + timedelta(days=1) + datetime_date: Optional[Any] = datetime.today() + timedelta(days=1) + + # Function attributes (discretized by rocketpy_encoder, serialized by RocketPyEncoder) + ellipsoid: Optional[Any] = None + barometric_height: Optional[Any] = None + barometric_height_ISA: Optional[Any] = None + pressure: Optional[Any] = None + pressure_ISA: Optional[Any] = None + temperature: Optional[Any] = None + temperature_ISA: Optional[Any] = None + density: Optional[Any] = None + speed_of_sound: Optional[Any] = None + dynamic_viscosity: Optional[Any] = None + gravity: Optional[Any] = None + somigliana_gravity: Optional[Any] = None + wind_speed: Optional[Any] = None + wind_direction: Optional[Any] = None + wind_heading: Optional[Any] = None + wind_velocity_x: Optional[Any] = None + wind_velocity_y: Optional[Any] = None + calculate_earth_radius: Optional[Any] = None + decimal_degrees_to_arc_seconds: Optional[Any] = None + geodesic_to_utm: Optional[Any] = None + utm_to_geodesic: Optional[Any] = None class EnvironmentView(EnvironmentModel): diff --git a/src/views/flight.py b/src/views/flight.py index 1a82f98..7353d97 100644 --- a/src/views/flight.py +++ b/src/views/flight.py @@ -1,157 +1,139 @@ -from typing import Optional +from typing import Optional, Any +from pydantic import ConfigDict from src.models.flight import FlightModel from src.views.interface import ApiBaseView from src.views.rocket import RocketView, RocketSimulation from src.views.environment import EnvironmentSimulation -from src.utils import AnyToPrimitive -class FlightSimulation(RocketSimulation, EnvironmentSimulation): +class FlightSimulation(ApiBaseView): + """ + 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 + any new attributes that might be encoded. + """ + + model_config = ConfigDict(extra='ignore', arbitrary_types_allowed=True) + message: str = "Flight successfully simulated" - name: Optional[str] = None - max_time: Optional[int] = None - min_time_step: Optional[int] = None - max_time_step: Optional[AnyToPrimitive] = None - equations_of_motion: Optional[str] = None - heading: Optional[int] = None - inclination: Optional[int] = None - initial_solution: Optional[list] = None - effective_1rl: Optional[float] = None - effective_2rl: Optional[float] = None - out_of_rail_time: Optional[float] = None - out_of_rail_time_index: Optional[int] = None - parachute_cd_s: Optional[float] = None + + # Core Flight attributes (always present) rail_length: Optional[float] = None - rtol: Optional[float] = None - t: Optional[float] = None - t_final: Optional[float] = None - t_initial: Optional[int] = None + inclination: Optional[float] = None + heading: Optional[float] = None terminate_on_apogee: Optional[bool] = None - time_overshoot: Optional[bool] = None - latitude: Optional[AnyToPrimitive] = None - longitude: Optional[AnyToPrimitive] = None - M1: Optional[AnyToPrimitive] = None - M2: Optional[AnyToPrimitive] = None - M3: Optional[AnyToPrimitive] = None - R1: Optional[AnyToPrimitive] = None - R2: Optional[AnyToPrimitive] = None - R3: Optional[AnyToPrimitive] = None - acceleration: Optional[AnyToPrimitive] = None - aerodynamic_bending_moment: Optional[AnyToPrimitive] = None - aerodynamic_drag: Optional[AnyToPrimitive] = None - aerodynamic_lift: Optional[AnyToPrimitive] = None - aerodynamic_spin_moment: Optional[AnyToPrimitive] = None - alpha1: Optional[AnyToPrimitive] = None - alpha2: Optional[AnyToPrimitive] = None - alpha3: Optional[AnyToPrimitive] = None - altitude: Optional[AnyToPrimitive] = None - angle_of_attack: Optional[AnyToPrimitive] = None - apogee: Optional[AnyToPrimitive] = None - apogee_freestream_speed: Optional[AnyToPrimitive] = None - apogee_state: Optional[AnyToPrimitive] = None - apogee_time: Optional[AnyToPrimitive] = None - apogee_x: Optional[AnyToPrimitive] = None - apogee_y: Optional[AnyToPrimitive] = None - atol: Optional[AnyToPrimitive] = None - attitude_angle: Optional[AnyToPrimitive] = None - attitude_frequency_response: Optional[AnyToPrimitive] = None - attitude_vector_x: Optional[AnyToPrimitive] = None - attitude_vector_y: Optional[AnyToPrimitive] = None - attitude_vector_z: Optional[AnyToPrimitive] = None - ax: Optional[AnyToPrimitive] = None - ay: Optional[AnyToPrimitive] = None - az: Optional[AnyToPrimitive] = None - bearing: Optional[AnyToPrimitive] = None - drag_power: Optional[AnyToPrimitive] = None - drift: Optional[AnyToPrimitive] = None - dynamic_pressure: Optional[AnyToPrimitive] = None - e0: Optional[AnyToPrimitive] = None - e1: Optional[AnyToPrimitive] = None - e2: Optional[AnyToPrimitive] = None - e3: Optional[AnyToPrimitive] = None - free_stream_speed: Optional[AnyToPrimitive] = None - frontal_surface_wind: Optional[AnyToPrimitive] = None - function_evaluations: Optional[AnyToPrimitive] = None - function_evaluations_per_time_step: Optional[AnyToPrimitive] = None - horizontal_speed: Optional[AnyToPrimitive] = None - impact_state: Optional[AnyToPrimitive] = None - impact_velocity: Optional[AnyToPrimitive] = None - initial_stability_margin: Optional[AnyToPrimitive] = None - kinetic_energy: Optional[AnyToPrimitive] = None - lateral_attitude_angle: Optional[AnyToPrimitive] = None - lateral_surface_wind: Optional[AnyToPrimitive] = None - mach_number: Optional[AnyToPrimitive] = None - max_acceleration: Optional[AnyToPrimitive] = None - max_acceleration_power_off: Optional[AnyToPrimitive] = None - max_acceleration_power_off_time: Optional[AnyToPrimitive] = None - max_acceleration_power_on: Optional[AnyToPrimitive] = None - max_acceleration_power_on_time: Optional[AnyToPrimitive] = None - max_acceleration_time: Optional[AnyToPrimitive] = None - max_dynamic_pressure: Optional[AnyToPrimitive] = None - max_dynamic_pressure_time: Optional[AnyToPrimitive] = None - max_mach_number: Optional[AnyToPrimitive] = None - max_mach_number_time: Optional[AnyToPrimitive] = None - max_rail_button1_normal_force: Optional[AnyToPrimitive] = None - max_rail_button1_shear_force: Optional[AnyToPrimitive] = None - max_rail_button2_normal_force: Optional[AnyToPrimitive] = None - max_rail_button2_shear_force: Optional[AnyToPrimitive] = None - max_reynolds_number: Optional[AnyToPrimitive] = None - max_reynolds_number_time: Optional[AnyToPrimitive] = None - max_speed: Optional[AnyToPrimitive] = None - max_speed_time: Optional[AnyToPrimitive] = None - max_stability_margin: Optional[AnyToPrimitive] = None - max_stability_margin_time: Optional[AnyToPrimitive] = None - max_total_pressure: Optional[AnyToPrimitive] = None - max_total_pressure_time: Optional[AnyToPrimitive] = None - min_stability_margin: Optional[AnyToPrimitive] = None - min_stability_margin_time: Optional[AnyToPrimitive] = None - omega1_frequency_response: Optional[AnyToPrimitive] = None - omega2_frequency_response: Optional[AnyToPrimitive] = None - omega3_frequency_response: Optional[AnyToPrimitive] = None - out_of_rail_stability_margin: Optional[AnyToPrimitive] = None - out_of_rail_state: Optional[AnyToPrimitive] = None - out_of_rail_velocity: Optional[AnyToPrimitive] = None - parachute_events: Optional[AnyToPrimitive] = None - path_angle: Optional[AnyToPrimitive] = None - phi: Optional[AnyToPrimitive] = None - potential_energy: Optional[AnyToPrimitive] = None - psi: Optional[AnyToPrimitive] = None - rail_button1_normal_force: Optional[AnyToPrimitive] = None - rail_button1_shear_force: Optional[AnyToPrimitive] = None - rail_button2_normal_force: Optional[AnyToPrimitive] = None - rail_button2_shear_force: Optional[AnyToPrimitive] = None - reynolds_number: Optional[AnyToPrimitive] = None - rotational_energy: Optional[AnyToPrimitive] = None - solution: Optional[AnyToPrimitive] = None - solution_array: Optional[AnyToPrimitive] = None - speed: Optional[AnyToPrimitive] = None - stability_margin: Optional[AnyToPrimitive] = None - static_margin: Optional[AnyToPrimitive] = None - stream_velocity_x: Optional[AnyToPrimitive] = None - stream_velocity_y: Optional[AnyToPrimitive] = None - stream_velocity_z: Optional[AnyToPrimitive] = None - theta: Optional[AnyToPrimitive] = None - thrust_power: Optional[AnyToPrimitive] = None - time: Optional[AnyToPrimitive] = None - time_steps: Optional[AnyToPrimitive] = None - total_energy: Optional[AnyToPrimitive] = None - total_pressure: Optional[AnyToPrimitive] = None - translational_energy: Optional[AnyToPrimitive] = None - vx: Optional[AnyToPrimitive] = None - vy: Optional[AnyToPrimitive] = None - vz: Optional[AnyToPrimitive] = None - w1: Optional[AnyToPrimitive] = None - w2: Optional[AnyToPrimitive] = None - w3: Optional[AnyToPrimitive] = None - x: Optional[AnyToPrimitive] = None - x_impact: Optional[AnyToPrimitive] = None - y: Optional[AnyToPrimitive] = None - y_impact: Optional[AnyToPrimitive] = None - y_sol: Optional[AnyToPrimitive] = None - z: Optional[AnyToPrimitive] = None - z_impact: Optional[AnyToPrimitive] = None - flight_phases: Optional[AnyToPrimitive] = None + initial_solution: Optional[list] = None + rocket: Optional[RocketSimulation] = None + env: Optional[EnvironmentSimulation] = None + + # Position and trajectory + latitude: Optional[Any] = None + longitude: Optional[Any] = None + altitude: Optional[Any] = None + x: Optional[Any] = None + y: Optional[Any] = None + z: Optional[Any] = None + + # Velocity components + vx: Optional[Any] = None + 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 + impact_velocity: Optional[Any] = None + + # Acceleration and forces + acceleration: Optional[Any] = None + max_acceleration: Optional[Any] = None + 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 + max_mach_number: Optional[Any] = None + max_mach_number_time: Optional[Any] = None + 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 + 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 + attitude_vector_y: Optional[Any] = None + attitude_vector_z: Optional[Any] = None + trajectory: Optional[Any] = None + velocity: Optional[Any] = None + acceleration_power_on: Optional[Any] = None + acceleration_power_off: Optional[Any] = None + stream_velocity: Optional[Any] = None + free_stream_speed: 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 + potential_energy: Optional[Any] = None + total_energy: Optional[Any] = None + thrust_power: Optional[Any] = None + 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/motor.py b/src/views/motor.py index 9f73a17..8523dcf 100644 --- a/src/views/motor.py +++ b/src/views/motor.py @@ -1,73 +1,70 @@ -from typing import List, Optional -from pydantic import BaseModel +from typing import Optional, Any +from pydantic import ConfigDict from src.views.interface import ApiBaseView from src.models.motor import MotorModel -from src.utils import AnyToPrimitive -class MotorSimulation(BaseModel): +class MotorSimulation(ApiBaseView): + """ + Motor simulation view that handles dynamically encoded RocketPy Motor attributes. + + Uses the new rocketpy_encoder which may return different attributes based on the + actual RocketPy Motor object. The model allows extra fields to accommodate + any new attributes that might be encoded. + """ + + model_config = ConfigDict(extra='ignore', arbitrary_types_allowed=True) + message: str = "Motor successfully simulated" - average_thrust: Optional[float] = None - burn_duration: Optional[float] = None - burn_out_time: Optional[float] = None + + # Core Motor attributes (always present) burn_start_time: Optional[float] = None - center_of_dry_mass_position: Optional[float] = None - coordinate_system_orientation: str = 'nozzle_to_combustion_chamber' - dry_I_11: Optional[float] = None - dry_I_12: Optional[float] = None - dry_I_13: Optional[float] = None - dry_I_22: Optional[float] = None - dry_I_23: Optional[float] = None - dry_I_33: Optional[float] = None + burn_out_time: Optional[float] = None dry_mass: Optional[float] = None - grain_burn_out: Optional[float] = None - grain_density: Optional[float] = None - grain_initial_height: Optional[float] = None - grain_initial_inner_radius: Optional[float] = None - grain_initial_mass: Optional[float] = None - grain_initial_volume: Optional[float] = None + dry_inertia: Optional[tuple] = None + center_of_dry_mass_position: Optional[float] = None + grains_center_of_mass_position: Optional[float] = None grain_number: Optional[int] = None + grain_density: Optional[float] = None grain_outer_radius: Optional[float] = None - grain_separation: Optional[float] = None - grains_center_of_mass_position: Optional[float] = None - interpolate: Optional[str] = None - max_thrust: Optional[float] = None - max_thrust_time: Optional[float] = None - nozzle_position: Optional[float] = None + grain_initial_inner_radius: Optional[float] = None + grain_initial_height: Optional[float] = None nozzle_radius: Optional[float] = None - propellant_initial_mass: Optional[float] = None - throat_area: Optional[float] = None throat_radius: Optional[float] = None - thrust_source: Optional[List[List[float]]] = None - total_impulse: Optional[float] = None - Kn: Optional[AnyToPrimitive] = None - I_11: Optional[AnyToPrimitive] = None - I_12: Optional[AnyToPrimitive] = None - I_13: Optional[AnyToPrimitive] = None - I_22: Optional[AnyToPrimitive] = None - I_23: Optional[AnyToPrimitive] = None - I_33: Optional[AnyToPrimitive] = None - burn_area: Optional[AnyToPrimitive] = None - burn_rate: Optional[AnyToPrimitive] = None - burn_time: Optional[AnyToPrimitive] = None - center_of_mass: Optional[AnyToPrimitive] = None - center_of_propellant_mass: Optional[AnyToPrimitive] = None - exhaust_velocity: Optional[AnyToPrimitive] = None - grain_height: Optional[AnyToPrimitive] = None - grain_volume: Optional[AnyToPrimitive] = None - grain_inner_radius: Optional[AnyToPrimitive] = None - mass_flow_rate: Optional[AnyToPrimitive] = None - propellant_I_11: Optional[AnyToPrimitive] = None - propellant_I_12: Optional[AnyToPrimitive] = None - propellant_I_13: Optional[AnyToPrimitive] = None - propellant_I_22: Optional[AnyToPrimitive] = None - propellant_I_23: Optional[AnyToPrimitive] = None - propellant_I_33: Optional[AnyToPrimitive] = None - propellant_mass: Optional[AnyToPrimitive] = None - reshape_thrust_curve: Optional[AnyToPrimitive] = None - total_mass: Optional[AnyToPrimitive] = None - total_mass_flow_rate: Optional[AnyToPrimitive] = None - thrust: Optional[AnyToPrimitive] = None + nozzle_position: Optional[float] = None + coordinate_system_orientation: Optional[str] = None + motor_kind: Optional[str] = None + interpolate: Optional[str] = None + + # Function attributes (discretized by rocketpy_encoder, serialized by RocketPyEncoder) + Kn: Optional[Any] = None + I_11: Optional[Any] = None + I_12: Optional[Any] = None + I_13: Optional[Any] = None + I_22: Optional[Any] = None + I_23: Optional[Any] = None + I_33: Optional[Any] = None + burn_area: Optional[Any] = None + burn_rate: Optional[Any] = None + burn_time: Optional[Any] = None + center_of_mass: Optional[Any] = None + center_of_propellant_mass: Optional[Any] = None + exhaust_velocity: Optional[Any] = None + grain_height: Optional[Any] = None + grain_volume: Optional[Any] = None + grain_inner_radius: Optional[Any] = None + mass_flow_rate: Optional[Any] = None + propellant_I_11: Optional[Any] = None + propellant_I_12: Optional[Any] = None + propellant_I_13: Optional[Any] = None + propellant_I_22: Optional[Any] = None + propellant_I_23: Optional[Any] = None + propellant_I_33: Optional[Any] = None + propellant_mass: Optional[Any] = None + reshape_thrust_curve: Optional[Any] = None + total_mass: Optional[Any] = None + total_mass_flow_rate: Optional[Any] = None + thrust: Optional[Any] = None class MotorView(MotorModel): diff --git a/src/views/rocket.py b/src/views/rocket.py index becee4a..60cff07 100644 --- a/src/views/rocket.py +++ b/src/views/rocket.py @@ -1,41 +1,52 @@ -from typing import Optional +from typing import Optional, Any, List, Tuple +from pydantic import ConfigDict from src.models.rocket import RocketModel from src.views.interface import ApiBaseView from src.views.motor import MotorView, MotorSimulation -from src.utils import AnyToPrimitive -class RocketSimulation(MotorSimulation): +class RocketSimulation(ApiBaseView): + """ + Rocket simulation view that handles dynamically encoded RocketPy Rocket attributes. + + Inherits from MotorSimulation and adds rocket-specific attributes. Uses the new + rocketpy_encoder which may return different attributes based on the actual + RocketPy Rocket object. The model allows extra fields to accommodate any new + attributes that might be encoded. + """ + + model_config = ConfigDict(extra='ignore', arbitrary_types_allowed=True) + message: str = "Rocket successfully simulated" - area: Optional[float] = None - coordinate_system_orientation: str = 'tail_to_nose' + + # Core Rocket attributes (always present) + radius: Optional[float] = None + mass: Optional[float] = None + inertia: Optional[tuple] = None + power_off_drag: Optional[Any] = None + power_on_drag: Optional[Any] = None center_of_mass_without_motor: Optional[float] = None - motor_center_of_dry_mass_position: Optional[float] = None - motor_position: Optional[float] = None - nozzle_position: Optional[float] = None - nozzle_to_cdm: Optional[float] = None - cp_eccentricity_x: Optional[float] = None - cp_eccentricity_y: Optional[float] = None - thrust_eccentricity_x: Optional[float] = None - thrust_eccentricity_y: Optional[float] = None - I_11_without_motor: Optional[AnyToPrimitive] = None - I_12_without_motor: Optional[AnyToPrimitive] = None - I_13_without_motor: Optional[AnyToPrimitive] = None - I_22_without_motor: Optional[AnyToPrimitive] = None - I_23_without_motor: Optional[AnyToPrimitive] = None - I_33_without_motor: Optional[AnyToPrimitive] = None - check_parachute_trigger: Optional[AnyToPrimitive] = None - com_to_cdm_function: Optional[AnyToPrimitive] = None - cp_position: Optional[AnyToPrimitive] = None - motor_center_of_mass_position: Optional[AnyToPrimitive] = None - nozzle_gyration_tensor: Optional[AnyToPrimitive] = None - power_off_drag: Optional[AnyToPrimitive] = None - power_on_drag: Optional[AnyToPrimitive] = None - reduced_mass: Optional[AnyToPrimitive] = None - stability_margin: Optional[AnyToPrimitive] = None - static_margin: Optional[AnyToPrimitive] = None - thrust_to_weight: Optional[AnyToPrimitive] = None - total_lift_coeff_der: Optional[AnyToPrimitive] = None + coordinate_system_orientation: Optional[str] = None + parachutes: Optional[list] = None + motor: Optional[MotorSimulation] = None + + # Function attributes (discretized by rocketpy_encoder, serialized by RocketPyEncoder) + I_11_without_motor: Optional[Any] = None + I_12_without_motor: Optional[Any] = None + I_13_without_motor: Optional[Any] = None + I_22_without_motor: Optional[Any] = None + I_23_without_motor: Optional[Any] = None + I_33_without_motor: Optional[Any] = None + check_parachute_trigger: Optional[Any] = None + com_to_cdm_function: Optional[Any] = None + cp_position: Optional[Any] = None + motor_center_of_mass_position: Optional[Any] = None + nozzle_gyration_tensor: Optional[Any] = None + reduced_mass: Optional[Any] = None + stability_margin: Optional[Any] = None + static_margin: Optional[Any] = None + thrust_to_weight: Optional[Any] = None + total_lift_coeff_der: Optional[Any] = None class RocketView(RocketModel):