Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/models/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
9 changes: 6 additions & 3 deletions src/services/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Comment on lines +57 to 62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Passing EnvironmentSimulation to collect_attributes is a no-op for a bare Environment object.

collect_attributes tries obj.env for EnvironmentSimulation; with an Environment instance, that path is skipped. Rely on rocketpy_encoder directly or update the utility to handle both Flight and Environment inputs.

Apply this diff:

-        encoded_attributes = collect_attributes(
-            self.environment,
-            [EnvironmentSimulation],
-        )
+        encoded_attributes = collect_attributes(self.environment)

Optionally, harden collect_attributes to support both patterns:

# utils.py (illustrative)
elif issubclass(attribute_class, EnvironmentSimulation):
    environment_attributes_list = [...]
    try:
        src = getattr(obj, "env", obj)  # works for Flight or Environment
        for key in environment_attributes_list:
            if key not in attributes.get("env", {}):
                try:
                    value = getattr(src, key)
                    if "env" not in attributes and src is not obj:
                        attributes["env"] = {}
                    (attributes["env"] if src is not obj else attributes)[key] = value
                except AttributeError:
                    pass
    except Exception:
        pass
🤖 Prompt for AI Agents
In src/services/environment.py around lines 57 to 62, passing
EnvironmentSimulation as the attribute_class into collect_attributes is a no-op
for a bare Environment instance because collect_attributes looks up obj.env for
EnvironmentSimulation and skips when obj is an Environment; fix by either (A)
bypassing collect_attributes and encode the environment directly with
rocketpy_encoder (i.e., call the encoder on self.environment and construct
EnvironmentSimulation from that result), or (B) update the collect_attributes
utility to handle both Flight and Environment by looking up src = getattr(obj,
"env", obj) before extracting environment-specific keys so attributes are
populated whether the caller is a Flight or an Environment object.


def get_environment_binary(self) -> bytes:
Expand Down
13 changes: 9 additions & 4 deletions src/services/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 6 additions & 3 deletions src/services/motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 7 additions & 4 deletions src/services/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Comment on lines +110 to +114
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Attribute backfill via collect_attributes is ineffective for a Rocket instance.

The helper looks for obj.rocket / obj.rocket.motor; with a Rocket input this branch is skipped. Let rocketpy_encoder drive encoding directly, or adjust the utility to detect object shape.

Apply this diff:

-        encoded_attributes = collect_attributes(
-            self.rocket,
-            [RocketSimulation, MotorSimulation]
-        )
+        encoded_attributes = collect_attributes(self.rocket)

If you prefer keeping class hints, update utils similarly to handle src = getattr(obj, "rocket", obj) and src = getattr(getattr(obj, "rocket", obj), "motor", getattr(obj, "motor", None)).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
encoded_attributes = collect_attributes(
self.rocket,
[RocketSimulation, MotorSimulation]
)
rocket_simulation = RocketSimulation(**encoded_attributes)
encoded_attributes = collect_attributes(self.rocket)
rocket_simulation = RocketSimulation(**encoded_attributes)
🤖 Prompt for AI Agents
In src/services/rocket.py around lines 110 to 114, collect_attributes is
currently ineffective for a Rocket instance because it expects obj.rocket /
obj.rocket.motor and skips when passed a Rocket; change the call to let
rocketpy_encoder handle encoding directly or update the utility to detect shape:
when implementing the fix, either (A) pass the original Rocket to
rocketpy_encoder instead of using collect_attributes so encoding is driven by
the encoder, or (B) modify collect_attributes to use src = getattr(obj,
"rocket", obj) and motor_src = getattr(src, "motor", getattr(obj, "motor",
None)) so it correctly backfills attributes for both wrapper and direct Rocket
instances, preserving existing class hints.

return rocket_simulation

def get_rocket_binary(self) -> bytes:
Expand Down
149 changes: 124 additions & 25 deletions src/utils.py
Original file line number Diff line number Diff line change
@@ -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__(
Expand All @@ -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:
Expand Down
66 changes: 40 additions & 26 deletions src/views/environment.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
from typing import Optional
from typing import Optional, Any
from datetime import datetime, timedelta
from pydantic import ConfigDict
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Datetime defaults are evaluated at import time; switch to default_factory.

Avoid stale defaults in long-lived processes.

Apply this diff:

-from pydantic import ConfigDict
+from pydantic import ConfigDict, Field
@@
-    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)
+    date: Optional[Any] = Field(default_factory=lambda: datetime.today() + timedelta(days=1))
+    local_date: Optional[Any] = Field(default_factory=lambda: datetime.today() + timedelta(days=1))
+    datetime_date: Optional[Any] = Field(default_factory=lambda: datetime.today() + timedelta(days=1))

Also applies to: 38-40

🤖 Prompt for AI Agents
In src/views/environment.py lines 3 and 38-40, datetime defaults are being set
at import time; replace any model/field defaults like default=datetime.now(...)
or default=datetime.utcnow(...) with a default_factory so the timestamp is
evaluated when an instance is created. Import and use pydantic Field (or the
appropriate model field API) with default_factory=datetime.now/utcnow for those
fields, or supply a callable that returns the current datetime, ensuring new
instances get fresh timestamps instead of stale import-time values.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

extra='ignore' contradicts the docstring; use extra='allow'.

With extra='ignore', unknown encoded attributes are dropped instead of preserved.

Apply this diff:

-    model_config = ConfigDict(extra='ignore', arbitrary_types_allowed=True)
+    model_config = ConfigDict(extra='allow', arbitrary_types_allowed=True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
model_config = ConfigDict(extra='ignore', arbitrary_types_allowed=True)
model_config = ConfigDict(extra='allow', arbitrary_types_allowed=True)
🤖 Prompt for AI Agents
In src/views/environment.py around line 17, the ConfigDict is set to
extra='ignore' which contradicts the docstring; change the model_config to use
extra='allow' (i.e., model_config = ConfigDict(extra='allow',
arbitrary_types_allowed=True)) so unknown encoded attributes are preserved
instead of dropped; update the single assignment on that line accordingly.


message: str = "Environment successfully simulated"

# Core Environment attributes (always present)
latitude: Optional[float] = None
longitude: Optional[float] = None
elevation: Optional[float] = 1
Expand All @@ -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):
Expand Down
Loading
Loading