STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
dynamics.py
Go to the documentation of this file.
1"""Discrete-time dynamics model for robust DDP controller.
2
3State vector: x = [P_copv, P_reg, P_u_F, P_u_O, P_d_F, P_d_O, V_u_F, V_u_O, m_gas_copv, m_gas_F, m_gas_O]
4Control: u = [u_F, u_O] in [0, 1]
5
6Gas masses are tracked for proper mass conservation and blowdown physics.
7"""
8
9from __future__ import annotations
10
11from typing import Tuple, Optional, Dict, Any
12import numpy as np
13from dataclasses import dataclass
14
15from .data_models import ControllerConfig
16
17
18# State indices
19IDX_P_COPV = 0
20IDX_P_REG = 1
21IDX_P_U_F = 2
22IDX_P_U_O = 3
23IDX_P_D_F = 4
24IDX_P_D_O = 5
25IDX_V_U_F = 6
26IDX_V_U_O = 7
27IDX_M_GAS_COPV = 8 # Gas mass in COPV [kg]
28IDX_M_GAS_F = 9 # Gas mass in fuel tank ullage [kg]
29IDX_M_GAS_O = 10 # Gas mass in oxidizer tank ullage [kg]
30
31# Control indices
32IDX_U_F = 0
33IDX_U_O = 1
34
35N_STATE = 11 # Expanded to include gas masses
36N_CONTROL = 2
37
38
39@dataclass
41 """Parameters for dynamics model (extracted from ControllerConfig)."""
42 # Required parameters (no defaults)
43 copv_cF: float
44 copv_cO: float
45 copv_loss: float
46 reg_ratio: float
47 alpha_F: float # Pressurization coefficient for fuel
48 alpha_O: float # Pressurization coefficient for oxidizer
49 rho_F: float # Propellant density for fuel
50 rho_O: float # Propellant density for oxidizer
51 tau_line_F: float # Feed line time constant for fuel
52 tau_line_O: float # Feed line time constant for oxidizer
53
54 # Optional parameters with defaults (must come after required fields)
55 reg_setpoint: Optional[float] = None # Regulator setpoint [Pa] (None = derived from COPV)
56 V_copv: float = 0.006 # COPV volume [m³] (default 6L)
57 R_gas: float = 296.8 # Gas constant [J/(kg·K)] for N2
58 T_gas: float = 293.0 # Initial gas temperature [K]
59 Z_gas: float = 1.0 # Compressibility factor (1.0 for ideal gas, can be pressure-dependent)
60 n_polytropic: float = 1.2 # Polytropic exponent (1.0=isothermal, 1.4=adiabatic, 1.2=typical for blowdown)
61 use_polytropic: bool = True # Use polytropic process (temperature changes) vs isothermal
62 use_real_gas: bool = False # Use real gas compressibility factor Z(P,T) vs ideal gas
63 T_gas_copv_initial: float = 293.0 # Initial COPV gas temperature [K]
64 T_gas_F_initial: float = 293.0 # Initial fuel tank gas temperature [K]
65 T_gas_O_initial: float = 250.0 # Initial oxidizer tank gas temperature [K] (LOX tank is colder)
66
67 # --- Pressurisation-path hardware ------------------------------------------------------
68 # These were literals inside step(): a COPV -> regulator -> tank chain with the orifice
69 # geometry baked into the function body, which is one of the four fragmented feed models
70 # docs/adr/0001 calls out. They are real hardware numbers a user must be able to set once
71 # this path is calibrated or replaced by lib/feedtwin. Defaults are exactly the values that
72 # were hardcoded, so behaviour is unchanged unless someone overrides them.
73 gamma_gas: float = 1.4 # Specific heat ratio of the pressurant (N2)
74 Cd_regulator: float = 0.7 # Discharge coefficient, regulator orifice
75 Cd_valve: float = 0.65 # Discharge coefficient, solenoid valve orifice
76 A_regulator: float = 2e-5 # Regulator orifice area [m^2] (~20 mm^2)
77 A_valve_F: float = 5e-5 # Fuel solenoid valve flow area [m^2] (~50 mm^2)
78 A_valve_O: float = 5e-5 # Oxidiser solenoid valve flow area [m^2] (~50 mm^2)
79
80 @classmethod
81 def from_config(cls, config: ControllerConfig) -> DynamicsParams:
82 """Create dynamics parameters from controller config."""
83 return cls(
84 copv_cF=config.copv_cF,
85 copv_cO=config.copv_cO,
86 copv_loss=config.copv_loss,
87 V_copv=getattr(config, 'V_copv', 0.006), # Default 6L
88 R_gas=getattr(config, 'R_gas', 296.8), # N2 gas constant
89 T_gas=getattr(config, 'T_gas', 293.0), # Initial gas temperature
90 Z_gas=getattr(config, 'Z_gas', 1.0), # Ideal gas (can be pressure-dependent)
91 reg_setpoint=config.reg_setpoint,
92 reg_ratio=config.reg_ratio,
93 alpha_F=config.alpha_F,
94 alpha_O=config.alpha_O,
95 rho_F=config.rho_F,
96 rho_O=config.rho_O,
97 tau_line_F=config.tau_line_F,
98 tau_line_O=config.tau_line_O,
99 # Real-world physics parameters
100 n_polytropic=getattr(config, 'n_polytropic', 1.2), # Typical for blowdown
101 use_polytropic=getattr(config, 'use_polytropic', True), # Enable polytropic processes
102 use_real_gas=getattr(config, 'use_real_gas', False), # Real gas Z(P,T) lookup
103 T_gas_copv_initial=getattr(config, 'T_gas_copv_initial', 293.0), # COPV initial temp
104 T_gas_F_initial=getattr(config, 'T_gas_F_initial', 293.0), # Fuel tank initial temp
105 T_gas_O_initial=getattr(config, 'T_gas_O_initial', 250.0), # LOX tank initial temp (colder)
106 # Pressurisation-path hardware; getattr so an older ControllerConfig still loads.
107 gamma_gas=getattr(config, 'gamma_gas', 1.4),
108 Cd_regulator=getattr(config, 'Cd_regulator', 0.7),
109 Cd_valve=getattr(config, 'Cd_valve', 0.65),
110 A_regulator=getattr(config, 'A_regulator', 2e-5),
111 A_valve_F=getattr(config, 'A_valve_F', 5e-5),
112 A_valve_O=getattr(config, 'A_valve_O', 5e-5),
113 )
114
115
117 x: np.ndarray,
118 u: np.ndarray,
119 dt: float,
120 params: DynamicsParams,
121 mdot_F: float,
122 mdot_O: float,
123) -> np.ndarray:
124 """
125 Discrete-time dynamics step.
126
127 Parameters:
128 -----------
129 x : np.ndarray, shape (N_STATE,)
130 Current state: [P_copv, P_reg, P_u_F, P_u_O, P_d_F, P_d_O, V_u_F, V_u_O, m_gas_copv, m_gas_F, m_gas_O]
131 u : np.ndarray, shape (N_CONTROL,)
132 Control input: [u_F, u_O] in [0, 1]
133 dt : float
134 Time step [s]
135 params : DynamicsParams
136 Dynamics parameters
137 mdot_F : float
138 Fuel mass flow rate [kg/s] (from engine physics)
139 mdot_O : float
140 Oxidizer mass flow rate [kg/s] (from engine physics)
141
142 Returns:
143 --------
144 x_next : np.ndarray, shape (N_STATE,)
145 Next state
146 """
147 # Validate input state size
148 if len(x) < 8:
149 raise ValueError(f"State vector must have at least 8 elements, got {len(x)}")
150
151 # Extract state
152 P_copv = x[IDX_P_COPV]
153 P_reg = x[IDX_P_REG]
154 P_u_F = x[IDX_P_U_F]
155 P_u_O = x[IDX_P_U_O]
156 P_d_F = x[IDX_P_D_F]
157 P_d_O = x[IDX_P_D_O]
158 V_u_F = x[IDX_V_U_F]
159 V_u_O = x[IDX_V_U_O]
160
161 # Extract gas masses (if not in state, compute from pressures using ideal gas law)
162 if len(x) > IDX_M_GAS_COPV:
163 m_gas_copv = x[IDX_M_GAS_COPV]
164 m_gas_F = x[IDX_M_GAS_F]
165 m_gas_O = x[IDX_M_GAS_O]
166 else:
167 # Initialize gas masses from pressures (backward compatibility)
168 # P = (m * R * T) / (V * Z), so m = (P * V * Z) / (R * T)
169 T_copv_init = getattr(params, 'T_gas_copv_initial', params.T_gas)
170 T_F_init = getattr(params, 'T_gas_F_initial', params.T_gas)
171 T_O_init = getattr(params, 'T_gas_O_initial', 250.0) # LOX tank is colder
172 m_gas_copv = (P_copv * params.V_copv * params.Z_gas) / (params.R_gas * T_copv_init)
173 m_gas_F = (P_u_F * V_u_F * params.Z_gas) / (params.R_gas * T_F_init) if V_u_F > 1e-10 else 0.0
174 m_gas_O = (P_u_O * V_u_O * params.Z_gas) / (params.R_gas * T_O_init) if V_u_O > 1e-10 else 0.0
175
176 # REAL-WORLD PHYSICS: Track gas temperatures for polytropic processes
177 # In real systems, gas temperature changes during expansion/compression
178 # Polytropic process: T = T0 * (rho/rho0)^(n-1) where rho = m/V
179 # n = 1.0: isothermal (constant T)
180 # n = 1.4: adiabatic (no heat transfer, γ for diatomic gas)
181 # n = 1.2: typical for blowdown (some heat transfer with tank walls)
182
183 # Reference state for the polytropic relation, captured on this params object's first
184 # step. It used to live on the FUNCTION object as private attributes, which made it
185 # process-global: the first trajectory ever stepped set the reference for every later
186 # one, across configs. An optimizer evaluating thousands of candidates -- exactly the
187 # Layer X access pattern docs/adr/0001 describes -- would hand candidates 2..N the
188 # reference state of candidate 1. Scoping it to `params` makes it per-config.
189 if not getattr(params, '_temp_initialized', False):
190 params._T_copv_0 = getattr(params, 'T_gas_copv_initial', params.T_gas)
191 params._T_F_0 = getattr(params, 'T_gas_F_initial', params.T_gas)
192 params._T_O_0 = getattr(params, 'T_gas_O_initial', 250.0) # LOX tank colder
193 params._m_copv_0 = m_gas_copv
194 params._m_F_0 = m_gas_F
195 params._m_O_0 = m_gas_O
196 params._V_F_0 = V_u_F
197 params._V_O_0 = V_u_O
198 params._temp_initialized = True
199
200 # Compute current gas temperatures using polytropic relation
201 # T = T0 * (rho/rho0)^(n-1) = T0 * (m/V) / (m0/V0))^(n-1)
202 # This accounts for both volume expansion and mass changes
203 use_polytropic = getattr(params, 'use_polytropic', True)
204 n_poly = getattr(params, 'n_polytropic', 1.2)
205
206 if use_polytropic:
207 # COPV temperature: polytropic expansion/compression
208 if params._m_copv_0 > 1e-10 and params.V_copv > 1e-10:
209 rho_copv_0 = params._m_copv_0 / params.V_copv
210 rho_copv = m_gas_copv / params.V_copv if params.V_copv > 1e-10 else rho_copv_0
211 if rho_copv_0 > 1e-10:
212 T_copv = params._T_copv_0 * (rho_copv / rho_copv_0) ** (n_poly - 1.0)
213 T_copv = max(200.0, min(400.0, T_copv)) # Clamp to reasonable range [200-400 K]
214 else:
215 T_copv = params._T_copv_0
216 else:
217 T_copv = params._T_copv_0
218
219 # Fuel tank temperature: polytropic expansion/compression
220 if params._m_F_0 > 1e-10 and params._V_F_0 > 1e-10:
221 rho_F_0 = params._m_F_0 / params._V_F_0
222 rho_F = m_gas_F / V_u_F if V_u_F > 1e-10 else rho_F_0
223 if rho_F_0 > 1e-10:
224 T_gas_F = params._T_F_0 * (rho_F / rho_F_0) ** (n_poly - 1.0)
225 T_gas_F = max(200.0, min(400.0, T_gas_F)) # Clamp to reasonable range
226 else:
227 T_gas_F = params._T_F_0
228 else:
229 T_gas_F = params._T_F_0
230
231 # Oxidizer tank temperature: polytropic expansion/compression
232 if params._m_O_0 > 1e-10 and params._V_O_0 > 1e-10:
233 rho_O_0 = params._m_O_0 / params._V_O_0
234 rho_O = m_gas_O / V_u_O if V_u_O > 1e-10 else rho_O_0
235 if rho_O_0 > 1e-10:
236 T_gas_O = params._T_O_0 * (rho_O / rho_O_0) ** (n_poly - 1.0)
237 T_gas_O = max(200.0, min(400.0, T_gas_O)) # Clamp to reasonable range
238 else:
239 T_gas_O = params._T_O_0
240 else:
241 T_gas_O = params._T_O_0
242 else:
243 # Isothermal process: temperature constant
244 T_copv = params._T_copv_0
245 T_gas_F = params._T_F_0
246 T_gas_O = params._T_O_0
247
248 # Extract control
249 u_F = np.clip(u[IDX_U_F], 0.0, 1.0)
250 u_O = np.clip(u[IDX_U_O], 0.0, 1.0)
251
252 # 1. Compute gas mass flow rates using PHYSICALLY ACCURATE choked flow equations
253 # Flow path: COPV -> Regulator -> Tanks (when valves open)
254 # Model: Compressible gas flow through orifices with proper choked/subsonic flow
255
256 # Pressurant properties and orifice geometry now come from DynamicsParams rather than
257 # being literals here -- same defaults, but settable per vehicle. See docs/adr/0001: this
258 # COPV -> regulator -> tank chain is one of the feed models lib/feedtwin absorbs.
259 gamma_gas = params.gamma_gas
260 Cd_regulator = params.Cd_regulator
261 Cd_valve = params.Cd_valve
262
263 A_regulator = params.A_regulator
264 A_valve_F = params.A_valve_F
265 A_valve_O = params.A_valve_O
266
267 # Gas flow from COPV to regulator [kg/s]
268 # CRITICAL: Flow ONLY happens when at least one valve is open (u > 0)
269 # When both valves are closed (u_F = 0, u_O = 0), NO flow from COPV to regulator
270 # This is the key: control gates the flow - no control = no flow = pure blowdown
271 u_total = max(u_F, u_O) # Flow happens if either valve is open
272
273 # Regulator can only supply flow if COPV pressure is above regulator setpoint
274 # Use current COPV pressure (P_copv) for flow calculation, not next (P_copv_next)
275 # P_copv_next will be calculated later after we determine the flow
276 if params.reg_setpoint is not None:
277 # Regulator maintains setpoint as long as COPV > setpoint
278 reg_output_pressure = params.reg_setpoint
279 can_supply = P_copv >= params.reg_setpoint
280 else:
281 # No setpoint: use ratio-based model
282 reg_output_pressure = min(P_copv, P_reg)
283 can_supply = P_copv > P_reg
284
285 # CRITICAL: Only flow if at least one valve is open AND regulator can supply
286 # If u_total = 0 (both valves closed), mdot_gas_to_reg = 0 (no flow)
287 if u_total > 1e-6 and can_supply:
288 # PHYSICALLY ACCURATE: Use choked flow equation for compressible gas
289 # Flow from COPV to regulator through regulator orifice
290 # Regulator maintains fixed output at setpoint, so downstream pressure is setpoint
291 if params.reg_setpoint is not None:
292 P_downstream = params.reg_setpoint # Regulator output pressure
293 else:
294 P_downstream = P_reg # Fallback to current regulator pressure
295
296 # Calculate compressible gas flow using proper choked flow equation
297 # Critical pressure ratio for choked flow
298 pr_crit = (2.0 / (gamma_gas + 1.0)) ** (gamma_gas / (gamma_gas - 1.0))
299 pr_ratio = P_downstream / P_copv if P_copv > 0 else 0.0
300
301 if pr_ratio < pr_crit:
302 # CHOKED FLOW: Flow is sonic at orifice
303 # mdot = Cd * A * P_up * sqrt(gamma/(R*T)) * (2/(gamma+1))^((gamma+1)/(2(gamma-1)))
304 C_star = np.sqrt(gamma_gas * (2.0 / (gamma_gas + 1.0)) ** ((gamma_gas + 1.0) / (gamma_gas - 1.0)))
305 mdot_gas_to_reg_max = Cd_regulator * A_regulator * P_copv / np.sqrt(params.R_gas * params.T_gas) * C_star
306 else:
307 # SUBSONIC FLOW: Flow is subsonic
308 # mdot = Cd * A * P_up * sqrt(2*gamma/((gamma-1)RT)) * sqrt(pr^(2/gamma) - pr^((gamma+1)/gamma))
309 # Use actual COPV temperature (not constant T_gas)
310 term1 = (2.0 * gamma_gas) / ((gamma_gas - 1.0) * params.R_gas * T_copv)
311 term2 = pr_ratio ** (2.0 / gamma_gas) - pr_ratio ** ((gamma_gas + 1.0) / gamma_gas)
312 mdot_gas_to_reg_max = Cd_regulator * A_regulator * P_copv * np.sqrt(term1 * term2)
313
314 # Scale by control input (u_total) - flow is gated by valve opening
315 # When u_total = 0, no flow; when u_total = 1, full flow
316 mdot_gas_to_reg = mdot_gas_to_reg_max * u_total
317
318 # Clamp to reasonable maximum and ensure non-negative
319 mdot_gas_to_reg = max(0.0, min(mdot_gas_to_reg, 0.2)) # [kg/s] - reasonable max for COPV
320 else:
321 mdot_gas_to_reg = 0.0
322
323 # Gas flow from regulator to tanks [kg/s]
324 # Split flow based on control inputs and pressure differences
325 # When u_F > 0 and P_reg > P_u_F: gas flows to fuel tank
326 # Flow rate: proportional to control input and pressure difference
327 # CRITICAL: Flow depends on pressure difference, not ullage volume
328 # Ullage volume affects pressure (P = m*R*T/V), but flow is driven by delta_P
329 # Use alpha_F and alpha_O as pressurization rate coefficients [1/s per unit pressure]
330 # Convert to mass flow: mdot = alpha * u * (P_reg - P_u) * effective_volume / (R*T)
331 # For choked flow: mdot ~ sqrt(delta_P), but linear approximation works for small delta_P
332 # Gas flow from regulator to tanks [kg/s]
333 # CRITICAL: This is where control actually affects the system!
334 # When u > 0, gas flows from regulator to tanks, increasing tank pressure
335 # Flow is proportional to control input and pressure difference
336 # alpha_F and alpha_O are pressurization rate coefficients [1/s per unit pressure]
337 # For choked flow through orifice: mdot = Cd * A * P_up * sqrt(gamma/(R*T)) * constant
338 # Linear approximation: mdot ~ alpha * u * (P_reg - P_u) * effective_area / (R*T)
339 # Use a reasonable effective flow area based on typical solenoid valve characteristics
340 # Typical solenoid: A ~ 1e-5 m² (10 mm²), Cd ~ 0.6-0.8
341 # For choked flow: mdot_max ~ 0.01-0.1 kg/s for typical pressures
342 # Scale alpha to give reasonable flow rates
343
344 # Gas flow from regulator to tanks [kg/s]
345 # CRITICAL: Model discrete PWM valve behavior to create spikes/ripple
346 # PWM means valve is either FULLY OPEN (u=1) or FULLY CLOSED (u=0), switching rapidly
347 # Duty cycle determines fraction of time valve is open
348 # To see spikes, model valve as binary: ON or OFF based on duty cycle
349 # Simple model: valve is ON if duty cycle > 0.5 (50%), OFF otherwise
350 # This creates discrete switching behavior and pressure spikes
351
352 # Gas flow from regulator to tanks [kg/s]
353 # CRITICAL: Model discrete PWM valve behavior to create spikes/ripple
354 # PWM means valve switches between fully open and fully closed
355 # Duty cycle = fraction of time valve is open
356 # To create visible spikes, model valve as binary (ON/OFF) with aggressive response
357 # When valve opens, pressure spikes up immediately
358 # When valve closes, pressure decays
359
360 # Model valve as binary based on duty cycle
361 # For realistic PWM: valve is ON for duty_cycle fraction of time, OFF otherwise
362 # To see spikes: use duty cycle directly but make flow response very aggressive
363 # Higher duty cycle = more time valve is open = more flow = higher pressure
364
365 # Use regulator output pressure for flow calculation
366 # CRITICAL: When COPV >= setpoint, regulator outputs at setpoint
367 # When COPV < setpoint, regulator output drops with COPV
368 # Use the actual regulator output pressure (which we'll compute, but for now use current logic)
369 if params.reg_setpoint is not None:
370 if P_copv >= params.reg_setpoint:
371 P_reg_effective = params.reg_setpoint # Regulator maintains setpoint
372 else:
373 P_reg_effective = P_copv # Regulator output limited by COPV (drops below setpoint)
374 else:
375 P_reg_effective = P_reg # Fallback to current regulator pressure
376
377 # PHYSICALLY ACCURATE: Gas flow from regulator to fuel tank through solenoid valve
378 if u_F > 1e-6 and P_reg_effective > P_u_F:
379 # Use proper choked flow equation for compressible gas through valve orifice
380 pr_ratio_F = P_u_F / P_reg_effective if P_reg_effective > 0 else 0.0
381 pr_crit = (2.0 / (gamma_gas + 1.0)) ** (gamma_gas / (gamma_gas - 1.0))
382
383 if pr_ratio_F < pr_crit:
384 # CHOKED FLOW: Flow is sonic at valve
385 # Use regulator temperature (assume same as COPV for now, or could use T_reg)
386 T_reg_flow = T_copv # Regulator output temperature (close to COPV temp)
387 C_star = np.sqrt(gamma_gas * (2.0 / (gamma_gas + 1.0)) ** ((gamma_gas + 1.0) / (gamma_gas - 1.0)))
388 mdot_gas_F_max = Cd_valve * A_valve_F * P_reg_effective / np.sqrt(params.R_gas * T_reg_flow) * C_star
389 else:
390 # SUBSONIC FLOW: Flow is subsonic
391 T_reg_flow = T_copv # Regulator output temperature
392 term1 = (2.0 * gamma_gas) / ((gamma_gas - 1.0) * params.R_gas * T_reg_flow)
393 term2 = pr_ratio_F ** (2.0 / gamma_gas) - pr_ratio_F ** ((gamma_gas + 1.0) / gamma_gas)
394 mdot_gas_F_max = Cd_valve * A_valve_F * P_reg_effective * np.sqrt(term1 * term2)
395
396 # Scale by duty cycle (u_F) - valve is open for u_F fraction of time (PWM)
397 # Average flow = max_flow * duty_cycle
398 mdot_gas_F = mdot_gas_F_max * u_F
399
400 # Clamp to available flow from regulator (mass conservation)
401 if u_total > 1e-6 and mdot_gas_to_reg > 0:
402 # Split available flow proportionally to control inputs
403 mdot_gas_F = min(mdot_gas_F, mdot_gas_to_reg * u_F / u_total)
404
405 mdot_gas_F = max(mdot_gas_F, 0.0) # Ensure non-negative
406 else:
407 mdot_gas_F = 0.0 # Valve closed or no pressure difference - no flow
408
409 # PHYSICALLY ACCURATE: Gas flow from regulator to oxidizer tank through solenoid valve
410 if u_O > 1e-6 and P_reg_effective > P_u_O:
411 # Use proper choked flow equation for compressible gas through valve orifice
412 pr_ratio_O = P_u_O / P_reg_effective if P_reg_effective > 0 else 0.0
413 pr_crit = (2.0 / (gamma_gas + 1.0)) ** (gamma_gas / (gamma_gas - 1.0))
414
415 if pr_ratio_O < pr_crit:
416 # CHOKED FLOW: Flow is sonic at valve
417 T_reg_flow = T_copv # Regulator output temperature
418 C_star = np.sqrt(gamma_gas * (2.0 / (gamma_gas + 1.0)) ** ((gamma_gas + 1.0) / (gamma_gas - 1.0)))
419 mdot_gas_O_max = Cd_valve * A_valve_O * P_reg_effective / np.sqrt(params.R_gas * T_reg_flow) * C_star
420 else:
421 # SUBSONIC FLOW: Flow is subsonic
422 T_reg_flow = T_copv # Regulator output temperature
423 term1 = (2.0 * gamma_gas) / ((gamma_gas - 1.0) * params.R_gas * T_reg_flow)
424 term2 = pr_ratio_O ** (2.0 / gamma_gas) - pr_ratio_O ** ((gamma_gas + 1.0) / gamma_gas)
425 mdot_gas_O_max = Cd_valve * A_valve_O * P_reg_effective * np.sqrt(term1 * term2)
426
427 # Scale by duty cycle (u_O) - valve is open for u_O fraction of time (PWM)
428 # Average flow = max_flow * duty_cycle
429 mdot_gas_O = mdot_gas_O_max * u_O
430
431 # Clamp to available flow from regulator (mass conservation)
432 if u_total > 1e-6 and mdot_gas_to_reg > 0:
433 # Split available flow proportionally to control inputs
434 mdot_gas_O = min(mdot_gas_O, mdot_gas_to_reg * u_O / u_total)
435
436 mdot_gas_O = max(mdot_gas_O, 0.0) # Ensure non-negative
437 else:
438 mdot_gas_O = 0.0 # Valve closed or no pressure difference - no flow
439
440 # Add leakage loss from COPV (always present, even when valves closed)
441 # Model as small constant flow rate [kg/s]
442 # Typical leakage: ~0.001-0.01 kg/s depending on system
443 mdot_gas_loss = (params.copv_loss * params.V_copv) / (params.R_gas * params.T_gas)
444 mdot_gas_loss = max(0.0, min(mdot_gas_loss, 0.01)) # Clamp to reasonable range
445
446 # CRITICAL: Mass conservation - total gas mass flow out of COPV
447 # mdot_gas_total = mdot_gas_to_reg + mdot_gas_loss
448 # Note: mdot_gas_to_reg already accounts for valve states (u_total)
449 mdot_gas_total = mdot_gas_to_reg + mdot_gas_loss
450
451 # Verify mass conservation: gas flowing to tanks should not exceed gas from COPV
452 mdot_gas_to_tanks = mdot_gas_F + mdot_gas_O
453 if mdot_gas_to_tanks > mdot_gas_to_reg + 1e-6: # Allow small numerical tolerance
454 # Clamp to available flow (mass conservation)
455 if mdot_gas_to_reg > 1e-6:
456 scale_factor = mdot_gas_to_reg / mdot_gas_to_tanks
457 mdot_gas_F = mdot_gas_F * scale_factor
458 mdot_gas_O = mdot_gas_O * scale_factor
459
460 # Update COPV gas mass
461 # CRITICAL: Gas mass decreases as gas flows out (to regulator and loss)
462 # As gas mass decreases, pressure decreases (P = m*R*T/V)
463 # This models the finite gas supply in the COPV
464 m_gas_copv_next = m_gas_copv - dt * mdot_gas_total
465 m_gas_copv_next = max(m_gas_copv_next, 0.0) # Clamp to non-negative
466
467 # REAL-WORLD PHYSICS: Compute COPV pressure from gas mass using ideal gas law with temperature
468 # P = (m * R * T) / (V * Z)
469 # As m decreases (gas flows out), P decreases
470 # As T decreases (polytropic expansion), P decreases further
471 # This is the key: limited gas supply + temperature drop means pressure drops faster
472 if params.V_copv > 1e-10:
473 # Update COPV temperature for next step (polytropic expansion)
474 if use_polytropic and params._m_copv_0 > 1e-10:
475 rho_copv_next = m_gas_copv_next / params.V_copv
476 rho_copv_0 = params._m_copv_0 / params.V_copv
477 if rho_copv_0 > 1e-10:
478 T_copv_next = params._T_copv_0 * (rho_copv_next / rho_copv_0) ** (n_poly - 1.0)
479 T_copv_next = max(200.0, min(400.0, T_copv_next))
480 else:
481 T_copv_next = T_copv
482 else:
483 T_copv_next = T_copv
484
485 P_copv_next = (m_gas_copv_next * params.R_gas * T_copv_next) / (params.V_copv * params.Z_gas)
486 else:
487 P_copv_next = 0.0
488 P_copv_next = max(P_copv_next, 0.0) # Clamp to non-negative
489
490 # 2. Regulator pressure - MATCH HARDWARE BEHAVIOR
491 # HARDWARE OBSERVATION: Upstream pressure (regulator) oscillates with control activity
492 # - Oscillations: ~128-140 psi when control is active (regular, ~5-10 Hz)
493 # - Tank pressures (P_fuel) are much smoother (damped by tank volume)
494 #
495 # Real regulator behavior:
496 # - Maintains setpoint when COPV can supply it
497 # - Shows oscillations due to valve opening/closing (PWM control)
498 # - Output pressure responds to downstream demand
499 # - When COPV drops below setpoint, output drops with COPV
500 if params.reg_setpoint is not None:
501 # Fixed setpoint regulator: output is clamped to setpoint when COPV can supply it
502 if P_copv >= params.reg_setpoint:
503 # COPV can supply setpoint - regulator maintains setpoint
504 base_pressure = params.reg_setpoint
505
506 # HARDWARE: Regulator output shows oscillations due to PWM valve switching
507 # Oscillations are more pronounced when control is active
508 # Hardware shows: oscillations between ~128-140 psi (setpoint ~1000 psi = 6.9 MPa)
509 # This is ~±5-7% oscillation around setpoint
510 if u_total > 1e-6:
511 # Add oscillation based on control activity (PWM switching)
512 # Amplitude: ~5-7% of setpoint (matches hardware ~128-140 psi range)
513 # Frequency: PWM frequency (would need time for proper modeling)
514 # For now, use control magnitude to estimate oscillation amplitude
515 oscillation_amplitude = 0.06 * params.reg_setpoint * u_total # ~6% of setpoint
516 # Simple oscillation: varies with control magnitude
517 # Hardware shows regular oscillations, so use a pattern
518 # Use sum of control inputs to create oscillation pattern
519 control_sum = u_F + u_O
520 oscillation = oscillation_amplitude * np.sin(control_sum * np.pi * 2.0) # Oscillates with control
521 P_reg_next = base_pressure + oscillation
522 # Clamp to reasonable range (±10% of setpoint)
523 P_reg_next = max(base_pressure * 0.9, min(base_pressure * 1.1, P_reg_next))
524 else:
525 P_reg_next = base_pressure # No control = no oscillation (stable at setpoint)
526 else:
527 # COPV pressure below setpoint - regulator CANNOT maintain setpoint
528 # Output drops with COPV (regulator output limited by input)
529 P_reg_next = P_copv # Output limited by COPV (drops below setpoint when COPV < setpoint)
530 else:
531 # No setpoint: use ratio-based model (less realistic)
532 P_reg_next = params.reg_ratio * P_copv
533 # Clamp to not exceed current COPV
534 P_reg_next = min(P_reg_next, P_copv)
535
536 # 3. Ullage volume dynamics (blowdown)
537 # V_u increases as propellant is consumed
538 # V_u,i[k+1] = V_u,i[k] + dt * mdot_i / rho_i
539 # CRITICAL: Mass flow to chamber ALWAYS happens (pressure-dependent, not gated by control)
540 # Control input (u) only affects gas flow INTO tank, not propellant flow OUT
541 # Propellant flows out based on feed pressure and injector characteristics
542 # mdot_F and mdot_O come from engine physics (pressure-dependent)
543 #
544 # BLOWDOWN: When u = 0 (valves closed):
545 # - No gas flows into tanks (mdot_gas_F = 0, mdot_gas_O = 0)
546 # - Gas mass in tanks stays constant (m_gas_F = const, m_gas_O = const)
547 # - Propellant still flows to chamber (mdot_F > 0, mdot_O > 0)
548 # - Ullage volumes increase (V_u_F increases, V_u_O increases)
549 # - Pressure decreases: P = (m * R * T) / (V * Z)
550 # Since m constant and V increases, P decreases (BLOWDOWN)
551 # - CRITICAL: As pressure decreases, mdot decreases (pressure-dependent)
552 # This makes blowdown EXPONENTIAL: dP/dt ~ -k*P, so P(t) = P0*exp(-k*t)
553
554 # Ensure mdot values are valid (non-negative, finite)
555 mdot_F_valid = max(0.0, mdot_F) if np.isfinite(mdot_F) else 0.0
556 mdot_O_valid = max(0.0, mdot_O) if np.isfinite(mdot_O) else 0.0
557
558 # CRITICAL: Make mdot pressure-dependent for exponential blowdown
559 # When pressure drops, mdot should decrease proportionally
560 # Model: mdot ~ sqrt(P) for choked flow, or mdot ~ P for linear approximation
561 # Use ullage pressure (P_u) as it's the driving pressure for flow
562 # For exponential blowdown: as P decreases, mdot decreases, making decay exponential
563
564 # Pressure-dependent scaling for mdot (makes blowdown exponential)
565 # When pressure is high, mdot is high; when pressure drops, mdot drops
566 # Use a reference pressure (e.g., regulator setpoint or typical operating pressure)
567 # For choked injector flow: mdot ~ sqrt(P), but linear approximation also works
568 P_ref_F = 5e6 # Reference pressure for fuel [Pa] (~725 psi, typical operating pressure)
569 P_ref_O = 5e6 # Reference pressure for oxidizer [Pa]
570
571 # Pressure-dependent mdot scaling
572 # For choked flow: mdot ~ sqrt(P), but linear is simpler and still gives exponential decay
573 # When P drops, mdot drops proportionally, making ullage growth slow down
574 # This creates exponential pressure decay: dP/dt = -k*P -> P(t) = P0*exp(-k*t)
575 # Use sqrt for choked flow behavior (more realistic)
576 mdot_scale_F = np.sqrt(max(P_u_F, 1e5) / P_ref_F) # sqrt(P/P_ref) for choked flow
577 mdot_scale_O = np.sqrt(max(P_u_O, 1e5) / P_ref_O)
578
579 # Clamp scaling to reasonable range (0.1 to 2.0)
580 # Prevents mdot from becoming too small or too large
581 mdot_scale_F = max(0.1, min(2.0, mdot_scale_F))
582 mdot_scale_O = max(0.1, min(2.0, mdot_scale_O))
583
584 # Apply pressure-dependent scaling to mdot
585 # CRITICAL: This makes blowdown exponential - as pressure drops, mdot drops, decay accelerates
586 # Higher pressure -> higher mdot -> faster ullage growth -> faster pressure drop initially
587 # Lower pressure -> lower mdot -> slower ullage growth -> but pressure still drops (exponential)
588 mdot_F_pressure_dependent = mdot_F_valid * mdot_scale_F
589 mdot_O_pressure_dependent = mdot_O_valid * mdot_scale_O
590
591 # CRITICAL: When u=0 (valves closed), we have PURE BLOWDOWN
592 # In pure blowdown: no gas enters (mdot_gas = 0), so gas mass is constant
593 # Pressure drops because ullage volume increases (propellant consumed)
594 # With pressure-dependent mdot, this creates EXPONENTIAL decay: P(t) = P0*exp(-k*t)
595 # Make blowdown more aggressive when valves are closed
596 # Note: u_total is already defined earlier in the function (line 152)
597 if u_total < 1e-6: # Both valves closed - pure blowdown
598 # In blowdown, mdot should be MORE pressure-dependent (stronger exponential decay)
599 # Scale mdot more aggressively with pressure to make blowdown visible
600 # This ensures exponential decay is clearly visible when solenoids are closed
601 blowdown_factor = 1.5 # Make blowdown 1.5x more aggressive
602 mdot_F_pressure_dependent = mdot_F_pressure_dependent * blowdown_factor
603 mdot_O_pressure_dependent = mdot_O_pressure_dependent * blowdown_factor
604
605 # Ullage volume growth: V_u increases as propellant is consumed
606 # CRITICAL: As pressure increases, mdot increases (from engine physics),
607 # which makes ullage grow FASTER (non-linear growth)
608 # Higher pressure -> higher mdot -> faster ullage growth -> exponential-like behavior
609 # When pressure is high, mdot is high, so ullage grows fast
610 # When pressure drops, mdot drops, so ullage grows slower
611 # This creates non-linear (exponential-like) ullage growth
612 #
613 # CRITICAL STARTUP BEHAVIOR: When flow starts, ullage volume increases IMMEDIATELY
614 # This causes pressure to drop INSTANTLY because P = (m*R*T)/(V*Z)
615 # As V increases (propellant consumed), P decreases even if m stays constant
616 # This is the key: at startup, when flow begins, pressure should drop significantly
617 V_u_F_next = V_u_F + dt * mdot_F_pressure_dependent / params.rho_F
618 V_u_O_next = V_u_O + dt * mdot_O_pressure_dependent / params.rho_O
619
620 # Ensure volumes are non-negative
621 V_u_F_next = max(V_u_F_next, 0.0)
622 V_u_O_next = max(V_u_O_next, 0.0)
623
624 # CRITICAL: Ensure ullage volume grows immediately when flow starts
625 # If mdot > 0, ullage MUST grow, causing immediate pressure drop
626 # This models the realistic behavior: flow starts -> propellant consumed -> ullage grows -> pressure drops
627 if mdot_F_pressure_dependent > 1e-6 and V_u_F_next <= V_u_F:
628 # Force ullage growth if flow is happening
629 V_u_F_next = V_u_F + dt * mdot_F_pressure_dependent / params.rho_F
630 if mdot_O_pressure_dependent > 1e-6 and V_u_O_next <= V_u_O:
631 # Force ullage growth if flow is happening
632 V_u_O_next = V_u_O + dt * mdot_O_pressure_dependent / params.rho_O
633
634 # 4. Ullage pressure dynamics - track gas mass
635 # Key principle: Gas mass in ullage changes due to:
636 # - Gas flow IN from regulator (when valve open, u > 0)
637 # - Gas mass stays constant when valve closed (no gas enters)
638 # Pressure changes due to:
639 # - Gas mass changes (from regulator flow)
640 # - Ullage volume changes (from propellant consumption - BLOWDOWN)
641 #
642 # CRITICAL PHYSICS: P = (m * R * T) / (V * Z)
643 # - As ullage volume V increases (propellant consumed), same gas mass m gives lower pressure
644 # - As gas mass m increases (valve open), pressure increases, but volume also increases
645 # - Net effect depends on relative rates: if V increases faster than m, pressure decreases (blowdown)
646 # - When valve closed: m constant, V increases -> pressure always decreases (pure blowdown)
647 # - CRITICAL: With pressure-dependent mdot, blowdown is EXPONENTIAL
648 # As P decreases, mdot decreases, V grows slower, but P still decreases exponentially
649 # dP/dt = -k*P -> P(t) = P0*exp(-k*t) (exponential decay)
650
651 # Fuel ullage pressure
652 # Update gas mass: increases when valve open (gas flows in)
653 m_gas_F_next = m_gas_F + dt * mdot_gas_F
654 m_gas_F_next = max(m_gas_F_next, 0.0) # Clamp to non-negative
655
656 # LIQUID ENGINE PHYSICS: Pressure from ideal gas law P = (m * R * T) / (V * Z)
657 #
658 # CRITICAL STARTUP: Sudden pressure drop when flow starts (controller catch-up)
659 # - Flow begins -> ullage volume increases IMMEDIATELY -> pressure drops INSTANTLY
660 # - Controller hasn't responded yet, so pressure drops before control can compensate
661 # - This is the realistic startup: dynamics have instantly changed
662 #
663 # CRITICAL END STATE: Final pressure MUCH LOWER than start pressure
664 # - COPV volume is SMALL (4.5-6L) at high pressure (2750 psi)
665 # - Tank volumes are HUGE (11-17L, grow as propellant consumed)
666 # - When COPV gas flows to tanks, it expands into much larger volumes
667 # - P_final ≈ P_initial * (V_copv / V_tank_final) - MUCH LOWER
668 # - With both tanks and growing ullage, final pressure can be 10-20% of initial
669 # - This is liquid engine physics: gas expands into huge tank volumes
670 #
671 # Both m, V, and T affect pressure:
672 # - Larger V (more ullage) requires more m to maintain same pressure
673 # - Lower T (polytropic expansion) reduces pressure further
674 # - When V increases faster than m, pressure decreases (blowdown)
675 # - When m increases faster than V, pressure increases (pressurization)
676 if V_u_F_next > 1e-10:
677 # Update fuel tank temperature for next step (polytropic expansion)
678 if use_polytropic and params._m_F_0 > 1e-10 and params._V_F_0 > 1e-10:
679 rho_F_next = m_gas_F_next / V_u_F_next
680 rho_F_0 = params._m_F_0 / params._V_F_0
681 if rho_F_0 > 1e-10:
682 T_gas_F_next = params._T_F_0 * (rho_F_next / rho_F_0) ** (n_poly - 1.0)
683 T_gas_F_next = max(200.0, min(400.0, T_gas_F_next))
684 else:
685 T_gas_F_next = T_gas_F
686 else:
687 T_gas_F_next = T_gas_F
688
689 # LIQUID ENGINE PHYSICS: Pressure calculation with polytropic temperature
690 # P = (m * R * T) / (V * Z)
691 # When gas flows from small COPV to large tanks, pressure DROPS SIGNIFICANTLY
692 # because tanks are much larger (11-17L vs 4.5-6L COPV)
693 P_u_F_next = (m_gas_F_next * params.R_gas * T_gas_F_next) / (V_u_F_next * params.Z_gas)
694 else:
695 P_u_F_next = 0.0
696
697 # CRITICAL: Ensure pressure drops immediately when flow starts
698 # If ullage volume increased (flow happened), pressure MUST decrease
699 # This models the realistic startup behavior: flow starts -> pressure drops instantly
700 if V_u_F_next > V_u_F and mdot_F_pressure_dependent > 1e-6:
701 # Flow started: ullage grew, so pressure should drop
702 # The ideal gas law already handles this, but ensure it's pronounced
703 # If pressure didn't drop enough, it means gas mass might be increasing too fast
704 # In pure blowdown (u=0), gas mass is constant, so pressure MUST drop
705 if u_F < 1e-6: # Valve closed - pure blowdown
706 # In blowdown, gas mass is constant, so pressure drop is purely from volume increase
707 # This should be IMMEDIATE and SIGNIFICANT at startup
708 pass # Ideal gas law already handles this correctly
709 P_u_F_next = max(P_u_F_next, 0.0) # Clamp to non-negative
710
711 # Oxidizer ullage pressure
712 # Update gas mass: increases when valve open (gas flows in), constant when valve closed
713 # CRITICAL: When u_O = 0, mdot_gas_O = 0, so m_gas_O stays constant
714 m_gas_O_next = m_gas_O + dt * mdot_gas_O
715 m_gas_O_next = max(m_gas_O_next, 0.0) # Clamp to non-negative
716
717 # REAL-WORLD PHYSICS: Same logic as fuel with polytropic temperature
718 # BLOWDOWN CASE (u_O = 0):
719 # - m_gas_O = constant (no gas flow in)
720 # - V_u_O increases (propellant consumed)
721 # - T_gas_O decreases (polytropic expansion)
722 # - P_u_O = (m_gas_O * R * T) / (V_u_O * Z)
723 # - As V_u_O increases and T decreases, P_u_O decreases (BLOWDOWN)
724 #
725 # CRITICAL STARTUP BEHAVIOR: When flow starts, pressure drops IMMEDIATELY
726 # This is the realistic behavior: flow begins -> ullage grows -> T drops -> pressure drops instantly
727 if V_u_O_next > 1e-10:
728 # Update oxidizer tank temperature for next step (polytropic expansion)
729 if use_polytropic and params._m_O_0 > 1e-10 and params._V_O_0 > 1e-10:
730 rho_O_next = m_gas_O_next / V_u_O_next
731 rho_O_0 = params._m_O_0 / params._V_O_0
732 if rho_O_0 > 1e-10:
733 T_gas_O_next = params._T_O_0 * (rho_O_next / rho_O_0) ** (n_poly - 1.0)
734 T_gas_O_next = max(200.0, min(400.0, T_gas_O_next))
735 else:
736 T_gas_O_next = T_gas_O
737 else:
738 T_gas_O_next = T_gas_O
739
740 # CRITICAL: Pressure calculation MUST account for volume expansion
741 # Same as fuel: when gas flows from small COPV to large tank, pressure DROPS SIGNIFICANTLY
742 P_u_O_next = (m_gas_O_next * params.R_gas * T_gas_O_next) / (V_u_O_next * params.Z_gas)
743 else:
744 P_u_O_next = 0.0
745
746 # CRITICAL STARTUP BEHAVIOR: Immediate pressure drop when flow starts
747 # Same as fuel: sudden drop because dynamics have instantly changed
748 # Flow begins -> ullage grows -> pressure drops instantly (controller catch-up)
749 if V_u_O_next > V_u_O and mdot_O_pressure_dependent > 1e-6:
750 # Flow started: ullage grew, so pressure MUST drop
751 # Ideal gas law P = m*R*T/V handles this correctly
752 pass # Ideal gas law already handles this correctly
753
754 # CRITICAL END STATE: Final pressure should be MUCH LOWER
755 # Same as fuel: when all gas flows from COPV to tanks, final pressure is much lower
756 # because tanks are much larger than COPV
757 P_u_O_next = max(P_u_O_next, 0.0) # Clamp to non-negative
758
759 # 5. Feed pressure dynamics (first-order lag with increased damping)
760 # HARDWARE OBSERVATION: Downstream pressures are much smoother than upstream
761 # This is due to:
762 # 1. Tank volume damping (large volume smooths out pressure changes)
763 # 2. Feed line damping (first-order lag)
764 # 3. Inertia of liquid propellant
765 #
766 # P_d_i[k+1] = P_d_i[k] + dt*( (P_u,i - P_d_i)/tau_line_i )
767 # Increase tau_line to match hardware smoothness (hardware shows very smooth downstream)
768 # Hardware: downstream pressure is smooth even when upstream oscillates
769 tau_line_F_effective = params.tau_line_F * 2.0 # Increase damping to match hardware
770 tau_line_O_effective = params.tau_line_O * 2.0
771
772 P_d_F_next = P_d_F + dt * (P_u_F - P_d_F) / tau_line_F_effective
773 P_d_O_next = P_d_O + dt * (P_u_O - P_d_O) / tau_line_O_effective
774
775 # Assemble next state (including gas masses)
776 x_next = np.array([
777 P_copv_next,
778 P_reg_next,
779 P_u_F_next,
780 P_u_O_next,
781 P_d_F_next,
782 P_d_O_next,
783 V_u_F_next,
784 V_u_O_next,
785 m_gas_copv_next,
786 m_gas_F_next,
787 m_gas_O_next,
788 ], dtype=np.float64)
789
790 # Validate output state size
791 if len(x_next) != N_STATE:
792 raise ValueError(f"Output state vector must have {N_STATE} elements, got {len(x_next)}")
793
794 return x_next
795
796
798 x: np.ndarray,
799 u: np.ndarray,
800 dt: float,
801 params: DynamicsParams,
802 mdot_F: float,
803 mdot_O: float,
804 eps: float = 1e-6,
805) -> Tuple[np.ndarray, np.ndarray]:
806 """
807 Linearize dynamics via finite differences.
808
809 Computes A and B matrices such that:
810 x[k+1] ≈ A @ x[k] + B @ u[k] + c
811
812 Parameters:
813 -----------
814 x : np.ndarray, shape (N_STATE,)
815 State vector
816 u : np.ndarray, shape (N_CONTROL,)
817 Control vector
818 dt : float
819 Time step [s]
820 params : DynamicsParams
821 Dynamics parameters
822 mdot_F : float
823 Fuel mass flow rate [kg/s]
824 mdot_O : float
825 Oxidizer mass flow rate [kg/s]
826 eps : float
827 Finite difference step size
828
829 Returns:
830 --------
831 A : np.ndarray, shape (N_STATE, N_STATE)
832 State Jacobian matrix
833 B : np.ndarray, shape (N_STATE, N_CONTROL)
834 Control Jacobian matrix
835 """
836 # Nominal next state
837 x_nom = step(x, u, dt, params, mdot_F, mdot_O)
838
839 # Compute A matrix (∂f/∂x) via finite differences
840 A = np.zeros((N_STATE, N_STATE), dtype=np.float64)
841 for i in range(N_STATE):
842 x_pert = x.copy()
843 x_pert[i] += eps
844 x_pert_next = step(x_pert, u, dt, params, mdot_F, mdot_O)
845 A[:, i] = (x_pert_next - x_nom) / eps
846
847 # Compute B matrix (∂f/∂u) via finite differences
848 B = np.zeros((N_STATE, N_CONTROL), dtype=np.float64)
849 for i in range(N_CONTROL):
850 u_pert = u.copy()
851 u_pert[i] += eps
852 u_pert[i] = np.clip(u_pert[i], 0.0, 1.0) # Ensure in [0, 1]
853 u_pert_next = step(x, u_pert, dt, params, mdot_F, mdot_O)
854 B[:, i] = (u_pert_next - x_nom) / eps
855
856 return A, B
857
858
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
DynamicsParams from_config(cls, ControllerConfig config)
Definition dynamics.py:81
Tuple[np.ndarray, np.ndarray] linearize(np.ndarray x, np.ndarray u, float dt, DynamicsParams params, float mdot_F, float mdot_O, float eps=1e-6)
Definition dynamics.py:805
np.ndarray step(np.ndarray x, np.ndarray u, float dt, DynamicsParams params, float mdot_F, float mdot_O)
Definition dynamics.py:123