Skip to content

Commit eca155d

Browse files
ENH: Add actuator class for roll, throttle, thrust vector control (squash for RocketPy PR) (#7)
* Merge RocketPy 1.12 and add updates for balloon popping challenge (#8) * Add actuator dynamics in TVC and ThrottleControl authored-by: RickyRicato <rickywang44@gmail.com> * Refactor actuator_tau parameters to use None as default in TVC and ThrottleControl authored-by: RickyRicato <rickywang44@gmail.com> * Add actuator dynamics to RollControl authored-by: RickyRicato <rickywang44@gmail.com> * Move actuators to a new actuator folder and rename actuator classes * Refactor roll, throttle, and thrust vector acutator with a new actuator class * Add pytest for all actuators * Update rocket.py and flight.py for actuator class update * Fix actuator class calls * Update active control example with new actuator class --------- Co-authored-by: RickyRicato <rickywang44@gmail.com>
1 parent f0fb5c2 commit eca155d

12 files changed

Lines changed: 2300 additions & 17 deletions

File tree

docs/examples/halcyon_flight_sim_active_control.ipynb

Lines changed: 543 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class _RollActuatorPrints:
2+
"""Class that contains all roll actuator prints."""
3+
4+
def __init__(self, roll_actuator):
5+
"""Initializes _RollActuatorPrints class
6+
7+
Parameters
8+
----------
9+
roll_actuator: rocketpy.RollActuator
10+
Instance of the RollActuator class.
11+
12+
Returns
13+
-------
14+
None
15+
"""
16+
self.roll_actuator = roll_actuator
17+
18+
def basics(self):
19+
"""Prints information of the roll actuator."""
20+
print(f"Information of {self.roll_actuator.name}:")
21+
print("----------------------------------")
22+
print(f"Torque demand rate: {self.roll_actuator.demand_rate} Hz")
23+
print(
24+
f"Torque range: {self.roll_actuator.actuator_range[0]:.2f} to {self.roll_actuator.actuator_range[1]:.2f} N·m"
25+
)
26+
print(f"Torque rate limit: {self.roll_actuator.actuator_rate_limit} N·m/s")
27+
print(
28+
f"Torque clamping: {'Enabled' if self.roll_actuator.clamp else 'Disabled'}"
29+
)
30+
print(f"Torque time constant: {self.roll_actuator.actuator_time_constant} sec")
31+
print(f"Current roll torque: {self.roll_actuator.roll_torque:.2f} N·m")
32+
33+
def all(self):
34+
"""Prints all information of the roll actuator."""
35+
self.basics()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class _ThrottleActuatorPrints:
2+
def __init__(self, throttle_actuator):
3+
"""Initializes _ThrottleActuatorPrints class
4+
5+
Parameters
6+
----------
7+
throttle_actuator: rocketpy.ThrottleActuator
8+
Instance of the ThrottleActuator class.
9+
10+
Returns
11+
-------
12+
None
13+
"""
14+
self.throttle_actuator = throttle_actuator
15+
16+
def basics(self):
17+
"""Prints information of the throttle actuator."""
18+
print(f"Information of {self.throttle_actuator.name}:")
19+
print("----------------------------------")
20+
print(f"Throttle demand rate: {self.throttle_actuator.demand_rate} Hz")
21+
print(
22+
f"Throttle range: {self.throttle_actuator.actuator_range[0]:.2f} to {self.throttle_actuator.actuator_range[1]:.2f}"
23+
)
24+
print(f"Throttle rate limit: {self.throttle_actuator.actuator_rate_limit}")
25+
print(
26+
f"Throttle Clamping: {'Enabled' if self.throttle_actuator.clamp else 'Disabled'}"
27+
)
28+
print(
29+
f"Throttle time constant: {self.throttle_actuator.actuator_time_constant} sec"
30+
)
31+
print(f"Current throttle: {self.throttle_actuator.throttle:.2f}")
32+
33+
def all(self):
34+
"""Prints all information of the throttle actuator."""
35+
self.basics()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
class _ThrustVectorActuatorPrints:
2+
"""Class that contains all thrust vector actuator prints."""
3+
4+
def __init__(self, thrust_vector_actuator):
5+
"""Initializes _ThrustVectorActuatorPrints class
6+
7+
Parameters
8+
----------
9+
thrust_vector_actuator: rocketpy.ThrustVectorActuator
10+
Instance of the thrust vector actuator class.
11+
12+
Returns
13+
-------
14+
None
15+
"""
16+
self.thrust_vector_actuator = thrust_vector_actuator
17+
18+
def basics(self):
19+
"""Prints information of the thrust vector actuator."""
20+
print(f"Information of {self.thrust_vector_actuator.name}:")
21+
print("----------------------------------")
22+
print(f"Gimbal demand rate: {self.thrust_vector_actuator.demand_rate} Hz")
23+
print(
24+
f"Gimbal range: {self.thrust_vector_actuator.actuator_range[0]:.2f} to {self.thrust_vector_actuator.actuator_range[1]:.2f} deg"
25+
)
26+
print(
27+
f"Gimbal rate limit: {self.thrust_vector_actuator.actuator_rate_limit} deg/sec"
28+
)
29+
print(
30+
f"Gimbal clamping: {'Enabled' if self.thrust_vector_actuator.clamp else 'Disabled'}"
31+
)
32+
print(
33+
f"Gimbal time constant: {self.thrust_vector_actuator.actuator_time_constant} sec"
34+
)
35+
print(
36+
f"Current gimbal angle: {self.thrust_vector_actuator.gimbal_angle:.2f} deg"
37+
)
38+
39+
def all(self):
40+
"""Prints all information of the thrust vector actuator."""
41+
self.basics()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from rocketpy.rocket.actuator.actuator import Actuator
2+
from rocketpy.rocket.actuator.roll import RollActuator
3+
from rocketpy.rocket.actuator.throttle import ThrottleActuator
4+
from rocketpy.rocket.actuator.thrust_vector import ThrustVectorActuator
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from abc import ABC, abstractmethod
2+
3+
import numpy as np
4+
5+
6+
class Actuator(ABC):
7+
"""Abstract class used to define actuators.
8+
9+
Actuators are used to model the dynamics of control systems such as
10+
throttle, thrust vector, and roll control. They can be used to simulate the response of
11+
the control system to changes in throttle, thrust vector, or roll torque commands."""
12+
13+
def __init__(
14+
self,
15+
name,
16+
demand_rate=None,
17+
actuator_range=(-np.inf, np.inf),
18+
actuator_rate_limit=None,
19+
clamp=True,
20+
actuator_initial_output=0.0,
21+
actuator_time_constant=None,
22+
):
23+
"""Initializes the Actuator class.
24+
25+
Parameters
26+
----------
27+
name : str
28+
Name of the actuator.
29+
demand_rate : float, optional
30+
Demand rate (Hz) of the actuator. Default is None for continuous-time actuator.
31+
actuator_range : tuple, optional
32+
Range of the actuator output. Default is (-np.Inf, np.Inf).
33+
actuator_rate_limit : float, optional
34+
Rate limit of the actuator per second. Default is None.
35+
clamp : bool, optional
36+
Whether to clamp the actuator output. Default is True.
37+
actuator_initial_output : float, optional
38+
Initial output of the actuator. Default is 0.0.
39+
actuator_time_constant : float, optional
40+
Time constant of the actuator, implemented as a discrete IIR filter. Default is None.
41+
42+
Returns
43+
-------
44+
None
45+
"""
46+
47+
self.name = name
48+
49+
assert demand_rate > 0 or demand_rate is None, (
50+
"demand_rate must be positive or None."
51+
)
52+
self.demand_rate = demand_rate
53+
54+
assert actuator_range[0] <= actuator_range[1], (
55+
"actuator_range[0] must be <= actuator_range[1]."
56+
)
57+
self.actuator_range = actuator_range
58+
59+
assert actuator_rate_limit is None or actuator_rate_limit >= 0, (
60+
"actuator_rate_limit must be non-negative or None."
61+
)
62+
self.actuator_rate_limit = actuator_rate_limit
63+
64+
self.clamp = clamp
65+
66+
assert actuator_time_constant is None or actuator_time_constant >= 0, (
67+
"actuator_time_constant must be non-negative or None."
68+
)
69+
self.actuator_time_constant = actuator_time_constant
70+
self._update_iir_coefficients()
71+
72+
self.actuator_initial_output = actuator_initial_output
73+
self._actuator_output = actuator_initial_output
74+
75+
def _update_iir_coefficients(self):
76+
"""Updates the IIR filter coefficient based on time constant and
77+
demand rate. Uses first-order discrete-time system:
78+
y[n] = alpha * u[n] + (1 - alpha) * y[n-1]
79+
where alpha = Ts / (tau + Ts)
80+
"""
81+
82+
if self.actuator_time_constant is not None and self.actuator_time_constant > 0:
83+
if self.demand_rate is not None:
84+
demand_period = 1.0 / self.demand_rate
85+
self._alpha = demand_period / (
86+
self.actuator_time_constant + demand_period
87+
)
88+
else:
89+
print(
90+
f"Warning: Actuator time constant currently only implemented on discrete controllers. '{self.name}' dynamics not applied."
91+
)
92+
self._alpha = 1.0 # No filtering, direct pass-through
93+
else:
94+
self._alpha = 1.0 # No filtering, direct pass-through
95+
96+
@property
97+
def actuator_output(self):
98+
return self._actuator_output
99+
100+
@actuator_output.setter
101+
def actuator_output(self, value):
102+
"""Sets the actuator output with optional clamping or warning.
103+
104+
Parameters
105+
----------
106+
value : float
107+
Desired actuator output.
108+
109+
Returns
110+
-------
111+
None
112+
"""
113+
# Apply first-order IIR actuator dynamics
114+
value = self._alpha * value + (1 - self._alpha) * self._actuator_output
115+
116+
# Apply rate limit if specified
117+
if self.actuator_rate_limit is not None:
118+
if self.demand_rate is not None:
119+
max_change = self.actuator_rate_limit / self.demand_rate
120+
change = value - self._actuator_output
121+
if abs(change) > max_change:
122+
value = self._actuator_output + np.sign(change) * max_change
123+
print(
124+
f"Warning: Actuator '{self.name}' output change {change:.3f} exceeds rate limit of {max_change:.3f} per time step."
125+
)
126+
else:
127+
print(
128+
f"Warning: Actuator rate limit currently only implemented for discrete controllers. '{self.name}' rate limit not applied."
129+
)
130+
131+
# Apply range limits if specified
132+
if self.clamp:
133+
value = np.clip(value, self.actuator_range[0], self.actuator_range[1])
134+
else:
135+
if value < self.actuator_range[0] or value > self.actuator_range[1]:
136+
print(
137+
f"Warning: Actuator '{self.name}' output {value:.3f} exceeds range limits {self.actuator_range}."
138+
)
139+
140+
self._actuator_output = value
141+
142+
def _reset(self):
143+
"""Resets the actuator to its initial state. This method
144+
is called at the beginning of each simulation to ensure the actuator
145+
is in the correct state."""
146+
self._actuator_output = self.actuator_initial_output
147+
148+
@abstractmethod
149+
def info(self):
150+
"""Prints summarized information of the actuator.
151+
152+
Returns
153+
-------
154+
None
155+
"""
156+
157+
@abstractmethod
158+
def all_info(self):
159+
"""Prints all information of the actuator.
160+
161+
Returns
162+
-------
163+
None
164+
"""

0 commit comments

Comments
 (0)