Skip to content

Commit 8c82c3a

Browse files
authored
ENH: Controller (AirBrakes) and Sensors Encoding (#849)
* ENH: add an option to discretize callable sources encoding. * ENH: allow for disallowing pickle on encoding. * MNT: Update CHANGELOG. * ENH: support for air brakes, controller and sensors encoding. * STY: solve linting and style remarks. * BUG: parachute callbacks attribute naming. * GIT: test agg backend for matplotlib workflows. * TST: include sensors and controllers encoding tests as non slow. * MNT: change recursive to iterative approach on hash search.
1 parent f89834b commit 8c82c3a

File tree

17 files changed

+299
-29
lines changed

17 files changed

+299
-29
lines changed

.github/workflows/test-pytest-slow.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
- cron: "0 17 * * 5" # at 05:00 PM, only on Friday
66
push:
77
branches:
8-
- main
8+
- master
99
paths:
1010
- "**.py"
1111
- ".github/**"
@@ -24,6 +24,7 @@ jobs:
2424
python-version: [3.9, 3.13]
2525
env:
2626
PYTHON: ${{ matrix.python-version }}
27+
MPLBACKEND: Agg
2728
steps:
2829
- uses: actions/checkout@main
2930
- name: Set up Python

.github/workflows/test_pytest.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
env:
2424
OS: ${{ matrix.os }}
2525
PYTHON: ${{ matrix.python-version }}
26+
MPLBACKEND: Agg
2627
steps:
2728
- uses: actions/checkout@main
2829
- name: Set up Python

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
3131
Attention: The newest changes should be on top -->
3232

3333
### Added
34-
34+
- ENH: Controller (AirBrakes) and Sensors Encoding [#849] (https://github.com/RocketPy-Team/RocketPy/pull/849)
3535
- EHN: Addition of ensemble variable to ECMWF dictionaries [#842] (https://github.com/RocketPy-Team/RocketPy/pull/842)
3636
- ENH: Added Crop and Clip Methods to Function Class [#817](https://github.com/RocketPy-Team/RocketPy/pull/817)
3737
- DOC: Add Flight class usage documentation and update index [#841](https://github.com/RocketPy-Team/RocketPy/pull/841)

rocketpy/_encoders.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,24 +107,32 @@ def object_hook(self, obj):
107107

108108
try:
109109
class_ = get_class_from_signature(signature)
110+
hash_ = signature.get("hash", None)
110111

111112
if class_.__name__ == "Flight" and not self.resimulate:
112113
new_flight = class_.__new__(class_)
113114
new_flight.prints = _FlightPrints(new_flight)
114115
new_flight.plots = _FlightPlots(new_flight)
115116
set_minimal_flight_attributes(new_flight, obj)
117+
if hash_ is not None:
118+
setattr(new_flight, "__rpy_hash", hash_)
116119
return new_flight
117120
elif hasattr(class_, "from_dict"):
118-
return class_.from_dict(obj)
121+
new_obj = class_.from_dict(obj)
122+
if hash_ is not None:
123+
setattr(new_obj, "__rpy_hash", hash_)
124+
return new_obj
119125
else:
120126
# Filter keyword arguments
121127
kwargs = {
122128
key: value
123129
for key, value in obj.items()
124130
if key in class_.__init__.__code__.co_varnames
125131
}
126-
127-
return class_(**kwargs)
132+
new_obj = class_(**kwargs)
133+
if hash_ is not None:
134+
setattr(new_obj, "__rpy_hash", hash_)
135+
return new_obj
128136
except (ImportError, AttributeError):
129137
return obj
130138
else:
@@ -157,7 +165,6 @@ def set_minimal_flight_attributes(flight, obj):
157165
"x_impact",
158166
"y_impact",
159167
"t_final",
160-
"flight_phases",
161168
"ax",
162169
"ay",
163170
"az",
@@ -207,7 +214,14 @@ def get_class_signature(obj):
207214
class_ = obj.__class__
208215
name = getattr(class_, "__qualname__", class_.__name__)
209216

210-
return {"module": class_.__module__, "name": name}
217+
signature = {"module": class_.__module__, "name": name}
218+
219+
try:
220+
signature.update({"hash": hash(obj)})
221+
except TypeError:
222+
pass
223+
224+
return signature
211225

212226

213227
def get_class_from_signature(signature):

rocketpy/control/controller.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from inspect import signature
2+
from typing import Iterable
3+
4+
from rocketpy.tools import from_hex_decode, to_hex_encode
25

36
from ..prints.controller_prints import _ControllerPrints
47

@@ -181,3 +184,46 @@ def info(self):
181184
def all_info(self):
182185
"""Prints out all information about the controller."""
183186
self.info()
187+
188+
def to_dict(self, **kwargs):
189+
allow_pickle = kwargs.get("allow_pickle", True)
190+
191+
if allow_pickle:
192+
controller_function = to_hex_encode(self.controller_function)
193+
else:
194+
controller_function = self.controller_function.__name__
195+
196+
return {
197+
"controller_function": controller_function,
198+
"sampling_rate": self.sampling_rate,
199+
"initial_observed_variables": self.initial_observed_variables,
200+
"name": self.name,
201+
"_interactive_objects_hash": hash(self.interactive_objects)
202+
if not isinstance(self.interactive_objects, Iterable)
203+
else [hash(obj) for obj in self.interactive_objects],
204+
}
205+
206+
@classmethod
207+
def from_dict(cls, data):
208+
interactive_objects = data.get("interactive_objects", [])
209+
controller_function = data.get("controller_function")
210+
sampling_rate = data.get("sampling_rate")
211+
initial_observed_variables = data.get("initial_observed_variables")
212+
name = data.get("name", "Controller")
213+
214+
try:
215+
controller_function = from_hex_decode(controller_function)
216+
except (TypeError, ValueError):
217+
pass
218+
219+
obj = cls(
220+
interactive_objects=interactive_objects,
221+
controller_function=controller_function,
222+
sampling_rate=sampling_rate,
223+
initial_observed_variables=initial_observed_variables,
224+
name=name,
225+
)
226+
setattr(
227+
obj, "_interactive_objects_hash", data.get("_interactive_objects_hash", [])
228+
)
229+
return obj

rocketpy/rocket/aero_surface/air_brakes.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,24 @@ def all_info(self):
206206
"""
207207
self.info()
208208
self.plots.drag_coefficient_curve()
209+
210+
def to_dict(self, **kwargs): # pylint: disable=unused-argument
211+
return {
212+
"drag_coefficient_curve": self.drag_coefficient,
213+
"reference_area": self.reference_area,
214+
"clamp": self.clamp,
215+
"override_rocket_drag": self.override_rocket_drag,
216+
"deployment_level": self.initial_deployment_level,
217+
"name": self.name,
218+
}
219+
220+
@classmethod
221+
def from_dict(cls, data):
222+
return cls(
223+
drag_coefficient_curve=data.get("drag_coefficient_curve"),
224+
reference_area=data.get("reference_area"),
225+
clamp=data.get("clamp"),
226+
override_rocket_drag=data.get("override_rocket_drag"),
227+
deployment_level=data.get("deployment_level"),
228+
name=data.get("name"),
229+
)

rocketpy/rocket/aero_surface/fins/fins.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,13 +427,25 @@ def compute_forces_and_moments(
427427
return R1, R2, R3, M1, M2, M3
428428

429429
def to_dict(self, **kwargs):
430+
if self.airfoil:
431+
if kwargs.get("discretize", False):
432+
lower = -np.pi / 6 if self.airfoil[1] == "radians" else -30
433+
upper = np.pi / 6 if self.airfoil[1] == "radians" else 30
434+
airfoil = (
435+
self.airfoil_cl.set_discrete(lower, upper, 50, mutate_self=False),
436+
self.airfoil[1],
437+
)
438+
else:
439+
airfoil = (self.airfoil_cl, self.airfoil[1]) if self.airfoil else None
440+
else:
441+
airfoil = None
430442
data = {
431443
"n": self.n,
432444
"root_chord": self.root_chord,
433445
"span": self.span,
434446
"rocket_radius": self.rocket_radius,
435447
"cant_angle": self.cant_angle,
436-
"airfoil": self.airfoil,
448+
"airfoil": airfoil,
437449
"name": self.name,
438450
}
439451

rocketpy/rocket/rocket.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import math
2+
import warnings
3+
from typing import Iterable
24

35
import numpy as np
46

@@ -21,7 +23,11 @@
2123
from rocketpy.rocket.aero_surface.generic_surface import GenericSurface
2224
from rocketpy.rocket.components import Components
2325
from rocketpy.rocket.parachute import Parachute
24-
from rocketpy.tools import deprecated, parallel_axis_theorem_from_com
26+
from rocketpy.tools import (
27+
deprecated,
28+
find_obj_from_hash,
29+
parallel_axis_theorem_from_com,
30+
)
2531

2632

2733
# pylint: disable=too-many-instance-attributes, too-many-public-methods, too-many-instance-attributes
@@ -2070,17 +2076,29 @@ def from_dict(cls, data):
20702076
for parachute in data["parachutes"]:
20712077
rocket.parachutes.append(parachute)
20722078

2073-
for air_brakes in data["air_brakes"]:
2074-
rocket.add_air_brakes(
2075-
drag_coefficient_curve=air_brakes["drag_coefficient_curve"],
2076-
controller_function=air_brakes["controller_function"],
2077-
sampling_rate=air_brakes["sampling_rate"],
2078-
clamp=air_brakes["clamp"],
2079-
reference_area=air_brakes["reference_area"],
2080-
initial_observed_variables=air_brakes["initial_observed_variables"],
2081-
override_rocket_drag=air_brakes["override_rocket_drag"],
2082-
name=air_brakes["name"],
2083-
controller_name=air_brakes["controller_name"],
2084-
)
2079+
for sensor, position in data["sensors"]:
2080+
rocket.add_sensor(sensor, position)
2081+
2082+
for air_brake in data["air_brakes"]:
2083+
rocket.air_brakes.append(air_brake)
2084+
2085+
for controller in data["_controllers"]:
2086+
interactive_objects_hash = getattr(controller, "_interactive_objects_hash")
2087+
if interactive_objects_hash is not None:
2088+
is_iterable = isinstance(interactive_objects_hash, Iterable)
2089+
if not is_iterable:
2090+
interactive_objects_hash = [interactive_objects_hash]
2091+
for hash_ in interactive_objects_hash:
2092+
if (hashed_obj := find_obj_from_hash(data, hash_)) is not None:
2093+
if not is_iterable:
2094+
controller.interactive_objects = hashed_obj
2095+
else:
2096+
controller.interactive_objects.append(hashed_obj)
2097+
else:
2098+
warnings.warn(
2099+
"Could not find controller interactive objects."
2100+
"Deserialization will proceed, results may not be accurate."
2101+
)
2102+
rocket._add_controllers(controller)
20852103

20862104
return rocket

rocketpy/sensors/accelerometer.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,28 @@ def export_measured_data(self, filename, file_format="csv"):
275275
file_format=file_format,
276276
data_labels=("t", "ax", "ay", "az"),
277277
)
278+
279+
def to_dict(self, **kwargs):
280+
data = super().to_dict(**kwargs)
281+
data.update({"consider_gravity": self.consider_gravity})
282+
return data
283+
284+
@classmethod
285+
def from_dict(cls, data):
286+
return cls(
287+
sampling_rate=data["sampling_rate"],
288+
orientation=data["orientation"],
289+
measurement_range=data["measurement_range"],
290+
resolution=data["resolution"],
291+
noise_density=data["noise_density"],
292+
noise_variance=data["noise_variance"],
293+
random_walk_density=data["random_walk_density"],
294+
random_walk_variance=data["random_walk_variance"],
295+
constant_bias=data["constant_bias"],
296+
operating_temperature=data["operating_temperature"],
297+
temperature_bias=data["temperature_bias"],
298+
temperature_scale_factor=data["temperature_scale_factor"],
299+
cross_axis_sensitivity=data["cross_axis_sensitivity"],
300+
consider_gravity=data["consider_gravity"],
301+
name=data["name"],
302+
)

rocketpy/sensors/barometer.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,20 @@ def export_measured_data(self, filename, file_format="csv"):
190190
file_format=file_format,
191191
data_labels=("t", "pressure"),
192192
)
193+
194+
@classmethod
195+
def from_dict(cls, data):
196+
return cls(
197+
sampling_rate=data["sampling_rate"],
198+
measurement_range=data["measurement_range"],
199+
resolution=data["resolution"],
200+
noise_density=data["noise_density"],
201+
noise_variance=data["noise_variance"],
202+
random_walk_density=data["random_walk_density"],
203+
random_walk_variance=data["random_walk_variance"],
204+
constant_bias=data["constant_bias"],
205+
operating_temperature=data["operating_temperature"],
206+
temperature_bias=data["temperature_bias"],
207+
temperature_scale_factor=data["temperature_scale_factor"],
208+
name=data["name"],
209+
)

0 commit comments

Comments
 (0)