diff --git a/pyproject.toml b/pyproject.toml index a88cbc7..51ee41d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ disable = """ no-member, protected-access, import-outside-toplevel, + too-many-statements, """ [tool.ruff] diff --git a/src/models/environment.py b/src/models/environment.py index f1e9b5a..447b2ab 100644 --- a/src/models/environment.py +++ b/src/models/environment.py @@ -1,8 +1,14 @@ -import datetime +from datetime import datetime, timezone, timedelta from typing import Optional, ClassVar, Self, Literal +from pydantic import Field from src.models.interface import ApiBaseModel +def _default_future_datetime() -> datetime: + """Factory function to create timezone-aware datetime one day in the future.""" + return datetime.now(timezone.utc) + timedelta(days=1) + + class EnvironmentModel(ApiBaseModel): NAME: ClassVar = 'environment' METHODS: ClassVar = ('POST', 'GET', 'PUT', 'DELETE') @@ -24,9 +30,7 @@ class EnvironmentModel(ApiBaseModel): 'ensemble', ] = 'standard_atmosphere' atmospheric_model_file: Optional[str] = None - date: Optional[datetime.datetime] = ( - datetime.datetime.today() + datetime.timedelta(days=1) - ) + date: Optional[datetime] = Field(default_factory=_default_future_datetime) @staticmethod def UPDATED(): diff --git a/src/models/interface.py b/src/models/interface.py index bd5b99c..b02f586 100644 --- a/src/models/interface.py +++ b/src/models/interface.py @@ -21,6 +21,7 @@ class ApiBaseModel(BaseModel, ABC): validate_default=True, validate_all_in_root=True, validate_assignment=True, + ser_json_exclude_none=True, ) def set_id(self, value): diff --git a/src/repositories/flight.py b/src/repositories/flight.py index 3b3448b..0df540c 100644 --- a/src/repositories/flight.py +++ b/src/repositories/flight.py @@ -19,7 +19,7 @@ def __init__(self): @repository_exception_handler async def create_flight(self, flight: FlightModel) -> str: - return await self.insert(flight.model_dump()) + return await self.insert(flight.model_dump(exclude_none=True)) @repository_exception_handler async def read_flight_by_id(self, flight_id: str) -> Optional[FlightModel]: @@ -27,7 +27,9 @@ async def read_flight_by_id(self, flight_id: str) -> Optional[FlightModel]: @repository_exception_handler async def update_flight_by_id(self, flight_id: str, flight: FlightModel): - await self.update_by_id(flight.model_dump(), data_id=flight_id) + await self.update_by_id( + flight.model_dump(exclude_none=True), data_id=flight_id + ) @repository_exception_handler async def delete_flight_by_id(self, flight_id: str): diff --git a/src/repositories/motor.py b/src/repositories/motor.py index 026655f..52c016b 100644 --- a/src/repositories/motor.py +++ b/src/repositories/motor.py @@ -19,7 +19,7 @@ def __init__(self): @repository_exception_handler async def create_motor(self, motor: MotorModel) -> str: - return await self.insert(motor.model_dump()) + return await self.insert(motor.model_dump(exclude_none=True)) @repository_exception_handler async def read_motor_by_id(self, motor_id: str) -> Optional[MotorModel]: @@ -27,7 +27,9 @@ async def read_motor_by_id(self, motor_id: str) -> Optional[MotorModel]: @repository_exception_handler async def update_motor_by_id(self, motor_id: str, motor: MotorModel): - await self.update_by_id(motor.model_dump(), data_id=motor_id) + await self.update_by_id( + motor.model_dump(exclude_none=True), data_id=motor_id + ) @repository_exception_handler async def delete_motor_by_id(self, motor_id: str): diff --git a/src/repositories/rocket.py b/src/repositories/rocket.py index f4b6f38..a3849d7 100644 --- a/src/repositories/rocket.py +++ b/src/repositories/rocket.py @@ -19,7 +19,7 @@ def __init__(self): @repository_exception_handler async def create_rocket(self, rocket: RocketModel) -> str: - return await self.insert(rocket.model_dump()) + return await self.insert(rocket.model_dump(exclude_none=True)) @repository_exception_handler async def read_rocket_by_id(self, rocket_id: str) -> Optional[RocketModel]: @@ -27,7 +27,9 @@ async def read_rocket_by_id(self, rocket_id: str) -> Optional[RocketModel]: @repository_exception_handler async def update_rocket_by_id(self, rocket_id: str, rocket: RocketModel): - await self.update_by_id(rocket.model_dump(), data_id=rocket_id) + await self.update_by_id( + rocket.model_dump(exclude_none=True), data_id=rocket_id + ) @repository_exception_handler async def delete_rocket_by_id(self, rocket_id: str): diff --git a/src/services/environment.py b/src/services/environment.py index d0cfa95..a0c7656 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 rocketpy_encoder, DiscretizeConfig class EnvironmentService: @@ -54,7 +54,9 @@ def get_environment_simulation(self) -> EnvironmentSimulation: EnvironmentSimulation """ - attributes = get_instance_attributes(self.environment) + attributes = rocketpy_encoder( + self.environment, DiscretizeConfig.for_environment() + ) env_simulation = EnvironmentSimulation(**attributes) return env_simulation diff --git a/src/services/flight.py b/src/services/flight.py index fdce768..564a4a9 100644 --- a/src/services/flight.py +++ b/src/services/flight.py @@ -3,12 +3,12 @@ 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.utils import rocketpy_encoder, DiscretizeConfig class FlightService: @@ -55,7 +55,9 @@ def get_flight_simulation(self) -> FlightSimulation: Returns: FlightSimulation """ - attributes = get_instance_attributes(self.flight) + attributes = rocketpy_encoder( + self.flight, DiscretizeConfig.for_flight() + ) flight_simulation = FlightSimulation(**attributes) return flight_simulation diff --git a/src/services/motor.py b/src/services/motor.py index 5b2acde..d96cccc 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 rocketpy_encoder, DiscretizeConfig class MotorService: @@ -140,7 +140,7 @@ def get_motor_simulation(self) -> MotorSimulation: Returns: MotorSimulation """ - attributes = get_instance_attributes(self.motor) + attributes = rocketpy_encoder(self.motor, DiscretizeConfig.for_motor()) motor_simulation = MotorSimulation(**attributes) return motor_simulation diff --git a/src/services/rocket.py b/src/services/rocket.py index 65bbfe3..878e9e0 100644 --- a/src/services/rocket.py +++ b/src/services/rocket.py @@ -11,13 +11,13 @@ 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.utils import rocketpy_encoder, DiscretizeConfig class RocketService: @@ -107,7 +107,9 @@ def get_rocket_simulation(self) -> RocketSimulation: Returns: RocketSimulation """ - attributes = get_instance_attributes(self.rocket) + attributes = rocketpy_encoder( + self.rocket, DiscretizeConfig.for_rocket() + ) rocket_simulation = RocketSimulation(**attributes) return rocket_simulation diff --git a/src/utils.py b/src/utils.py index d31d747..7f211b2 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,48 +1,157 @@ -# fork of https://github.com/encode/starlette/blob/master/starlette/middleware/gzip.py import gzip import io +import logging +import json +import copy +from datetime import datetime -from typing import Annotated, NoReturn, Any -import numpy as np +from typing import NoReturn, Tuple -from pydantic import PlainSerializer +from rocketpy import Function +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: + +class DiscretizeConfig: + """ + Configuration class for RocketPy function discretization. + + This class allows easy configuration of discretization parameters + for different types of RocketPy objects and their callable attributes. + """ + + def __init__( + self, bounds: Tuple[float, float] = (0, 10), samples: int = 200 + ): + self.bounds = bounds + self.samples = samples + + @classmethod + def for_environment(cls) -> 'DiscretizeConfig': + return cls(bounds=(0, 50000), samples=100) + + @classmethod + def for_motor(cls) -> 'DiscretizeConfig': + return cls(bounds=(0, 10), samples=150) + + @classmethod + def for_rocket(cls) -> 'DiscretizeConfig': + return cls(bounds=(0, 1), samples=100) + + @classmethod + def for_flight(cls) -> 'DiscretizeConfig': + return cls(bounds=(0, 30), samples=200) + + +def rocketpy_encoder(obj, config: DiscretizeConfig = DiscretizeConfig()): """ - Convert complex types to Python primitives. + Encode a RocketPy object using official RocketPy encoders. + + 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. Args: - v: Any value, particularly those with a 'source' attribute - containing numpy arrays or generic types. + obj: RocketPy object (Environment, Motor, Rocket, Flight) + config: DiscretizeConfig object with discretization parameters (optional) 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() + 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 - return str(v.source) + try: + attr_value = getattr(obj_copy, attr_name) + except Exception: + continue - if isinstance(v, (np.generic,)): - return v.item() + if callable(attr_value) and isinstance(attr_value, Function): + 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) - if isinstance(v, (np.ndarray,)): - return v.tolist() + except Exception as e: + logger.warning(f"Failed to discretize {attr_name}: {e}") + + try: + json_str = json.dumps( + obj_copy, + cls=RocketPyEncoder, + include_outputs=True, + include_function_data=True, + ) + encoded_result = json.loads(json_str) - return str(v) + # 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 -AnyToPrimitive = Annotated[ - Any, - PlainSerializer(to_python_primitive), -] +def _fix_datetime_fields(data): + """ + Fix datetime fields that RocketPyEncoder converted to lists. + """ + if isinstance(data, dict): + fixed = {} + for key, value in data.items(): + if ( + key in ['date', 'local_date', 'datetime_date'] + and isinstance(value, list) + and len(value) >= 3 + ): + # Convert [year, month, day, hour, ...] back to datetime + try: + year, month, day = value[0:3] + hour = value[3] if len(value) > 3 else 0 + minute = value[4] if len(value) > 4 else 0 + second = value[5] if len(value) > 5 else 0 + microsecond = value[6] if len(value) > 6 else 0 + + fixed[key] = datetime( + year, month, day, hour, minute, second, microsecond + ) + except (ValueError, TypeError, IndexError): + # If conversion fails, keep the original value + fixed[key] = value + else: + fixed[key] = _fix_datetime_fields(value) + return fixed + if isinstance(data, (list, tuple)): + return [_fix_datetime_fields(item) for item in data] + return data class RocketPyGZipMiddleware: @@ -70,6 +179,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: @@ -161,6 +271,13 @@ async def send_with_gzip(self, message: Message) -> None: await self.send(message) + else: + # Pass through other message types unmodified. + if not self.started: + self.started = True + await self.send(self.initial_message) + await self.send(message) + async def unattached_send(message: Message) -> NoReturn: raise RuntimeError("send awaitable not set") # pragma: no cover diff --git a/src/views/environment.py b/src/views/environment.py index 53322b3..5147d35 100644 --- a/src/views/environment.py +++ b/src/views/environment.py @@ -1,12 +1,33 @@ -from typing import Optional -from datetime import datetime, timedelta +from typing import Optional, Any +from datetime import datetime, timezone, timedelta +from pydantic import ConfigDict, Field from src.views.interface import ApiBaseView from src.models.environment import EnvironmentModel -from src.utils import AnyToPrimitive + + +def _default_future_datetime() -> datetime: + """Factory function to create timezone-aware datetime one day in the future.""" + return datetime.now(timezone.utc) + timedelta(days=1) 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( + ser_json_exclude_none=True, extra='allow', 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 @@ -22,31 +43,39 @@ class EnvironmentSimulation(ApiBaseView): initial_east: Optional[float] = None 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 + max_expected_height: Optional[int] = None + date: Optional[datetime] = Field(default_factory=_default_future_datetime) + local_date: Optional[datetime] = Field( + default_factory=_default_future_datetime + ) + datetime_date: Optional[datetime] = Field( + default_factory=_default_future_datetime + ) + + # 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..4d8c211 100644 --- a/src/views/flight.py +++ b/src/views/flight.py @@ -1,156 +1,110 @@ -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): + """ + 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( + ser_json_exclude_none=True, extra='allow', 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 + environment: 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 + + # Key flight metrics + apogee: Optional[Any] = None + apogee_time: Optional[Any] = None + apogee_x: Optional[Any] = None + apogee_y: 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 + + # 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 + + # Time and simulation data + time: Optional[Any] = None + solution: Optional[Any] = None + + # Function attributes + # discretized by rocketpy_encoder + # serialized by RocketPyEncoder + 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 + 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 + 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 class FlightView(FlightModel): diff --git a/src/views/interface.py b/src/views/interface.py index ef58c5a..f20c125 100644 --- a/src/views/interface.py +++ b/src/views/interface.py @@ -1,5 +1,6 @@ -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict class ApiBaseView(BaseModel): message: str = 'View not implemented' + model_config = ConfigDict(ser_json_exclude_none=True) diff --git a/src/views/motor.py b/src/views/motor.py index 9f73a17..1ca4aea 100644 --- a/src/views/motor.py +++ b/src/views/motor.py @@ -1,73 +1,76 @@ -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( + ser_json_exclude_none=True, extra='allow', 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..3107d3a 100644 --- a/src/views/rocket.py +++ b/src/views/rocket.py @@ -1,41 +1,62 @@ -from typing import Optional +from typing import Optional, Any +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): + """ + 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( + ser_json_exclude_none=True, extra='allow', 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[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 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):