dpang commited on
Commit
e8edc26
Β·
verified Β·
1 Parent(s): 7078c69

Add server/spacecraft_physics.py

Browse files
Files changed (1) hide show
  1. server/spacecraft_physics.py +287 -0
server/spacecraft_physics.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Space Robotics Lab, SnT, University of Luxembourg, SpaceR
2
+ # RANS: arXiv:2310.07393
3
+
4
+ """
5
+ 2-D Spacecraft (Modular Floating Platform) Physics Simulation
6
+ =============================================================
7
+ Pure-NumPy implementation of the rigid-body dynamics described in the RANS
8
+ paper, Section III. This lets the environment run inside Docker containers
9
+ without NVIDIA Isaac Gym / Isaac Sim.
10
+
11
+ State vector: [x, y, ΞΈ, vx, vy, Ο‰]
12
+ x, y β€” world-frame position (m)
13
+ ΞΈ β€” heading / yaw angle (rad, wrapped to (βˆ’Ο€, Ο€])
14
+ vx, vy β€” world-frame linear velocity (m/s)
15
+ Ο‰ β€” angular velocity (rad/s)
16
+
17
+ Action: activations ∈ [0, 1]^n_thrusters (continuous) or {0,1}^n (binary).
18
+
19
+ Dynamics:
20
+ F_body = Ξ£_i a_i Β· F_max_i Β· dΜ‚_i (body-frame force vector)
21
+ F_world = R(ΞΈ) Β· F_body
22
+ a_linear = F_world / m (linear acceleration)
23
+ Ο„_i = (p_i Γ— F_max_i Β· dΜ‚_i)_z = p_xΒ·d_y βˆ’ p_yΒ·d_x
24
+ Ξ± = Ξ£_i a_i Β· Ο„_i / I (angular acceleration)
25
+ Integration: Euler with timestep dt
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import math
31
+ from dataclasses import dataclass, field
32
+ from typing import List, Optional
33
+
34
+ import numpy as np
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Thruster configuration
39
+ # ---------------------------------------------------------------------------
40
+
41
+ @dataclass
42
+ class ThrusterConfig:
43
+ """Configuration for a single thruster on the 2-D spacecraft."""
44
+
45
+ position: np.ndarray # [px, py] in body frame (m)
46
+ direction: np.ndarray # unit-vector [dx, dy] of applied force in body frame
47
+ force_max: float = 1.0 # peak thrust (N)
48
+
49
+ def __post_init__(self) -> None:
50
+ self.position = np.asarray(self.position, dtype=np.float64)
51
+ self.direction = np.asarray(self.direction, dtype=np.float64)
52
+ # Normalise direction
53
+ norm = np.linalg.norm(self.direction)
54
+ if norm > 1e-9:
55
+ self.direction = self.direction / norm
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Spacecraft configuration
60
+ # ---------------------------------------------------------------------------
61
+
62
+ @dataclass
63
+ class SpacecraftConfig:
64
+ """
65
+ Physical parameters and thruster layout for the 2-D spacecraft.
66
+
67
+ Matches the MFP2D (Modular Floating Platform) configuration used in RANS.
68
+ """
69
+
70
+ mass: float = 10.0 # kg
71
+ inertia: float = 0.50 # kgΒ·mΒ²
72
+ dt: float = 0.02 # simulation timestep (50 Hz)
73
+ max_episode_steps: int = 500
74
+ thrusters: List[ThrusterConfig] = field(default_factory=list)
75
+
76
+ @classmethod
77
+ def default_8_thruster(cls) -> "SpacecraftConfig":
78
+ """
79
+ Standard 8-thruster MFP2D layout from the RANS paper.
80
+
81
+ Thrusters are arranged in four pairs:
82
+ β€’ Two pairs provide pure translational force (Β±X, Β±Y in body frame)
83
+ β€’ Two pairs create coupled translational + rotational force (diagonal)
84
+
85
+ This gives the platform full 3-DoF controllability (x, y, ΞΈ).
86
+ """
87
+ r = 0.31 # m β€” radial distance from CoM to thruster attachment point
88
+
89
+ thrusters = [
90
+ # ── Translational thrusters (body Β±X) ──────────────────────────
91
+ # Mounted on Β±Y edges, thrust along +X body axis
92
+ ThrusterConfig(position=[ 0.0, r], direction=[1.0, 0.0]),
93
+ ThrusterConfig(position=[ 0.0, -r], direction=[1.0, 0.0]),
94
+ # Mounted on Β±Y edges, thrust along βˆ’X body axis
95
+ ThrusterConfig(position=[ 0.0, r], direction=[-1.0, 0.0]),
96
+ ThrusterConfig(position=[ 0.0, -r], direction=[-1.0, 0.0]),
97
+
98
+ # ── Rotational / combined thrusters (diagonal) ──────────────────
99
+ # CCW torque: thrusters at corners fire tangentially
100
+ ThrusterConfig(
101
+ position=[ r * 0.707, r * 0.707],
102
+ direction=[-0.707, 0.707],
103
+ ),
104
+ ThrusterConfig(
105
+ position=[-r * 0.707, -r * 0.707],
106
+ direction=[ 0.707, -0.707],
107
+ ),
108
+ # CW torque
109
+ ThrusterConfig(
110
+ position=[-r * 0.707, r * 0.707],
111
+ direction=[ 0.707, 0.707],
112
+ ),
113
+ ThrusterConfig(
114
+ position=[ r * 0.707, -r * 0.707],
115
+ direction=[-0.707, -0.707],
116
+ ),
117
+ ]
118
+ return cls(thrusters=thrusters)
119
+
120
+ @classmethod
121
+ def default_4_thruster(cls) -> "SpacecraftConfig":
122
+ """
123
+ Minimal 4-thruster layout (under-actuated in rotation).
124
+ Useful for simpler position-tracking experiments.
125
+ """
126
+ r = 0.31
127
+ thrusters = [
128
+ ThrusterConfig(position=[ 0.0, r], direction=[1.0, 0.0]), # +X
129
+ ThrusterConfig(position=[ 0.0, -r], direction=[-1.0, 0.0]), # βˆ’X
130
+ ThrusterConfig(position=[ r, 0.0], direction=[0.0, 1.0]), # +Y
131
+ ThrusterConfig(position=[-r, 0.0], direction=[0.0, -1.0]), # βˆ’Y
132
+ ]
133
+ return cls(thrusters=thrusters)
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Spacecraft dynamics
138
+ # ---------------------------------------------------------------------------
139
+
140
+ class Spacecraft2D:
141
+ """
142
+ 2-D spacecraft rigid-body simulator.
143
+
144
+ State: s = [x, y, ΞΈ, vx, vy, Ο‰] (float64)
145
+ Action: a = [a_0, …, a_{Nβˆ’1}] ∈ [0, 1] per thruster
146
+
147
+ The simulation runs at ``config.dt`` seconds per step.
148
+ """
149
+
150
+ def __init__(self, config: Optional[SpacecraftConfig] = None) -> None:
151
+ self.config = config or SpacecraftConfig.default_8_thruster()
152
+ self.n_thrusters = len(self.config.thrusters)
153
+ self._precompute_thruster_matrices()
154
+ self._state = np.zeros(6, dtype=np.float64)
155
+
156
+ # ------------------------------------------------------------------
157
+ # Pre-computation
158
+ # ------------------------------------------------------------------
159
+
160
+ def _precompute_thruster_matrices(self) -> None:
161
+ """
162
+ Build static matrices for body-frame force and torque.
163
+
164
+ force_body_matrix [2 Γ— N]: col i = direction_i * force_max_i
165
+ torque_vector [N]: element i = (p Γ— d)_z * force_max_i
166
+ """
167
+ n = self.n_thrusters
168
+ self._force_mat = np.zeros((2, n), dtype=np.float64)
169
+ self._torque_vec = np.zeros(n, dtype=np.float64)
170
+
171
+ for i, t in enumerate(self.config.thrusters):
172
+ self._force_mat[:, i] = t.direction * t.force_max
173
+ # Cross product z-component: px*dy βˆ’ py*dx
174
+ self._torque_vec[i] = (
175
+ t.position[0] * t.direction[1] - t.position[1] * t.direction[0]
176
+ ) * t.force_max
177
+
178
+ # ------------------------------------------------------------------
179
+ # Public API
180
+ # ------------------------------------------------------------------
181
+
182
+ def reset(self, state: Optional[np.ndarray] = None) -> np.ndarray:
183
+ """
184
+ Reset to a given state (or zeros).
185
+
186
+ Args:
187
+ state: [x, y, ΞΈ, vx, vy, Ο‰], or None β†’ zero state.
188
+
189
+ Returns:
190
+ Copy of the initial state.
191
+ """
192
+ if state is None:
193
+ self._state = np.zeros(6, dtype=np.float64)
194
+ else:
195
+ self._state = np.asarray(state, dtype=np.float64).copy()
196
+ self._state[2] = self._wrap_angle(self._state[2])
197
+ return self._state.copy()
198
+
199
+ def step(self, activations: np.ndarray) -> np.ndarray:
200
+ """
201
+ Advance simulation by one timestep.
202
+
203
+ Args:
204
+ activations: Thruster commands, shape [n_thrusters], clamped to [0,1].
205
+
206
+ Returns:
207
+ New state [x, y, ΞΈ, vx, vy, Ο‰].
208
+ """
209
+ a = np.clip(activations, 0.0, 1.0)
210
+ x, y, theta, vx, vy, omega = self._state
211
+ dt = self.config.dt
212
+
213
+ # Body-frame force vector (shape [2])
214
+ F_body = self._force_mat @ a
215
+
216
+ # Rotate to world frame
217
+ c, s = math.cos(theta), math.sin(theta)
218
+ ax = (c * F_body[0] - s * F_body[1]) / self.config.mass
219
+ ay = (s * F_body[0] + c * F_body[1]) / self.config.mass
220
+
221
+ # Angular acceleration
222
+ alpha = (self._torque_vec @ a) / self.config.inertia
223
+
224
+ # Euler integration (position uses mid-point correction)
225
+ self._state[0] = x + vx * dt + 0.5 * ax * dt * dt
226
+ self._state[1] = y + vy * dt + 0.5 * ay * dt * dt
227
+ theta_new = theta + omega * dt + 0.5 * alpha * dt * dt
228
+ self._state[2] = self._wrap_angle(theta_new)
229
+ self._state[3] = vx + ax * dt
230
+ self._state[4] = vy + ay * dt
231
+ self._state[5] = omega + alpha * dt
232
+
233
+ return self._state.copy()
234
+
235
+ # ------------------------------------------------------------------
236
+ # Observation helpers
237
+ # ------------------------------------------------------------------
238
+
239
+ def get_thruster_transforms(self) -> np.ndarray:
240
+ """
241
+ Return thruster layout as a float array for the observation.
242
+
243
+ Returns:
244
+ Shape [n_thrusters, 5]: each row = [px, py, dx, dy, force_max]
245
+ """
246
+ T = np.zeros((self.n_thrusters, 5), dtype=np.float32)
247
+ for i, t in enumerate(self.config.thrusters):
248
+ T[i, 0:2] = t.position
249
+ T[i, 2:4] = t.direction
250
+ T[i, 4] = t.force_max
251
+ return T
252
+
253
+ def get_thruster_masks(self) -> np.ndarray:
254
+ """Binary mask: 1.0 for every valid thruster slot."""
255
+ return np.ones(self.n_thrusters, dtype=np.float32)
256
+
257
+ # ------------------------------------------------------------------
258
+ # Properties
259
+ # ------------------------------------------------------------------
260
+
261
+ @property
262
+ def state(self) -> np.ndarray:
263
+ return self._state.copy()
264
+
265
+ @property
266
+ def position(self) -> np.ndarray:
267
+ return self._state[:2].copy()
268
+
269
+ @property
270
+ def heading(self) -> float:
271
+ return float(self._state[2])
272
+
273
+ @property
274
+ def linear_velocity(self) -> np.ndarray:
275
+ return self._state[3:5].copy()
276
+
277
+ @property
278
+ def angular_velocity(self) -> float:
279
+ return float(self._state[5])
280
+
281
+ # ------------------------------------------------------------------
282
+ # Internal utilities
283
+ # ------------------------------------------------------------------
284
+
285
+ @staticmethod
286
+ def _wrap_angle(angle: float) -> float:
287
+ return (angle + math.pi) % (2.0 * math.pi) - math.pi