STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
actuation.py
Go to the documentation of this file.
1"""Actuation command generation for robust DDP controller.
2
3Converts relaxed controls u~ in [0,1] to actual actuation commands:
4- Quantized duty levels
5- Dwell time enforcement
6- PWM or binary execution backends
7"""
8
9from __future__ import annotations
10
11from typing import Optional, Literal
12from dataclasses import dataclass
13from enum import Enum
14import numpy as np
15
16from .data_models import ControllerConfig, ControllerState
17from .dynamics import N_CONTROL, IDX_U_F, IDX_U_O
18
19
20class ExecutionBackend(Enum):
21 """Execution backend type."""
22 PWM = "pwm" # Pulse-width modulation
23 BINARY = "binary" # Pure binary (on/off)
24
25
26@dataclass
28 """Actuation command output."""
29 # Binary on/off commands (for binary backend or thresholding)
30 u_F_onoff: bool # Fuel solenoid on/off
31 u_O_onoff: bool # Oxidizer solenoid on/off
32
33 # Duty cycle commands (for PWM backend)
34 duty_F: float # Fuel duty cycle [0, 1]
35 duty_O: float # Oxidizer duty cycle [0, 1]
36
37 # Quantized relaxed controls (for reference)
38 u_F_quantized: float # Quantized fuel control [0, 1]
39 u_O_quantized: float # Quantized oxidizer control [0, 1]
40
41 # Backend used
42 backend: ExecutionBackend
43
44 # Dwell timer updates (for state update)
45 dwell_timer_F: float # Updated fuel dwell timer [s]
46 dwell_timer_O: float # Updated oxidizer dwell timer [s]
47
48
50 u_relaxed: np.ndarray,
51 state: ControllerState,
52 cfg: ControllerConfig,
53 dt: Optional[float] = None,
54 backend: ExecutionBackend = ExecutionBackend.PWM,
55 u_prev_relaxed: Optional[np.ndarray] = None,
56 sigma_delta_state: Optional[dict[str, float]] = None,
57) -> ActuationCommand:
58 """
59 Convert relaxed controls to actuation commands.
60
61 Parameters:
62 -----------
63 u_relaxed : np.ndarray, shape (N_CONTROL,)
64 Relaxed control vector [u_F, u_O] in [0, 1]
65 state : ControllerState
66 Controller state (for dwell timers)
67 cfg : ControllerConfig
68 Controller configuration
69 dt : float, optional
70 Time step [s] (defaults to cfg.dt)
71 backend : ExecutionBackend
72 Execution backend (PWM or BINARY)
73 u_prev_relaxed : np.ndarray, optional, shape (N_CONTROL,)
74 Previous relaxed control (for dwell enforcement)
75 If None, uses state.u_prev
76
77 Returns:
78 --------
79 cmd : ActuationCommand
80 Actuation command with duty cycles and on/off states
81 """
82 if dt is None:
83 dt = cfg.dt
84
85 # Validate input
86 if u_relaxed.shape != (N_CONTROL,):
87 raise ValueError(f"u_relaxed must have shape ({N_CONTROL},), got {u_relaxed.shape}")
88
89 u_F = float(np.clip(u_relaxed[IDX_U_F], 0.0, 1.0))
90 u_O = float(np.clip(u_relaxed[IDX_U_O], 0.0, 1.0))
91
92 # Get previous controls for dwell enforcement
93 if u_prev_relaxed is not None:
94 u_F_prev = float(np.clip(u_prev_relaxed[IDX_U_F], 0.0, 1.0))
95 u_O_prev = float(np.clip(u_prev_relaxed[IDX_U_O], 0.0, 1.0))
96 else:
97 # Extract from state (legacy format: P_u_fuel, P_u_ox)
98 # For now, assume these are normalized [0, 1] or convert
99 u_F_prev = state.u_prev.get("P_u_fuel", 0.0)
100 u_O_prev = state.u_prev.get("P_u_ox", 0.0)
101 # Normalize if needed (assuming max is 1.0 for now)
102 u_F_prev = float(np.clip(u_F_prev, 0.0, 1.0))
103 u_O_prev = float(np.clip(u_O_prev, 0.0, 1.0))
104
105 # Get current dwell timers
106 dwell_timer_F = state.dwell_timers.get("P_u_fuel", 0.0)
107 dwell_timer_O = state.dwell_timers.get("P_u_ox", 0.0)
108
109 # Step 1: Quantize to duty grid
110 u_F_quantized = quantize_duty(u_F, cfg.duty_quantization)
111 u_O_quantized = quantize_duty(u_O, cfg.duty_quantization)
112
113 # Step 2: Enforce dwell time
114 u_F_dwell, dwell_timer_F_new = enforce_dwell(
115 u_F_quantized, u_F_prev, dwell_timer_F, cfg.dwell_time, dt
116 )
117 u_O_dwell, dwell_timer_O_new = enforce_dwell(
118 u_O_quantized, u_O_prev, dwell_timer_O, cfg.dwell_time, dt
119 )
120
121 # Step 3: Generate actuation command based on backend
122 if backend == ExecutionBackend.PWM:
123 duty_F = u_F_dwell
124 duty_O = u_O_dwell
125 u_F_onoff = duty_F > 0.0
126 u_O_onoff = duty_O > 0.0
127 elif backend == ExecutionBackend.BINARY:
128 # Binary backend: use sigma-delta modulation or threshold
129 sd_state_F = sigma_delta_state.get("F", None) if sigma_delta_state else None
130 sd_state_O = sigma_delta_state.get("O", None) if sigma_delta_state else None
131 duty_F, u_F_onoff = binary_actuation(u_F_dwell, dt, cfg, sd_state_F)
132 duty_O, u_O_onoff = binary_actuation(u_O_dwell, dt, cfg, sd_state_O)
133 else:
134 raise ValueError(f"Unknown backend: {backend}")
135
136 return ActuationCommand(
137 u_F_onoff=u_F_onoff,
138 u_O_onoff=u_O_onoff,
139 duty_F=duty_F,
140 duty_O=duty_O,
141 u_F_quantized=u_F_quantized,
142 u_O_quantized=u_O_quantized,
143 backend=backend,
144 dwell_timer_F=dwell_timer_F_new,
145 dwell_timer_O=dwell_timer_O_new,
146 )
147
148
149def quantize_duty(u: float, duty_quantization: float) -> float:
150 """
151 Quantize control to duty grid.
152
153 Parameters:
154 -----------
155 u : float
156 Control value in [0, 1]
157 duty_quantization : float
158 Duty quantization step (e.g., 0.01 for 1% steps)
159
160 Returns:
161 --------
162 u_quantized : float
163 Quantized control value
164 """
165 # Clamp to [0, 1]
166 u = np.clip(u, 0.0, 1.0)
167
168 # Quantize to grid: round to nearest multiple of duty_quantization
169 n_steps = round(u / duty_quantization)
170 u_quantized = n_steps * duty_quantization
171
172 # Clamp again to ensure in [0, 1]
173 u_quantized = np.clip(u_quantized, 0.0, 1.0)
174
175 return float(u_quantized)
176
177
179 u_new: float,
180 u_prev: float,
181 dwell_timer: float,
182 dwell_time: float,
183 dt: float,
184) -> tuple[float, float]:
185 """
186 Enforce minimum dwell time between control changes.
187
188 If control changed, reset timer. If timer < dwell_time, keep previous control.
189
190 Parameters:
191 -----------
192 u_new : float
193 New control value
194 u_prev : float
195 Previous control value
196 dwell_timer : float
197 Current dwell timer [s]
198 dwell_time : float
199 Minimum dwell time [s]
200 dt : float
201 Time step [s]
202
203 Returns:
204 --------
205 u_output : float
206 Output control (may be u_prev if dwell not satisfied)
207 dwell_timer_new : float
208 Updated dwell timer [s]
209 """
210 # Check if control changed (with tolerance for quantization)
211 control_changed = abs(u_new - u_prev) > 1e-6
212
213 if control_changed:
214 # Control changed: check if dwell time satisfied
215 if dwell_timer >= dwell_time:
216 # Dwell satisfied: allow change
217 return u_new, 0.0 # Reset timer
218 else:
219 # Dwell not satisfied: keep previous control
220 dwell_timer_new = dwell_timer + dt
221 return u_prev, dwell_timer_new
222 else:
223 # Control unchanged: increment timer
224 dwell_timer_new = dwell_timer + dt
225 return u_new, dwell_timer_new
226
227
229 duty_desired: float,
230 dt: float,
231 cfg: ControllerConfig,
232 sigma_delta_state: Optional[float] = None,
233) -> tuple[float, bool]:
234 """
235 Generate binary actuation command using sigma-delta modulation.
236
237 Sigma-delta modulation provides average duty matching without high-frequency chatter.
238
239 Parameters:
240 -----------
241 duty_desired : float
242 Desired duty cycle [0, 1]
243 dt : float
244 Time step [s]
245 cfg : ControllerConfig
246 Controller configuration
247 sigma_delta_state : float, optional
248 Internal sigma-delta state (accumulator)
249 If None, uses threshold method instead
250
251 Returns:
252 --------
253 duty_output : float
254 Output duty (0 or 1 for binary)
255 onoff : bool
256 Binary on/off command
257 """
258 if sigma_delta_state is None:
259 # Simple threshold method (no sigma-delta)
260 # Use threshold: if duty > 0.5, turn on
261 threshold = getattr(cfg, 'binary_threshold', 0.5)
262 onoff = duty_desired > threshold
263 duty_output = 1.0 if onoff else 0.0
264 return duty_output, onoff
265 else:
266 # Sigma-delta modulation (first-order)
267 # Algorithm:
268 # 1. accumulator += duty_desired
269 # 2. output = 1 if accumulator >= 1, else 0
270 # 3. accumulator -= output
271
272 accumulator = sigma_delta_state + duty_desired
273
274 # Generate output
275 if accumulator >= 1.0:
276 onoff = True
277 duty_output = 1.0
278 accumulator -= 1.0
279 else:
280 onoff = False
281 duty_output = 0.0
282
283 return duty_output, onoff
284
285
287 state: ControllerState,
288 cmd: ActuationCommand,
289) -> None:
290 """
291 Update dwell timers in controller state.
292
293 Parameters:
294 -----------
295 state : ControllerState
296 Controller state to update (mutated)
297 cmd : ActuationCommand
298 Actuation command with updated dwell timers
299 """
300 state.dwell_timers["P_u_fuel"] = cmd.dwell_timer_F
301 state.dwell_timers["P_u_ox"] = cmd.dwell_timer_O
302
303 # Also update u_prev with quantized values
304 state.u_prev["P_u_fuel"] = cmd.u_F_quantized
305 state.u_prev["P_u_ox"] = cmd.u_O_quantized
306
307
308def create_duty_grid(duty_quantization: float) -> np.ndarray:
309 """
310 Create duty cycle grid.
311
312 Parameters:
313 -----------
314 duty_quantization : float
315 Duty quantization step
316
317 Returns:
318 --------
319 grid : np.ndarray
320 Duty grid: [0, step, 2*step, ..., 1.0]
321 """
322 n_steps = int(1.0 / duty_quantization) + 1
323 grid = np.linspace(0.0, 1.0, n_steps)
324 return grid
325
np.ndarray create_duty_grid(float duty_quantization)
Definition actuation.py:308
None update_state_dwell_timers(ControllerState state, ActuationCommand cmd)
Definition actuation.py:289
tuple[float, bool] binary_actuation(float duty_desired, float dt, ControllerConfig cfg, Optional[float] sigma_delta_state=None)
Definition actuation.py:233
ActuationCommand compute_actuation(np.ndarray u_relaxed, ControllerState state, ControllerConfig cfg, Optional[float] dt=None, ExecutionBackend backend=ExecutionBackend.PWM, Optional[np.ndarray] u_prev_relaxed=None, Optional[dict[str, float]] sigma_delta_state=None)
Definition actuation.py:57
tuple[float, float] enforce_dwell(float u_new, float u_prev, float dwell_timer, float dwell_time, float dt)
Definition actuation.py:184
float quantize_duty(float u, float duty_quantization)
Definition actuation.py:149