Skip to content

Commit e5fcc93

Browse files
RahulKrishna145nitheeshhsGui-FernandesBRMateusStano
authored
ENH: Air brakes controller functions now support 8-parameter signature (#854)
* solved environment parameter global usage, now can be used locally * f string placeholder changes in test file * ENH: Update air brakes controller to support 8-parameter signature and improve environment access * make lint * solved environment parameter global usage, now can be used locally * f string placeholder changes in test file * ENH: Update air brakes controller to support 8-parameter signature and improve environment access * make lint * fix tests --------- Co-authored-by: Nitheesh Krishna <nitheeshhs@gmail.com> Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Co-authored-by: Mateus Stano Junqueira <69485049+MateusStano@users.noreply.github.com>
1 parent 37dd8de commit e5fcc93

10 files changed

Lines changed: 536 additions & 54 deletions

File tree

CHANGELOG.md

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

3333
### Added
34+
35+
- ENH: Air brakes controller functions now support 8-parameter signature [#854](https://github.com/RocketPy-Team/RocketPy/pull/854)
3436
- TST: Add acceptance tests for 3DOF flight simulation based on Bella Lui rocket [#914] (https://github.com/RocketPy-Team/RocketPy/pull/914_
3537
- ENH: Add background map auto download functionality to Monte Carlo plots [#896](https://github.com/RocketPy-Team/RocketPy/pull/896)
3638
- MNT: net thrust addition to 3 dof in flight class [#907] (https://github.com/RocketPy-Team/RocketPy/pull/907)

docs/user/airbrakes.rst

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,21 @@ Lets define the controller function:
167167
.. jupyter-execute::
168168

169169
def controller_function(
170-
time, sampling_rate, state, state_history, observed_variables, air_brakes
170+
time, sampling_rate, state, state_history, observed_variables, air_brakes, sensors, environment
171171
):
172172
# state = [x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]
173173
altitude_ASL = state[2]
174-
altitude_AGL = altitude_ASL - env.elevation
174+
altitude_AGL = altitude_ASL - environment.elevation
175175
vx, vy, vz = state[3], state[4], state[5]
176176

177177
# Get winds in x and y directions
178-
wind_x, wind_y = env.wind_velocity_x(altitude_ASL), env.wind_velocity_y(altitude_ASL)
178+
wind_x, wind_y = environment.wind_velocity_x(altitude_ASL), environment.wind_velocity_y(altitude_ASL)
179179

180180
# Calculate Mach number
181181
free_stream_speed = (
182182
(wind_x - vx) ** 2 + (wind_y - vy) ** 2 + (vz) ** 2
183183
) ** 0.5
184-
mach_number = free_stream_speed / env.speed_of_sound(altitude_ASL)
184+
mach_number = free_stream_speed / environment.speed_of_sound(altitude_ASL)
185185

186186
# Get previous state from state_history
187187
previous_state = state_history[-1]
@@ -224,6 +224,22 @@ Lets define the controller function:
224224

225225
.. note::
226226

227+
- The ``controller_function`` accepts 6, 7, or 8 parameters for backward
228+
compatibility:
229+
230+
* **6 parameters** (original): ``time``, ``sampling_rate``, ``state``,
231+
``state_history``, ``observed_variables``, ``air_brakes``
232+
* **7 parameters** (with sensors): adds ``sensors`` as the 7th parameter
233+
* **8 parameters** (with environment): adds ``sensors`` and ``environment``
234+
as the 7th and 8th parameters
235+
236+
- The **environment parameter** provides access to atmospheric conditions
237+
(wind, temperature, pressure, elevation) without relying on global variables.
238+
This enables proper serialization of rockets with air brakes and improves
239+
code modularity. Available methods include ``environment.elevation``,
240+
``environment.wind_velocity_x(altitude)``, ``environment.wind_velocity_y(altitude)``,
241+
``environment.speed_of_sound(altitude)``, and others.
242+
227243
- The code inside the ``controller_function`` can be as complex as needed.
228244
Anything can be implemented inside the function, including filters,
229245
apogee prediction, and any controller logic.

rocketpy/control/controller.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ def __init__(
6060
7. `sensors` (list): A list of sensors that are attached to the
6161
rocket. The most recent measurements of the sensors are provided
6262
with the ``sensor.measurement`` attribute. The sensors are
63-
listed in the same order as they are added to the rocket
63+
listed in the same order as they are added to the rocket.
64+
8. `environment` (Environment): The environment object containing
65+
atmospheric conditions, wind data, gravity, and other
66+
environmental parameters. This allows the controller to access
67+
environmental data locally without relying on global variables.
6468
6569
This function will be called during the simulation at the specified
6670
sampling rate. The function should evaluate and change the interactive
@@ -102,7 +106,7 @@ def __init__(
102106
def __init_controller_function(self, controller_function):
103107
"""Checks number of arguments of the controller function and initializes
104108
it with the correct number of arguments. This is a workaround to allow
105-
the controller function to receive sensors without breaking changes"""
109+
the controller function to receive sensors and environment without breaking changes"""
106110
sig = signature(controller_function)
107111
if len(sig.parameters) == 6:
108112
# pylint: disable=unused-argument
@@ -114,6 +118,7 @@ def new_controller_function(
114118
observed_variables,
115119
interactive_objects,
116120
sensors,
121+
environment,
117122
):
118123
return controller_function(
119124
time,
@@ -125,18 +130,43 @@ def new_controller_function(
125130
)
126131

127132
elif len(sig.parameters) == 7:
133+
# pylint: disable=unused-argument
134+
def new_controller_function(
135+
time,
136+
sampling_rate,
137+
state_vector,
138+
state_history,
139+
observed_variables,
140+
interactive_objects,
141+
sensors,
142+
environment,
143+
):
144+
return controller_function(
145+
time,
146+
sampling_rate,
147+
state_vector,
148+
state_history,
149+
observed_variables,
150+
interactive_objects,
151+
sensors,
152+
)
153+
154+
elif len(sig.parameters) == 8:
128155
new_controller_function = controller_function
129156
else:
130157
raise ValueError(
131-
"The controller function must have 6 or 7 arguments. "
158+
"The controller function must have 6, 7, or 8 arguments. "
132159
"The arguments must be in the following order: "
133160
"(time, sampling_rate, state_vector, state_history, "
134-
"observed_variables, interactive_objects, sensors)."
135-
"Sensors argument is optional."
161+
"observed_variables, interactive_objects, sensors, environment). "
162+
"Supported signatures: "
163+
"6 parameters (no sensors, no environment), "
164+
"7 parameters (with sensors, no environment), or "
165+
"8 parameters (with sensors and environment)."
136166
)
137167
return new_controller_function
138168

139-
def __call__(self, time, state_vector, state_history, sensors):
169+
def __call__(self, time, state_vector, state_history, sensors, environment):
140170
"""Call the controller function. This is used by the simulation class.
141171
142172
Parameters
@@ -157,6 +187,9 @@ def __call__(self, time, state_vector, state_history, sensors):
157187
measurements of the sensors are provided with the
158188
``sensor.measurement`` attribute. The sensors are listed in the same
159189
order as they are added to the rocket.
190+
environment : Environment
191+
The environment object containing atmospheric conditions, wind data,
192+
gravity, and other environmental parameters.
160193
161194
Returns
162195
-------
@@ -170,6 +203,7 @@ def __call__(self, time, state_vector, state_history, sensors):
170203
self.observed_variables,
171204
self.interactive_objects,
172205
sensors,
206+
environment,
173207
)
174208
if observed_variables is not None:
175209
self.observed_variables.append(observed_variables)

rocketpy/rocket/rocket.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,9 +1696,13 @@ def add_air_brakes(
16961696
listed in the same order as they are provided in the
16971697
`interactive_objects` argument.
16981698
7. `sensors` (list): A list of sensors that are attached to the
1699-
rocket. The most recent measurements of the sensors are provided
1700-
with the ``sensor.measurement`` attribute. The sensors are
1701-
listed in the same order as they are added to the rocket.
1699+
rocket. The most recent measurements of the sensors are provided
1700+
with the ``sensor.measurement`` attribute. The sensors are
1701+
listed in the same order as they are added to the rocket.
1702+
8. `environment` (Environment): The environment object containing
1703+
atmospheric conditions, wind data, gravity, and other
1704+
environmental parameters. This allows the controller to access
1705+
environmental data locally without relying on global variables.
17021706
17031707
This function will be called during the simulation at the specified
17041708
sampling rate. The function should evaluate and change the observed

rocketpy/simulation/flight.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@ def __simulate(self, verbose):
716716
self.y_sol,
717717
self.solution,
718718
self.sensors,
719+
self.env,
719720
)
720721

721722
for parachute in node.parachutes:
@@ -874,6 +875,7 @@ def __process_sensors_and_controllers_at_current_node(self, node, phase):
874875
self.y_sol,
875876
self.solution,
876877
self.sensors,
878+
self.env,
877879
)
878880

879881
def __measure_sensors(self, component_sensors, u_dot, t=None, y_sol=None):
@@ -1295,6 +1297,7 @@ def __process_overshootable_nodes(self, phase, phase_index, node_index):
12951297
overshootable_node.y_sol,
12961298
self.solution,
12971299
self.sensors,
1300+
self.env,
12981301
)
12991302

13001303
# Process sensors at overshootable node

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"tests.fixtures.environment.environment_fixtures",
1212
"tests.fixtures.flight.flight_fixtures",
1313
"tests.fixtures.function.function_fixtures",
14+
"tests.fixtures.controller.controller_fixtures",
1415
"tests.fixtures.motor.liquid_fixtures",
1516
"tests.fixtures.motor.hybrid_fixtures",
1617
"tests.fixtures.motor.solid_motor_fixtures",

tests/fixtures/controller/__init__.py

Whitespace-only changes.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import pytest
2+
3+
4+
## Controller
5+
@pytest.fixture
6+
def controller_function():
7+
"""Create a controller function that updates the air brakes deployment level
8+
based on the altitude and vertical velocity of the rocket. This is the same
9+
controller function that is used in the air brakes example in the
10+
documentation.
11+
12+
Returns
13+
-------
14+
function
15+
A controller function
16+
"""
17+
18+
def controller_function( # pylint: disable=unused-argument
19+
time, sampling_rate, state, state_history, observed_variables, air_brakes
20+
):
21+
z = state[2]
22+
vz = state[5]
23+
previous_vz = state_history[-1][5]
24+
if time < 3.9:
25+
return None
26+
if z < 1500:
27+
air_brakes.deployment_level = 0
28+
else:
29+
new_deployment_level = (
30+
air_brakes.deployment_level + 0.1 * vz + 0.01 * previous_vz**2
31+
)
32+
if new_deployment_level > air_brakes.deployment_level + 0.2 / sampling_rate:
33+
new_deployment_level = air_brakes.deployment_level + 0.2 / sampling_rate
34+
elif (
35+
new_deployment_level < air_brakes.deployment_level - 0.2 / sampling_rate
36+
):
37+
new_deployment_level = air_brakes.deployment_level - 0.2 / sampling_rate
38+
else:
39+
new_deployment_level = air_brakes.deployment_level
40+
air_brakes.deployment_level = new_deployment_level
41+
42+
return controller_function
43+
44+
45+
@pytest.fixture
46+
def controller_function_with_environment():
47+
"""Create a controller function that uses the environment parameter to access
48+
atmospheric conditions without relying on global variables. This demonstrates
49+
the new environment parameter feature for air brakes controllers.
50+
51+
Returns
52+
-------
53+
function
54+
A controller function that uses environment parameter
55+
"""
56+
57+
def controller_function( # pylint: disable=unused-argument
58+
time,
59+
sampling_rate,
60+
state,
61+
state_history,
62+
observed_variables,
63+
air_brakes,
64+
sensors,
65+
environment,
66+
):
67+
# state = [x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]
68+
altitude_asl = state[2] # altitude above sea level
69+
altitude_agl = (
70+
altitude_asl - environment.elevation
71+
) # altitude above ground level
72+
vx, vy, vz = state[3], state[4], state[5]
73+
74+
# Use environment parameter instead of global variable
75+
wind_x = environment.wind_velocity_x(altitude_asl)
76+
wind_y = environment.wind_velocity_y(altitude_asl)
77+
78+
# Calculate Mach number using environment data
79+
free_stream_speed = ((wind_x - vx) ** 2 + (wind_y - vy) ** 2 + (vz) ** 2) ** 0.5
80+
mach_number = free_stream_speed / environment.speed_of_sound(altitude_asl)
81+
82+
if time < 3.9:
83+
return None
84+
85+
if altitude_agl < 1500:
86+
air_brakes.deployment_level = 0
87+
else:
88+
previous_vz = state_history[-1][5] if state_history else vz
89+
new_deployment_level = (
90+
air_brakes.deployment_level + 0.1 * vz + 0.01 * previous_vz**2
91+
)
92+
# Rate limiting
93+
max_change = 0.2 / sampling_rate
94+
if new_deployment_level > air_brakes.deployment_level + max_change:
95+
new_deployment_level = air_brakes.deployment_level + max_change
96+
elif new_deployment_level < air_brakes.deployment_level - max_change:
97+
new_deployment_level = air_brakes.deployment_level - max_change
98+
99+
air_brakes.deployment_level = new_deployment_level
100+
101+
# Return observed variables including Mach number
102+
return (time, air_brakes.deployment_level, mach_number)
103+
104+
return controller_function

tests/fixtures/function/function_fixtures.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -84,47 +84,6 @@ def func_2d_from_csv():
8484
return func
8585

8686

87-
## Controller
88-
@pytest.fixture
89-
def controller_function():
90-
"""Create a controller function that updates the air brakes deployment level
91-
based on the altitude and vertical velocity of the rocket. This is the same
92-
controller function that is used in the air brakes example in the
93-
documentation.
94-
95-
Returns
96-
-------
97-
function
98-
A controller function
99-
"""
100-
101-
def controller_function( # pylint: disable=unused-argument
102-
time, sampling_rate, state, state_history, observed_variables, air_brakes
103-
):
104-
z = state[2]
105-
vz = state[5]
106-
previous_vz = state_history[-1][5]
107-
if time < 3.9:
108-
return None
109-
if z < 1500:
110-
air_brakes.deployment_level = 0
111-
else:
112-
new_deployment_level = (
113-
air_brakes.deployment_level + 0.1 * vz + 0.01 * previous_vz**2
114-
)
115-
if new_deployment_level > air_brakes.deployment_level + 0.2 / sampling_rate:
116-
new_deployment_level = air_brakes.deployment_level + 0.2 / sampling_rate
117-
elif (
118-
new_deployment_level < air_brakes.deployment_level - 0.2 / sampling_rate
119-
):
120-
new_deployment_level = air_brakes.deployment_level - 0.2 / sampling_rate
121-
else:
122-
new_deployment_level = air_brakes.deployment_level
123-
air_brakes.deployment_level = new_deployment_level
124-
125-
return controller_function
126-
127-
12887
@pytest.fixture
12988
def lambda_quad_func():
13089
"""Create a lambda function based on a string.

0 commit comments

Comments
 (0)