Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# VSCode config
.vscode/
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
3 changes: 2 additions & 1 deletion src/models/sub/aerosurfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ class Fins(BaseModel):
root_chord: float
span: float
position: float

# Optional parameters
tip_chord: Optional[float] = None
cant_angle: Optional[float] = None
rocket_radius: Optional[float] = None
airfoil: Optional[
Tuple[List[Tuple[float, float]], Literal['radians', 'degrees']]
] = None
sweep_length: Optional[float] = None
sweep_angle: Optional[float] = None

def get_additional_parameters(self):
return {
Expand Down
9 changes: 5 additions & 4 deletions src/services/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from rocketpy.environment.environment import Environment as RocketPyEnvironment
from src.models.environment import EnvironmentModel
from src.views.environment import EnvironmentSimulation
from src.utils import rocketpy_encoder, DiscretizeConfig
from src.utils import collect_attributes


class EnvironmentService:
Expand Down Expand Up @@ -54,10 +54,11 @@ def get_environment_simulation(self) -> EnvironmentSimulation:
EnvironmentSimulation
"""

attributes = rocketpy_encoder(
self.environment, DiscretizeConfig.for_environment()
encoded_attributes = collect_attributes(
self.environment,
[EnvironmentSimulation],
)
env_simulation = EnvironmentSimulation(**attributes)
env_simulation = EnvironmentSimulation(**encoded_attributes)
return env_simulation

def get_environment_binary(self) -> bytes:
Expand Down
17 changes: 13 additions & 4 deletions src/services/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from src.services.rocket import RocketService
from src.models.flight import FlightModel
from src.views.flight import FlightSimulation
from src.utils import rocketpy_encoder, DiscretizeConfig
from src.views.rocket import RocketSimulation
from src.views.motor import MotorSimulation
from src.views.environment import EnvironmentSimulation
from src.utils import collect_attributes


class FlightService:
Expand Down Expand Up @@ -55,10 +58,16 @@ def get_flight_simulation(self) -> FlightSimulation:
Returns:
FlightSimulation
"""
attributes = rocketpy_encoder(
self.flight, DiscretizeConfig.for_flight()
encoded_attributes = collect_attributes(
self.flight,
[
FlightSimulation,
RocketSimulation,
MotorSimulation,
EnvironmentSimulation,
],
)
flight_simulation = FlightSimulation(**attributes)
flight_simulation = FlightSimulation(**encoded_attributes)
return flight_simulation

def get_flight_binary(self) -> bytes:
Expand Down
9 changes: 6 additions & 3 deletions src/services/motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from src.models.sub.tanks import TankKinds
from src.models.motor import MotorKinds, MotorModel
from src.views.motor import MotorSimulation
from src.utils import rocketpy_encoder, DiscretizeConfig
from src.utils import collect_attributes


class MotorService:
Expand Down Expand Up @@ -140,8 +140,11 @@ def get_motor_simulation(self) -> MotorSimulation:
Returns:
MotorSimulation
"""
attributes = rocketpy_encoder(self.motor, DiscretizeConfig.for_motor())
motor_simulation = MotorSimulation(**attributes)
encoded_attributes = collect_attributes(
self.motor,
[MotorSimulation],
)
motor_simulation = MotorSimulation(**encoded_attributes)
return motor_simulation

def get_motor_binary(self) -> bytes:
Expand Down
9 changes: 5 additions & 4 deletions src/services/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from src.models.sub.aerosurfaces import NoseCone, Tail, Fins
from src.services.motor import MotorService
from src.views.rocket import RocketSimulation
from src.utils import rocketpy_encoder, DiscretizeConfig
from src.views.motor import MotorSimulation
from src.utils import collect_attributes


class RocketService:
Expand Down Expand Up @@ -107,10 +108,10 @@ def get_rocket_simulation(self) -> RocketSimulation:
Returns:
RocketSimulation
"""
attributes = rocketpy_encoder(
self.rocket, DiscretizeConfig.for_rocket()
encoded_attributes = collect_attributes(
self.rocket, [RocketSimulation, MotorSimulation]
)
rocket_simulation = RocketSimulation(**attributes)
rocket_simulation = RocketSimulation(**encoded_attributes)
return rocket_simulation

def get_rocket_binary(self) -> bytes:
Expand Down
217 changes: 150 additions & 67 deletions src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@
import io
import logging
import json
import copy
from datetime import datetime

from typing import NoReturn, Tuple

from rocketpy import Function
import numpy as np
from scipy.interpolate import interp1d

from rocketpy import Function, Flight
from rocketpy._encoders import RocketPyEncoder

from starlette.datastructures import Headers, MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send

from src.views.environment import EnvironmentSimulation
from src.views.flight import FlightSimulation
from src.views.motor import MotorSimulation
from src.views.rocket import RocketSimulation

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -46,78 +53,154 @@ def for_flight(cls) -> 'DiscretizeConfig':
return cls(bounds=(0, 30), samples=200)


def rocketpy_encoder(obj, config: DiscretizeConfig = DiscretizeConfig()):
"""
Encode a RocketPy object using official RocketPy encoders.
class InfinityEncoder(RocketPyEncoder):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

This function creates a copy of the object, discretizes callable Function
attributes on the copy, and then uses RocketPy's official RocketPyEncoder for
complete object serialization. The original object remains unchanged.
def default(self, obj):
if (
isinstance(obj, Function)
and not callable(obj.source)
and obj.__dom_dim__ == 1
):
size = len(obj._domain)
reduction_factor = 1
if size > 25:
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe this can be simplified to single line with ternary approach

reduction_factor = size // 25
if reduction_factor > 1:
obj = obj.set_discrete(
obj.x_array[0],
obj.x_array[-1],
size // reduction_factor,
mutate_self=False,
)
if isinstance(obj, Flight):
Comment on lines +61 to +77
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

Function downsampling doesn’t enforce ≤25 for sizes 26–49; avoid private attr

size // 25 floors to 1 when 26 ≤ size ≤ 49, so no reduction happens. Also prefer public arrays over _domain when available.

-        if (
+        if (
             isinstance(obj, Function)
             and not callable(obj.source)
             and obj.__dom_dim__ == 1
         ):
-            size = len(obj._domain)
-            reduction_factor = 1
-            if size > 25:
-                reduction_factor = size // 25
-            if reduction_factor > 1:
-                obj = obj.set_discrete(
-                    obj.x_array[0],
-                    obj.x_array[-1],
-                    size // reduction_factor,
-                    mutate_self=False,
-                )
+            size = len(getattr(obj, "x_array", [])) or len(getattr(obj, "_domain", []))
+            target_samples = 25 if size > 25 else size
+            if target_samples and target_samples != size:
+                obj = obj.set_discrete(
+                    obj.x_array[0],
+                    obj.x_array[-1],
+                    target_samples,
+                    mutate_self=False,
+                )
📝 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
if (
isinstance(obj, Function)
and not callable(obj.source)
and obj.__dom_dim__ == 1
):
size = len(obj._domain)
reduction_factor = 1
if size > 25:
reduction_factor = size // 25
if reduction_factor > 1:
obj = obj.set_discrete(
obj.x_array[0],
obj.x_array[-1],
size // reduction_factor,
mutate_self=False,
)
if isinstance(obj, Flight):
if (
isinstance(obj, Function)
and not callable(obj.source)
and obj.__dom_dim__ == 1
):
size = len(getattr(obj, "x_array", [])) or len(getattr(obj, "_domain", []))
target_samples = 25 if size > 25 else size
if target_samples and target_samples != size:
obj = obj.set_discrete(
obj.x_array[0],
obj.x_array[-1],
target_samples,
mutate_self=False,
)
if isinstance(obj, Flight):
🤖 Prompt for AI Agents
In src/utils.py around lines 61–77, the downsampling logic uses size =
len(obj._domain) and reduction_factor = size // 25 which floors to 1 for sizes
26–49 and also accesses a private _domain; change to compute size from a public
attribute when available (e.g. getattr(obj, "domain", getattr(obj, "x_array",
obj._domain))) and compute reduction_factor = math.ceil(size / 25) (import math
if needed), then apply downsampling only when reduction_factor > 1 using the
computed reduction_factor (keeping the existing set_discrete call but using the
new size calculation) so sizes 26–49 are correctly reduced and private
attributes are avoided when possible.

obj._Flight__evaluate_post_process
Copy link

Copilot AI Aug 31, 2025

Choose a reason for hiding this comment

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

This line accesses a private method but doesn't call it or assign the result. It should either call the method with parentheses obj._Flight__evaluate_post_process() or remove this line if it's not needed.

Suggested change
obj._Flight__evaluate_post_process
obj._Flight__evaluate_post_process()

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

seems to be a legit statement from copilot

Copy link
Contributor

Choose a reason for hiding this comment

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

The method is actually a cached property. It needs to be called at this step to make sure the Flight has its value cached for internal usage. Note: when I pop some of its attributes at the end to reduce size, this property would break if called after, caching it prevents this problem.

solution = np.array(obj.solution)
size = len(solution)
if size > 25:
reduction_factor = size // 25
reduced_solution = np.zeros(
(size // reduction_factor, solution.shape[1])
)
reduced_scale = np.linspace(
solution[0, 0], solution[-1, 0], size // reduction_factor
)
for i, col in enumerate(solution.T):
reduced_solution[:, i] = interp1d(
solution[:, 0], col, assume_sorted=True
)(reduced_scale)
obj.solution = reduced_solution.tolist()

Args:
obj: RocketPy object (Environment, Motor, Rocket, Flight)
config: DiscretizeConfig object with discretization parameters (optional)
obj.flight_phases = None
obj.function_evaluations = None

Returns:
Dictionary of encoded attributes
"""
return super().default(obj)

if config is None:
config = DiscretizeConfig()
try:
# Create a copy to avoid mutating the original object
obj_copy = copy.deepcopy(obj)
except Exception:
# Fall back to a shallow copy if deep copy is not supported
obj_copy = copy.copy(obj)

for attr_name in dir(obj_copy):
if attr_name.startswith('_'):
continue

try:
attr_value = getattr(obj_copy, attr_name)
except Exception:
continue
def rocketpy_encoder(obj):
"""
Encode a RocketPy object using official RocketPy encoders.

if callable(attr_value) and isinstance(attr_value, Function):
Uses InfinityEncoder for serialization and reduction.
"""
json_str = json.dumps(
obj,
cls=InfinityEncoder,
include_outputs=True,
include_function_data=True,
discretize=True,
allow_pickle=False,
)
encoded_result = json.loads(json_str)
return _fix_datetime_fields(encoded_result)


def collect_attributes(obj, attribute_classes=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

would you mind to set the signature as Union[FlightSim, EnvSim, ...) ?

"""
Collect attributes from various simulation classes and populate them from the flight object.
"""
if attribute_classes is None:
attribute_classes = []

attributes = rocketpy_encoder(obj)

for attribute_class in attribute_classes:
if issubclass(attribute_class, FlightSimulation):
flight_attributes_list = [
attr
for attr in attribute_class.__annotations__.keys()
if attr not in ["message", "rocket", "env"]
]
try:
discretized_func = Function(attr_value.source)
discretized_func.set_discrete(
lower=config.bounds[0],
upper=config.bounds[1],
samples=config.samples,
mutate_self=True,
)

setattr(obj_copy, attr_name, discretized_func)

except Exception as e:
logger.warning(f"Failed to discretize {attr_name}: {e}")
for key in flight_attributes_list:
if key not in attributes:
try:
value = getattr(obj, key)
attributes[key] = value
except Exception:
pass
except Exception:
pass

elif issubclass(attribute_class, RocketSimulation):
rocket_attributes_list = [
attr
for attr in attribute_class.__annotations__.keys()
if attr not in ["message", "motor"]
]
try:
for key in rocket_attributes_list:
if key not in attributes.get("rocket", {}):
try:
value = getattr(obj.rocket, key)
attributes.setdefault("rocket", {})[key] = value
except Exception:
pass
except Exception:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think there is space for any exception in the search, there is explicit default to {} already if 'rocket' is not found,

so I think we should be able to remove this extra try catch

also, I would prefer to be more specific if possible to which exception we are comfortable skipping and which we need to raise, maybe not all of them should be skipped (at the expense of possibly compromising simulation integrity)

pass

elif issubclass(attribute_class, MotorSimulation):
motor_attributes_list = [
attr
for attr in attribute_class.__annotations__.keys()
if attr not in ["message"]
]
try:
for key in motor_attributes_list:
if key not in attributes.get("rocket", {}).get(
"motor", {}
):
try:
value = getattr(obj.rocket.motor, key)
attributes.setdefault("rocket", {}).setdefault(
"motor", {}
)[key] = value
except Exception:
pass
except Exception:
Copy link
Collaborator

Choose a reason for hiding this comment

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

same as above

pass

elif issubclass(attribute_class, EnvironmentSimulation):
environment_attributes_list = [
attr
for attr in attribute_class.__annotations__.keys()
if attr not in ["message"]
]
try:
for key in environment_attributes_list:
if key not in attributes.get("env", {}):
try:
value = getattr(obj.env, key)
attributes.setdefault("env", {})[key] = value
except Exception:
pass
except Exception:
Copy link
Collaborator

Choose a reason for hiding this comment

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

same as above

pass
else:
continue

Comment on lines +119 to 202
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 31, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling and reduce complexity

The collect_attributes function has high cyclomatic complexity (23 branches) and deeply nested blocks. The broad exception catching could hide bugs. Consider:

  1. Extracting methods for each attribute class type
  2. Using more specific exception types
  3. Logging errors instead of silently passing
+import logging
+
+logger = logging.getLogger(__name__)
+
 def collect_attributes(obj, attribute_classes=None):
     """
     Collect attributes from various simulation classes and populate them from the flight object.
     """
     if attribute_classes is None:
         attribute_classes = []

     attributes = rocketpy_encoder(obj)

     for attribute_class in attribute_classes:
         if issubclass(attribute_class, FlightSimulation):
-            flight_attributes_list = [
-                attr
-                for attr in attribute_class.__annotations__.keys()
-                if attr not in ["message", "rocket", "env"]
-            ]
-            try:
-                for key in flight_attributes_list:
-                    if key not in attributes:
-                        try:
-                            value = getattr(obj, key)
-                            attributes[key] = value
-                        except Exception:
-                            pass
-            except Exception:
-                pass
+            _collect_flight_attributes(obj, attribute_class, attributes)
         elif issubclass(attribute_class, RocketSimulation):
-            # ... similar refactor
+            _collect_rocket_attributes(obj, attribute_class, attributes)
         elif issubclass(attribute_class, MotorSimulation):
-            # ... similar refactor
+            _collect_motor_attributes(obj, attribute_class, attributes)
         elif issubclass(attribute_class, EnvironmentSimulation):
-            # ... similar refactor
+            _collect_environment_attributes(obj, attribute_class, attributes)
         else:
             continue

     return attributes

+def _collect_flight_attributes(obj, attribute_class, attributes):
+    """Collect flight-specific attributes."""
+    flight_attributes_list = [
+        attr
+        for attr in attribute_class.__annotations__.keys()
+        if attr not in ["message", "rocket", "env"]
+    ]
+    for key in flight_attributes_list:
+        if key not in attributes:
+            try:
+                value = getattr(obj, key)
+                attributes[key] = value
+            except AttributeError:
+                logger.debug(f"Attribute {key} not found on flight object")
+            except Exception as e:
+                logger.warning(f"Error collecting flight attribute {key}: {e}")

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: Pylint

[warning] 125-125: pylint: R1702: Too many nested blocks (6/5) (too-many-nested-blocks)


[warning] 125-125: pylint: R1702: Too many nested blocks (6/5) (too-many-nested-blocks)


[warning] 125-125: pylint: R1702: Too many nested blocks (6/5) (too-many-nested-blocks)


[warning] 116-116: pylint: R0912: Too many branches (23/12) (too-many-branches)


[warning] 125-125: pylint: R1702: Too many nested blocks (6/5) (too-many-nested-blocks)

🤖 Prompt for AI Agents
src/utils.py lines 116-199: The collect_attributes function is too complex and
swallows all exceptions; refactor by extracting four small helper functions
(populate_flight_attributes(obj, attributes, attribute_class),
populate_rocket_attributes(...), populate_motor_attributes(...),
populate_environment_attributes(...)) and call them in the main loop with an
early-continue style to avoid deep nesting; replace bare except Exception blocks
with specific catches (AttributeError, TypeError, KeyError) around getattr and
issubclass checks, and surface failures by logging them (add a module logger via
import logging; logger = logging.getLogger(__name__) and use logger.exception or
logger.error) instead of silently passing; ensure helpers use
attributes.setdefault for nested dicts and avoid repeated get(..., {}) checks to
simplify logic.

Copy link
Collaborator

Choose a reason for hiding this comment

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

that is a very nice refactoring suggestion,

it modularizes the attribute collection distributing complexity out of global collection more evenly between each object

Copy link
Contributor

Choose a reason for hiding this comment

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

@GabrielBarberini Thank you! I'm glad the refactoring suggestion resonates with you. Breaking down the complex collect_attributes function into focused helper functions would indeed make the code more maintainable and easier to reason about. The modular approach also makes it easier to test each attribute collection strategy independently and provides better error visibility through specific logging.

try:
json_str = json.dumps(
obj_copy,
cls=RocketPyEncoder,
include_outputs=True,
include_function_data=True,
)
encoded_result = json.loads(json_str)

# Post-process to fix datetime fields that got converted to lists
return _fix_datetime_fields(encoded_result)
except Exception as e:
logger.warning(f"Failed to encode with RocketPyEncoder: {e}")
attributes = {}
for attr_name in dir(obj_copy):
if not attr_name.startswith('_'):
try:
attr_value = getattr(obj_copy, attr_name)
if not callable(attr_value):
attributes[attr_name] = str(attr_value)
except Exception:
continue
return attributes
return rocketpy_encoder(attributes)
Copy link

Copilot AI Aug 31, 2025

Choose a reason for hiding this comment

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

This creates infinite recursion since collect_attributes calls rocketpy_encoder at line 121, which returns the result of collect_attributes. The return should likely be just attributes instead.

Suggested change
return rocketpy_encoder(attributes)
return attributes

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure I got what copilot is trying to say but I think return here yields something like rocketpy_encoder(rocketpy_encoder(obj).modified())

.modified() being just an illustrative made up noop to emphasize something changed after instantiation.

is that really what we want?



def _fix_datetime_fields(data):
Expand Down
Loading
Loading