From 5927b63d07167305fbf9ce27d2097b04b20b5595 Mon Sep 17 00:00:00 2001 From: GabrielBarberini Date: Wed, 9 Oct 2024 20:13:43 -0300 Subject: [PATCH] updates pydantic and fix optional parameters for the API --- lib/models/aerosurfaces.py | 8 ++-- lib/models/environment.py | 23 ++++++--- lib/models/flight.py | 34 +++++++------- lib/models/motor.py | 93 +++++++++++++++++++------------------ lib/models/rocket.py | 83 ++++++++------------------------- lib/services/environment.py | 3 +- lib/services/motor.py | 4 +- lib/utils.py | 15 ++++++ lib/views/environment.py | 14 ++++-- lib/views/flight.py | 29 ++++++------ lib/views/motor.py | 5 +- lib/views/rocket.py | 5 +- requirements.txt | 3 +- 13 files changed, 158 insertions(+), 161 deletions(-) diff --git a/lib/models/aerosurfaces.py b/lib/models/aerosurfaces.py index 069909e..f356601 100644 --- a/lib/models/aerosurfaces.py +++ b/lib/models/aerosurfaces.py @@ -34,12 +34,12 @@ class Fins(BaseModel): name: str n: int root_chord: float - tip_chord: Optional[float] + tip_chord: Optional[float] = None span: float position: float - cant_angle: float - radius: float - airfoil: Tuple[List[Tuple[float, float]], AngleUnit] + cant_angle: Optional[float] = None + radius: Optional[float] = None + airfoil: Optional[Tuple[List[Tuple[float, float]], AngleUnit]] = None # TODO: implement airbrakes diff --git a/lib/models/environment.py b/lib/models/environment.py index 07c08c6..e007096 100644 --- a/lib/models/environment.py +++ b/lib/models/environment.py @@ -1,16 +1,27 @@ import datetime +from enum import Enum from typing import Optional from pydantic import BaseModel +class AtmosphericModelTypes(str, Enum): + STANDARD_ATMOSPHERE: str = "STANDARD_ATMOSPHERE" + CUSTOM_ATMOSPHERE: str = "CUSTOM_ATMOSPHERE" + WYOMING_SOUNDING: str = "WYOMING_SOUNDING" + NOAARUCSOUNDING: str = "NOAARUCSOUNDING" + FORECAST: str = "FORECAST" + REANALYSIS: str = "REANALYSIS" + ENSEMBLE: str = "ENSEMBLE" + + class Env(BaseModel): - latitude: float = 0 - longitude: float = 0 - elevation: Optional[int] = 1400 + latitude: float + longitude: float + elevation: Optional[int] = None - # Opional parameters - atmospheric_model_type: Optional[str] = "standard_atmosphere" - atmospheric_model_file: Optional[str] = "GFS" + # Optional parameters + atmospheric_model_type: Optional[AtmosphericModelTypes] = None + atmospheric_model_file: Optional[str] = None date: Optional[datetime.datetime] = ( datetime.datetime.today() + datetime.timedelta(days=1) ) diff --git a/lib/models/flight.py b/lib/models/flight.py index 82e01aa..c92be47 100644 --- a/lib/models/flight.py +++ b/lib/models/flight.py @@ -6,26 +6,24 @@ class EquationsOfMotion(str, Enum): - STANDARD = "STANDARD" - SOLID_PROPULSION = "SOLID_PROPULSION" + STANDARD: str = "STANDARD" + SOLID_PROPULSION: str = "SOLID_PROPULSION" class Flight(BaseModel): name: str = "Flight" - environment: Env = Env() - rocket: Rocket = Rocket() - rail_length: float = 5.2 - inclination: Optional[int] = 80.0 - heading: Optional[int] = 90.0 + environment: Env + rocket: Rocket + rail_length: float + inclination: Optional[int] = None + heading: Optional[int] = None # TODO: implement initial_solution - terminate_on_apogee: Optional[bool] = False - max_time: Optional[int] = 600 - max_time_step: Optional[float] = 9999 - min_time_step: Optional[int] = 0 - rtol: Optional[float] = 1e-3 - atol: Optional[float] = 1e-3 - time_overshoot: Optional[bool] = True - verbose: Optional[bool] = False - equations_of_motion: Optional[EquationsOfMotion] = ( - EquationsOfMotion.STANDARD - ) + terminate_on_apogee: Optional[bool] = None + max_time: Optional[int] = None + max_time_step: Optional[float] = None + min_time_step: Optional[int] = None + rtol: Optional[float] = None + atol: Optional[float] = None + time_overshoot: Optional[bool] = None + verbose: Optional[bool] = None + equations_of_motion: Optional[EquationsOfMotion] = None diff --git a/lib/models/motor.py b/lib/models/motor.py index 3d58f29..2b20947 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, Tuple, List +from typing import Optional, Tuple, List, Union from pydantic import BaseModel, PrivateAttr @@ -27,38 +27,41 @@ class TankFluids(BaseModel): density: float +class InterpolationMethods(str, Enum): + LINEAR: str = "LINEAR" + SPLINE: str = "SPLINE" + AKIMA: str = "AKIMA" + + class MotorTank(BaseModel): # Required parameters - geometry: List[Tuple[Tuple[float, float], float]] = [ - ((0.0, 5.0), 1.0), - ((5.0, 10.0), 2.0), - ] - gas: TankFluids = TankFluids(name="GAS", density=100) - liquid: TankFluids = TankFluids(name="LIQUID", density=1000) - flux_time: Tuple[float, float] = (0.0, 3.9) - position: float = 1.0 - discretize: int = 100 + geometry: List[Tuple[Tuple[float, float], float]] + gas: TankFluids + liquid: TankFluids + flux_time: Tuple[float, float] + position: float + discretize: int # Level based tank parameters - liquid_height: Optional[float] = 0.5 + liquid_height: Optional[float] # Mass based tank parameters - liquid_mass: Optional[float] = 5.0 - gas_mass: Optional[float] = 0.1 + liquid_mass: Optional[float] + gas_mass: Optional[float] # Mass flow based tank parameters - gas_mass_flow_rate_in: Optional[float] = 0.0 - gas_mass_flow_rate_out: Optional[float] = 0.1 - liquid_mass_flow_rate_in: Optional[float] = 0.0 - liquid_mass_flow_rate_out: Optional[float] = 1 - initial_liquid_mass: Optional[float] = 5.0 - initial_gas_mass: Optional[float] = 0.4 + gas_mass_flow_rate_in: Optional[float] + gas_mass_flow_rate_out: Optional[float] + liquid_mass_flow_rate_in: Optional[float] + liquid_mass_flow_rate_out: Optional[float] + initial_liquid_mass: Optional[float] + initial_gas_mass: Optional[float] # Ullage based tank parameters - ullage: Optional[float] = 0.1 + ullage: Optional[float] # Optional parameters - name: Optional[str] = "Tank" + name: Optional[str] # Computed parameters tank_kind: TankKinds = TankKinds.MASS_FLOW @@ -66,41 +69,39 @@ class MotorTank(BaseModel): class Motor(BaseModel): # Required parameters - thrust_source: List[List[float]] = [[0.0, 0.0], [1.0, 1.0]] - burn_time: float = 3.9 - nozzle_radius: float = 0.033 - dry_mass: float = 1.815 - dry_inertia: Tuple[float, float, float] = (0.125, 0.125, 0.002) - center_of_dry_mass_position: float = 0.317 + thrust_source: List[List[float]] + burn_time: float + nozzle_radius: float + dry_mass: float + dry_inertia: Tuple[float, float, float] + center_of_dry_mass_position: float # Generic motor parameters - chamber_radius: Optional[float] = 0.033 - chamber_height: Optional[float] = 0.1 - chamber_position: Optional[float] = 0.0 - propellant_initial_mass: Optional[float] = 1.0 - nozzle_position: Optional[float] = 0.0 + chamber_radius: Optional[float] = None + chamber_height: Optional[float] = None + chamber_position: Optional[float] = None + propellant_initial_mass: Optional[float] = None + nozzle_position: Optional[float] = None # Liquid motor parameters - tanks: Optional[List[MotorTank]] = [MotorTank()] + tanks: Optional[List[MotorTank]] = None # Solid motor parameters - grain_number: Optional[int] = 5 - grain_density: Optional[float] = 1815 - grain_outer_radius: Optional[float] = 0.033 - grain_initial_inner_radius: Optional[float] = 0.015 - grain_initial_height: Optional[float] = 0.12 - grains_center_of_mass_position: Optional[float] = -0.85704 - grain_separation: Optional[float] = 0.005 + grain_number: Optional[int] = None + grain_density: Optional[float] = None + grain_outer_radius: Optional[float] = None + grain_initial_inner_radius: Optional[float] = None + grain_initial_height: Optional[float] = None + grains_center_of_mass_position: Optional[float] = None + grain_separation: Optional[float] = None # Hybrid motor parameters - throat_radius: Optional[float] = 0.011 + throat_radius: Optional[float] = None # Optional parameters - interpolation_method: Optional[str] = "linear" - coordinate_system_orientation: Optional[CoordinateSystemOrientation] = ( - CoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER - ) - reshape_thrust_curve: Optional[bool] = False + interpolation_method: Optional[InterpolationMethods] = None + coordinate_system_orientation: Optional[CoordinateSystemOrientation] = None + reshape_thrust_curve: Optional[Union[bool, tuple]] = None # Computed parameters _motor_kind: MotorKinds = PrivateAttr(default=MotorKinds.SOLID) diff --git a/lib/models/rocket.py b/lib/models/rocket.py index 1ebc3a0..1f8dae3 100644 --- a/lib/models/rocket.py +++ b/lib/models/rocket.py @@ -7,7 +7,6 @@ NoseCone, Tail, RailButtons, - FinsKinds, ) @@ -17,71 +16,29 @@ class CoordinateSystemOrientation(str, Enum): class Parachute(BaseModel): - name: str = "Main" - cd_s: float = 10 - sampling_rate: int = 105 - lag: float = 1.5 - trigger: Union[str, float] = "apogee" - noise: Tuple[float, float, float] = (0, 8.3, 0.5) + name: str + cd_s: float + sampling_rate: int + lag: float + trigger: Union[str, float] + noise: Tuple[float, float, float] class Rocket(BaseModel): # Required parameters - motor: Motor = Motor() - radius: float = 0.0632 - mass: float = 16.235 - motor_position: float = -1.255 - center_of_mass_without_motor: int = 0 - inertia: Tuple[float, float, float] = (6.321, 6.321, 0.0346) - power_off_drag: List[Tuple[float, float]] = [ - (0.0, 0.0), - (0.1, 0.1), - (0.2, 0.2), - ] - power_on_drag: List[Tuple[float, float]] = [ - (0.0, 0.0), - (0.1, 0.1), - (0.2, 0.2), - ] + motor: Motor + radius: float + mass: float + motor_position: float + center_of_mass_without_motor: int + inertia: Tuple[float, float, float] + power_off_drag: List[Tuple[float, float]] + power_on_drag: List[Tuple[float, float]] # Optional parameters - parachutes: Optional[List[Parachute]] = [Parachute()] - rail_buttons: Optional[RailButtons] = RailButtons( - name="RailButtons", - upper_button_position=-0.5, - lower_button_position=0.2, - angular_position=45, - ) - nose: Optional[NoseCone] = NoseCone( - name="Nose", - length=0.55829, - kind="vonKarman", - position=1.278, - base_radius=0.0635, - rocket_radius=0.0635, - ) - fins: Optional[List[Fins]] = [ - Fins( - fins_kind=FinsKinds.TRAPEZOIDAL, - name="Fins", - n=4, - root_chord=0.12, - tip_chord=0.04, - span=0.1, - position=-1.04956, - cant_angle=0, - radius=0.0635, - airfoil=([(0.0, 0.0), (0.1, 0.1), (0.2, 0.2)], "RADIANS"), - ) - ] - tail: Optional[Tail] = Tail( - name="Tail", - top_radius=0.0635, - bottom_radius=0.0435, - length=0.06, - position=-1.194656, - radius=0.0635, - ) - coordinate_system_orientation: Optional[CoordinateSystemOrientation] = ( - CoordinateSystemOrientation.TAIL_TO_NOSE - ) + parachutes: Optional[List[Parachute]] = None + rail_buttons: Optional[RailButtons] = None + nose: Optional[NoseCone] = None + fins: Optional[List[Fins]] = None + tail: Optional[Tail] = None + coordinate_system_orientation: Optional[CoordinateSystemOrientation] = None diff --git a/lib/services/environment.py b/lib/services/environment.py index e52af0b..f98ea84 100644 --- a/lib/services/environment.py +++ b/lib/services/environment.py @@ -29,7 +29,8 @@ def from_env_model(cls, env: Env) -> Self: date=env.date, ) rocketpy_env.set_atmospheric_model( - type=env.atmospheric_model_type, file=env.atmospheric_model_file + type=env.atmospheric_model_type.value.lower(), + file=env.atmospheric_model_file, ) return cls(environment=rocketpy_env) diff --git a/lib/services/motor.py b/lib/services/motor.py index c28f486..20cce13 100644 --- a/lib/services/motor.py +++ b/lib/services/motor.py @@ -46,8 +46,8 @@ def from_motor_model(cls, motor: Motor) -> Self: if motor.coordinate_system_orientation else None ), - "interpolation_method": motor.interpolation_method, - "reshape_thrust_curve": motor.reshape_thrust_curve, + "interpolation_method": motor.interpolation_method.value.lower(), + "reshape_thrust_curve": False or motor.reshape_thrust_curve, } match motor.motor_kind: diff --git a/lib/utils.py b/lib/utils.py index 666d1c2..db3077c 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -3,6 +3,8 @@ import io import typing +import numpy as np + from starlette.datastructures import Headers, MutableHeaders from starlette.types import ASGIApp, Message, Receive, Scope, Send @@ -126,3 +128,16 @@ async def send_with_gzip(self, message: Message) -> None: async def unattached_send(message: Message) -> typing.NoReturn: raise RuntimeError("send awaitable not set") # pragma: no cover + + +def to_python_primitive(v): + 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) + + return str(v) diff --git a/lib/views/environment.py b/lib/views/environment.py index f6c59b1..bbde25d 100644 --- a/lib/views/environment.py +++ b/lib/views/environment.py @@ -1,9 +1,10 @@ -from typing import Any, Optional +from typing import Optional, Any +from datetime import datetime from pydantic import BaseModel +from lib.utils import to_python_primitive class EnvSummary(BaseModel): - # TODO: if Any is Callable, jumps pydantic parsing, expects a dill binary object latitude: Optional[float] longitude: Optional[float] elevation: Optional[float] @@ -20,10 +21,10 @@ class EnvSummary(BaseModel): initial_hemisphere: Optional[str] initial_ew: Optional[str] max_expected_height: Optional[int] - date: Optional[Any] + date: Optional[datetime] + local_date: Optional[datetime] + datetime_date: Optional[datetime] ellipsoid: Optional[Any] - local_date: Optional[Any] - datetime_date: Optional[Any] barometric_height: Optional[Any] barometric_height_ISA: Optional[Any] pressure: Optional[Any] @@ -45,6 +46,9 @@ class EnvSummary(BaseModel): geodesic_to_utm: Optional[Any] utm_to_geodesic: Optional[Any] + class Config: + json_encoders = {Any: to_python_primitive} + class EnvCreated(BaseModel): env_id: str diff --git a/lib/views/flight.py b/lib/views/flight.py index 1b3e87d..fd5900a 100644 --- a/lib/views/flight.py +++ b/lib/views/flight.py @@ -3,15 +3,15 @@ from lib.models.flight import Flight from lib.views.rocket import RocketView, RocketSummary from lib.views.environment import EnvSummary +from lib.utils import to_python_primitive class FlightSummary(RocketSummary, EnvSummary): - # TODO: if Any is Callable, jumps pydantic parsing, expects a dill binary object # TODO: implement {flight_id}/summary/motor; {flight_id}/summary/rocket; {flight_id}/summary/environment name: Optional[str] max_time: Optional[int] min_time_step: Optional[int] - # max_time_step: Optional[float] + max_time_step: Optional[Any] equations_of_motion: Optional[str] heading: Optional[int] inclination: Optional[int] @@ -48,7 +48,7 @@ class FlightSummary(RocketSummary, EnvSummary): angle_of_attack: Optional[Any] apogee: Optional[Any] apogee_freestream_speed: Optional[Any] - # apogee_state: Optional[Any] + apogee_state: Optional[Any] apogee_time: Optional[Any] apogee_x: Optional[Any] apogee_y: Optional[Any] @@ -74,7 +74,7 @@ class FlightSummary(RocketSummary, EnvSummary): function_evaluations: Optional[Any] function_evaluations_per_time_step: Optional[Any] horizontal_speed: Optional[Any] - # impact_state: Optional[Any] + impact_state: Optional[Any] impact_velocity: Optional[Any] initial_stability_margin: Optional[Any] kinetic_energy: Optional[Any] @@ -109,9 +109,9 @@ class FlightSummary(RocketSummary, EnvSummary): omega2_frequency_response: Optional[Any] omega3_frequency_response: Optional[Any] out_of_rail_stability_margin: Optional[Any] - # out_of_rail_state: Optional[Any] + out_of_rail_state: Optional[Any] out_of_rail_velocity: Optional[Any] - # parachute_events: Optional[Any] + parachute_events: Optional[Any] path_angle: Optional[Any] phi: Optional[Any] potential_energy: Optional[Any] @@ -123,7 +123,7 @@ class FlightSummary(RocketSummary, EnvSummary): reynolds_number: Optional[Any] rotational_energy: Optional[Any] solution: Optional[Any] - # solution_array: Optional[Any] + solution_array: Optional[Any] speed: Optional[Any] stability_margin: Optional[Any] static_margin: Optional[Any] @@ -132,8 +132,8 @@ class FlightSummary(RocketSummary, EnvSummary): stream_velocity_z: Optional[Any] theta: Optional[Any] thrust_power: Optional[Any] - # time: Optional[Any] - # time_steps: Optional[Any] + time: Optional[Any] + time_steps: Optional[Any] total_energy: Optional[Any] total_pressure: Optional[Any] translational_energy: Optional[Any] @@ -147,12 +147,15 @@ class FlightSummary(RocketSummary, EnvSummary): x_impact: Optional[Any] y: Optional[Any] y_impact: Optional[Any] - # y_sol: Optional[Any] + y_sol: Optional[Any] z: Optional[Any] z_impact: Optional[Any] - # flight_phases: Optional[Any] - # FlightPhases: Optional[Any] - # TimeNodes: Optional[Any] + flight_phases: Optional[Any] + FlightPhases: Optional[Any] + TimeNodes: Optional[Any] + + class Config: + json_encoders = {Any: to_python_primitive} class FlightCreated(BaseModel): diff --git a/lib/views/motor.py b/lib/views/motor.py index d1e6e8a..67a9c2c 100644 --- a/lib/views/motor.py +++ b/lib/views/motor.py @@ -1,10 +1,10 @@ from typing import List, Any, Optional from pydantic import BaseModel from lib.models.motor import Motor, MotorKinds +from lib.utils import to_python_primitive class MotorSummary(BaseModel): - # TODO: if Any is Callable, jumps pydantic parsing, expects a dill binary object average_thrust: Optional[float] burn_duration: Optional[float] burn_out_time: Optional[float] @@ -67,6 +67,9 @@ class MotorSummary(BaseModel): total_mass_flow_rate: Optional[Any] thrust: Optional[Any] + class Config: + json_encoders = {Any: to_python_primitive} + class MotorCreated(BaseModel): motor_id: str diff --git a/lib/views/rocket.py b/lib/views/rocket.py index 3ceac24..5733440 100644 --- a/lib/views/rocket.py +++ b/lib/views/rocket.py @@ -2,10 +2,10 @@ from pydantic import BaseModel from lib.models.rocket import Rocket from lib.views.motor import MotorView, MotorSummary +from lib.utils import to_python_primitive class RocketSummary(MotorSummary): - # TODO: if Any is Callable, jumps pydantic parsing, expects a dill binary object area: Optional[float] center_of_mass_without_motor: Optional[float] motor_center_of_dry_mass_position: Optional[float] @@ -35,6 +35,9 @@ class RocketSummary(MotorSummary): thrust_to_weight: Optional[Any] total_lift_coeff_der: Optional[Any] + class Config: + json_encoders = {Any: to_python_primitive} + class RocketCreated(BaseModel): rocket_id: str diff --git a/requirements.txt b/requirements.txt index 0366b13..8d7b739 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,8 @@ motor dill python-dotenv fastapi -pydantic==1.10.18 +pydantic +numpy==1.26.4 pymongo jsonpickle gunicorn