Source code for opengnc.actuators.thruster

"""
Thruster models including Chemical, Electric, and Multi-thruster clusters.
"""

from __future__ import annotations

from typing import Any

import numpy as np

from opengnc.actuators.actuator import Actuator


[docs] class Thruster(Actuator): """ Base Thruster model. Produces thrust force and consumes mass based on Isp. Parameters ---------- max_thrust : float, optional Maximum thrust (N). Default is 1.0. min_impulse_bit : float, optional Minimum impulse bit (Ns). Default is 0.0. isp : float, optional Specific Impulse (s). Default is None. name : str, optional Actuator name. Default is "Thruster". """ def __init__( self, max_thrust: float = 1.0, min_impulse_bit: float = 0.0, isp: float | None = None, name: str = "Thruster", ) -> None: """ Initialize thruster base. Parameters ---------- max_thrust : float, optional Maximum thrust (N). Default 1.0. min_impulse_bit : float | None, optional Minimum impulse bit (Ns). Default 0.0. isp : float | None, optional Specific Impulse (s). name : str, optional Actuator name. Default "Thruster". """ super().__init__(name=name, saturation=max_thrust) self.max_thrust: float = max_thrust self.min_impulse_bit: float = min_impulse_bit self.isp: float | None = isp
[docs] def command( self, thrust_cmd: float | None = None, dt: float | None = None, *args: Any, **kwargs: Any ) -> float: """ Calculate delivered thrust. Parameters ---------- thrust_cmd : float Commanded thrust (N). dt : float, optional Time step duration (s). Required for MIB checks. **kwargs : dict Additional parameters. Returns ------- float Delivered thrust (N). """ # Saturation (clip to max thrust) if thrust_cmd is None: if not args: raise ValueError("thrust_cmd is required.") thrust_cmd = float(args[0]) thrust = float(self.apply_saturation(thrust_cmd)) # Minimum Impulse Bit Logic # If dt is provided, check if the requested impulse is possible. if dt is not None and self.min_impulse_bit > 0 and abs(thrust) > 1e-9: requested_impulse: float = abs(thrust) * dt if requested_impulse < self.min_impulse_bit: # Deadband behavior for impulses < MIB thrust = 0.0 return float(thrust)
[docs] def get_mass_flow(self, thrust: float) -> float: r""" Calculate mass flow rate. Equation: $\dot{m} = \frac{T}{I_{sp} g_0}$ Parameters ---------- thrust : float Actual thrust produced (N). Returns ------- float Mass flow rate (kg/s). """ if self.isp and self.isp > 0: g0: float = 9.80665 return thrust / (self.isp * g0) return 0.0
[docs] class ChemicalThruster(Thruster): """ Chemical Thruster model. Models On/Off behavior or PWM-averaged thrust with minimum on-time constraints. Parameters ---------- max_thrust : float, optional Maximum thrust (N). Default is 10.0. isp : float, optional Specific impulse (s). Default is 300.0. min_on_time : float, optional Minimum valve open time (s). Default is 0.010. name : str, optional Actuator name. Default is "ChemThruster". """ def __init__( self, max_thrust: float = 10.0, isp: float = 300.0, min_on_time: float = 0.010, name: str = "ChemThruster", ) -> None: """ Initialize chemical thruster. Parameters ---------- max_thrust : float, optional Maximum thrust (N). Default 10.0. isp : float, optional Specific impulse (s). Default 300.0. min_on_time : float, optional Minimum valve open time (s). Default 0.010. name : str, optional Actuator name. Default "ChemThruster". """ self.min_on_time: float = min_on_time mib: float = max_thrust * min_on_time super().__init__(max_thrust=max_thrust, isp=isp, min_impulse_bit=mib, name=name)
[docs] def command( self, thrust_cmd: float | None = None, dt: float | None = None, *args: Any, **kwargs: Any ) -> float: """ Considers PWM constraints for chemical valve. If the commanded thrust implies an on-time < min_on_time, it is zeroed. Parameters ---------- thrust_cmd : float Commanded thrust (N). dt : float, optional Control period (s). **kwargs : dict Additional parameters. Returns ------- float Delivered thrust (N). """ if thrust_cmd is None and args: thrust_cmd = float(args[0]) thrust: float = super().command(thrust_cmd, dt=dt, **kwargs) if dt is not None and self.min_on_time > 0 and abs(thrust) > 1e-9: required_on_time: float = (abs(thrust) / self.max_thrust) * dt if required_on_time < self.min_on_time: thrust = 0.0 return thrust
[docs] class ElectricThruster(Thruster): """ Electric Thruster model (e.g., Hall Effect, Ion). Power-limited thrust generation. Parameters ---------- max_thrust : float, optional Maximum thrust (N). Default is 0.1. isp : float, optional Specific impulse (s). Default is 1500.0. power_efficiency : float, optional Electrical to Jet power efficiency (eta). Default is 0.6. name : str, optional Actuator name. Default is "ElecThruster". """ def __init__( self, max_thrust: float = 0.1, isp: float = 1500.0, power_efficiency: float = 0.6, name: str = "ElecThruster", ) -> None: """ Initialize electric thruster. Parameters ---------- max_thrust : float, optional Maximum thrust (N). Default 0.1. isp : float, optional Specific impulse (s). Default 1500.0. power_efficiency : float, optional Electrical to Jet power efficiency (eta). Default 0.6. name : str, optional Actuator name. Default "ElecThruster". """ super().__init__(max_thrust=max_thrust, isp=isp, name=name) self.power_efficiency: float = power_efficiency self.g0: float = 9.80665
[docs] def get_power_consumption(self, thrust: float) -> float: r""" Calculate electrical power requirement. Equation: $P_{in} = \frac{T I_{sp} g_0}{2 \eta}$ Parameters ---------- thrust : float Produced thrust (N). Returns ------- float Electrical power consumption (W). """ if self.power_efficiency <= 0: return float("inf") if self.isp is None: return 0.0 ve = self.isp * self.g0 return float(thrust * ve / (2 * self.power_efficiency))
[docs] class ThrusterCluster: """ A collection of thrusters with defined allocation logic. Maps generalized 6-DOF force/torque commands to individual thruster outputs. Parameters ---------- thrusters : list[Thruster] List of thruster objects. positions : np.ndarray (N, 3) positions of thrusters in body frame (m). directions : np.ndarray (N, 3) thrust unit vectors in body frame. """ def __init__(self, thrusters: list[Thruster], positions: np.ndarray, directions: np.ndarray) -> None: self.thrusters = thrusters self.N = len(thrusters) self.pos = np.array(positions) self.dir = np.array(directions) # Force_i = T_i * dir_i # Torque_i = pos_i x (T_i * dir_i) self.A = np.zeros((6, self.N)) for i in range(self.N): self.A[0:3, i] = self.dir[i] self.A[3:6, i] = np.cross(self.pos[i], self.dir[i]) # Default allocator from opengnc.actuators.allocation import PseudoInverseAllocator self.allocator = PseudoInverseAllocator(self.A)
[docs] def command( self, force_torque_cmd: np.ndarray | None = None, dt: float | None = None, *args: Any ) -> np.ndarray: """ Distribute 6-DOF force/torque command to individual thrusters. Parameters ---------- force_torque_cmd : np.ndarray Desired [Fx, Fy, Fz, Tx, Ty, Tz] in body frame (N, Nm). dt : float, optional Time step for MIB and duty cycle checks (s). Returns ------- np.ndarray Delivered thrusts for each thruster in the cluster (N). """ if force_torque_cmd is None: if not args: raise ValueError("force_torque_cmd is required.") force_torque_cmd = np.asarray(args[0]) thrust_cmds = self.allocator.allocate(force_torque_cmd) # Apply individual thruster constraints delivered_thrusts = [] for i, cmd in enumerate(thrust_cmds): cmd_clamped = max(0.0, cmd) delivered = self.thrusters[i].command(cmd_clamped, dt=dt) delivered_thrusts.append(delivered) return np.array(delivered_thrusts)