Jeff Towers commited on
Commit
c4fdbd6
·
1 Parent(s): a5c87d8

Add RICOCHET-001: Bouncy Ball Bola deployment physics

Browse files

- ricochet_deployment.py: impact-triggered sail unfurling primitive
(RicochetMarionette, DeploymentState, BounceDynamics, ImpactTrigger,
ShellSeparator, SailPayout)
- blueprint RICOCHET-001 doc
- physics package re-exports new symbols

blueprints/RICOCHET-001_bouncy_ball_deployment.md ADDED
@@ -0,0 +1,676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RICOCHET-001: Bouncy Ball Bola Deployment System
2
+
3
+ **Classification:** Impact-Triggered Sail Unfurling
4
+ **Status:** DRAFT v0.1
5
+ **Date:** 2026-01-24
6
+ **Codename:** "SUPERBALL MARIONETTE"
7
+
8
+ ---
9
+
10
+ ## 1. CONCEPT OVERVIEW
11
+
12
+ **Throw a big rubber ball at the ground. It bounces. It ERUPTS into a sailing marionette.**
13
+
14
+ The bounce isn't wasted energy - it's the LAUNCH MECHANISM.
15
+
16
+ ```
17
+ PHASE 1: THROW PHASE 2: IMPACT PHASE 3: ERUPTION
18
+ ════════════════ ═══════════════ ═════════════════
19
+
20
+ ○ ○ ⛵ ⛵
21
+ /|\ ← Human throws ╱│╲ ← Ball compresses \ /
22
+ │ downward ▄███▄ stores elastic \_/
23
+ / \ ═══════ energy ┌──┴──┐
24
+ │CORE │
25
+ ◉ ←─ Compressed ◉▄▄▄▄◉ ← Deformation ═══╧═══
26
+ │ marionette ═══════ triggers latch │
27
+ │ │ ⛵─┼─⛵
28
+ ▼ │ │
29
+ ───────────────── ───────────────── ─────────────────
30
+ GROUND GROUND GROUND
31
+
32
+ BOUNCE lifts
33
+ entire system!
34
+
35
+
36
+ PHASE 4: FLIGHT PHASE 5: MARIONETTE MODE
37
+ ═══════════════ ════════════════════════
38
+
39
+ ⛵ ⛵ ⛵ ⛵
40
+ \ / \ /
41
+ \ / ← Sails catch air \ /
42
+ ┌──┴──┐ \ /
43
+ │BRAIN│ ← Champion awakens ┌───┴───┐
44
+ └──┬──┘ │CONTROL│
45
+ │ └───┬───┘
46
+ ⛵ ⛵ │
47
+ ⛵──┼──⛵
48
+ ~~~~ WIND ~~~~
49
+ AUTONOMOUS FLIGHT
50
+ ```
51
+
52
+ ---
53
+
54
+ ## 2. THE SUPERBALL SHELL
55
+
56
+ ### 2.1 Material: High-Restitution Rubber
57
+
58
+ The outer shell is made of **polybutadiene rubber** (same as actual Super Balls):
59
+ - Coefficient of Restitution: **0.85 - 0.92** (bounces to ~80% of drop height!)
60
+ - Stores massive elastic energy on impact
61
+ - Survives repeated ground strikes
62
+
63
+ ```
64
+ SUPERBALL CROSS-SECTION
65
+ ═══════════════════════
66
+
67
+ ┌─────────────────────────────────────┐
68
+ │ OUTER RUBBER SHELL │
69
+ │ (polybutadiene, 8mm thick) │
70
+ │ ┌─────────────────────────────┐ │
71
+ │ │ COMPRESSION CHAMBER │ │
72
+ │ │ ┌───────────────────────┐ │ │
73
+ │ │ │ │ │ │
74
+ │ │ │ ╔═══════════════╗ │ │ │
75
+ │ │ │ ║ SPOOL CORE ║ │ │ │
76
+ │ │ │ ║ ┌─────────┐ ║ │ │ │
77
+ │ │ │ ║ │ SAILS │ ║ │ │ │
78
+ │ │ │ ║ │ (wound) │ ║ │ │ │
79
+ │ │ │ ║ │ ◎◎◎◎◎◎◎ │ ║ │ │ │
80
+ │ │ │ ║ └─────────┘ ║ │ │ │
81
+ │ │ │ ║ CHAMPION ║ │ │ │
82
+ │ │ │ ║ BRAIN ║ │ │ │
83
+ │ │ │ ╚═══════════════╝ │ │ │
84
+ │ │ │ │ │ │
85
+ │ │ └───────────────────────┘ │ ��
86
+ │ │ LATCH RING ●────● │ │
87
+ │ └─────────────────────────────┘ │
88
+ │ │
89
+ └─────────────────────────────────────┘
90
+
91
+ ◉ = 15-25 cm diameter
92
+ ```
93
+
94
+ ### 2.2 Ball Specifications
95
+
96
+ | Property | Value | Notes |
97
+ |----------|-------|-------|
98
+ | Diameter | 15-25 cm | Softball to volleyball size |
99
+ | Shell Thickness | 6-10 mm | Balances bounce vs payload |
100
+ | Total Mass | 0.8-2.0 kg | Throwable by human |
101
+ | Restitution Coefficient | 0.85+ | HIGH bounce required |
102
+ | Impact Survival | 50+ m/s | Handles hard throws |
103
+
104
+ ---
105
+
106
+ ## 3. THE ERUPTION MECHANISM
107
+
108
+ ### 3.1 G-Force Triggered Latch
109
+
110
+ The shell contains a **compression-activated latch** that releases at a specific G-force threshold.
111
+
112
+ ```
113
+ LATCH MECHANISM (Cross-section view)
114
+ ═════════════════════════════════════
115
+
116
+ ARMED STATE TRIGGERED STATE
117
+ ════════════ ═══════════════
118
+
119
+ ┌────────────────┐ ┌────────────────┐
120
+ │ SHELL WALL │ │ SHELL WALL │
121
+ │ ┌────────────┐ │ │ ┌────────────┐ │
122
+ │ │ ┌──────┐ │ │ │ │ │ │
123
+ │ │ │LATCH │ │ │ ══► IMPACT ══► │ │ LATCH │ │
124
+ │ │ │ PIN │ │ │ │ │ SHEARED │ │
125
+ │ │ └──┬───┘ │ │ │ │ ↓ │ │
126
+ │ │ │ │ │ │ │ ═════ │ │
127
+ │ │ ▼▼▼▼▼▼▼ │ │ │ │ ▲▲▲▲▲ │ │
128
+ │ │ SPRING │ │ │ │ RELEASED! │ │
129
+ │ │ LOADED │ │ │ │ │ │
130
+ │ └────────────┘ │ │ └────────────┘ │
131
+ └────────────────┘ └────────────────┘
132
+
133
+
134
+ G-FORCE THRESHOLD:
135
+ ══════════════════
136
+
137
+ G-force
138
+
139
+ 150G │ ████ ← TRIGGER ZONE
140
+ 100G │ █████████ (100-200G)
141
+ 50G │ │
142
+ 0G ├────┼────────────► time
143
+ │ │
144
+ IMPACT
145
+ MOMENT
146
+ ```
147
+
148
+ ### 3.2 Shell Separation
149
+
150
+ On trigger, the shell **splits along pre-scored seams** like a blooming flower:
151
+
152
+ ```
153
+ SHELL BLOOM SEQUENCE
154
+ ════════════════════
155
+
156
+ T = 0ms T = 5ms T = 20ms T = 50ms
157
+ (Impact) (Latch releases) (Petals open) (Full bloom)
158
+
159
+ ◉ ◉ ╱│╲ \ /
160
+ │ /│\ / │ \ \_/
161
+ │ / │ \ / │ \ │
162
+ ═══════ ═══════════ ════════════ ═══════════
163
+
164
+
165
+ SAILS BEGIN
166
+ TO DEPLOY
167
+
168
+
169
+ TOP VIEW OF PETAL SEPARATION:
170
+ ═════════════════════════════
171
+
172
+ ┌───┐ ╱ ╲ ╲ ╱
173
+ ╱ ╲ │ │ ╲ ╱
174
+ │ ◉ │ ═══► │ ◉ │ ═══► ╲ ╱
175
+ ╲ ╱ │ │ ◉
176
+ └───┘ ╲ ╱ ╱ ╲
177
+ ╱ ╲
178
+ CLOSED CRACKING DETACHED
179
+ (petals fly off)
180
+ ```
181
+
182
+ ---
183
+
184
+ ## 4. BOUNCE ENERGY HARVESTING
185
+
186
+ ### 4.1 The Bounce = Free Launch Velocity
187
+
188
+ When the ball hits the ground, it compresses and rebounds. We HARVEST this:
189
+
190
+ ```
191
+ ENERGY FLOW DIAGRAM
192
+ ═══════════════════
193
+
194
+ THROW ENERGY ──────────────────────────────────────────────►
195
+ │ │
196
+ │ ┌──────────────────────────────────────────────┐ │
197
+ │ │ │ │
198
+ ▼ ▼ ▼ │
199
+ ╔═══════════╗ ╔═══════════════╗ ╔═══════════════╗ │
200
+ ║ KINETIC ║ ──► ║ ELASTIC ║ ──► ║ KINETIC ║ │
201
+ ║ (down) ║ ║ (compressed) ║ ║ (UP!) ║ │
202
+ ╚═══════════╝ ╚═══════════════╝ ╚═══════════════╝ │
203
+ │ │ │ │
204
+ │ │ │ │
205
+ │ ▼ ▼ ▼
206
+ │ ┌─────────────┐ ┌──────────────────┐
207
+ │ │ LATCH TRIPS │ │ SAILS DEPLOY │
208
+ │ │ (uses tiny │ │ (catch upward │
209
+ │ │ fraction) │ │ momentum!) │
210
+ │ └─────────────┘ └──────────────────┘
211
+
212
+ └──────► ~15% lost to ground/heat
213
+
214
+
215
+ VELOCITY DIAGRAM:
216
+ ═════════════════
217
+
218
+ V (m/s)
219
+
220
+ +15 │ ●●●●●●●●●●●● ← SAILS DEPLOYED
221
+ │ ● ↖ (catching wind)
222
+ +10 │ ●
223
+ │ ● ← UPWARD BOUNCE
224
+ +5 │ ●
225
+ │ ●
226
+ 0 ├───────────────●─────────────────► time
227
+ │ ●│
228
+ -5 │ ● │
229
+ │ ● │
230
+ -10 │ ● │
231
+ │ ● │
232
+ -15 │ ● │ ← DOWNWARD (thrown)
233
+ │ ● │
234
+ -20 │ ● │
235
+
236
+ IMPACT
237
+ (latch triggers)
238
+ ```
239
+
240
+ ### 4.2 Timing is Everything
241
+
242
+ The shell must separate DURING the bounce, not before or after:
243
+
244
+ ```
245
+ CRITICAL TIMING WINDOW
246
+ ══════════════════════
247
+
248
+ ─────────────────────────────────────────────────────────────►
249
+ time
250
+
251
+ │◄──── APPROACH ────►│◄─ CONTACT ─►│◄──── REBOUND ────►│
252
+ │ │ │ │
253
+ │ Ball falling │ Compression │ Ball rising │
254
+ │ Shell intact │ Latch trips │ Shell blooming │
255
+ │ │ │ Sails unfurling │
256
+ │ │ │ │
257
+ │ ◉ │ ▄◉▄ │ ⛵ ⛵ │
258
+ │ │ │ ═════ │ \│/ │
259
+ │ ▼ │ │ ◉ │
260
+ │ │ │ ↑ │
261
+ │ │ │ │
262
+ ────────────────────────────────────────────────────────────
263
+
264
+
265
+ FAILURE MODES:
266
+ ══════════════
267
+
268
+ TOO EARLY (pre-impact): TOO LATE (post-apex):
269
+
270
+ ⛵ ⛵ ⛵ ⛵
271
+ \│/ \│/
272
+ ◉ ← Sails deploy ◉ ← Sails deployed
273
+ │ while falling! │ but falling again!
274
+ ▼ (no upward momentum) ▼ (missed the bounce)
275
+ ═══════ ═══════
276
+
277
+ BAD! BAD!
278
+ ```
279
+
280
+ ---
281
+
282
+ ## 5. SAIL DEPLOYMENT SEQUENCE
283
+
284
+ ### 5.1 Coiled Configuration (Pre-Deploy)
285
+
286
+ Inside the ball, sails are wound tightly around the spool core:
287
+
288
+ ```
289
+ INTERNAL COIL STRUCTURE
290
+ ═══════════════════════
291
+
292
+ TOP VIEW (looking down into ball):
293
+
294
+ ╔═══════════════════════════╗
295
+ ║ ║
296
+ ║ ┌─────────────────┐ ║
297
+ ║ │ ◎◎◎◎◎◎◎◎◎◎◎◎ │ ║
298
+ ║ │ ◎ ┌───────┐ ◎ │ ║
299
+ ║ │ ◎ │ SPOOL │ ◎ │ ║
300
+ ║ │ ◎ │ CORE │ ◎ │ ║
301
+ ║ │ ◎ │ │ ◎ │ ║ ← 4 sails wound
302
+ ║ │ ◎ │ BRAIN │ ◎ │ ║ as tight spirals
303
+ ║ │ ◎ └───────┘ ◎ │ ║
304
+ ║ │ ◎◎◎◎◎◎◎◎◎◎◎◎ │ ║
305
+ ║ └─────────────────┘ ║
306
+ ║ ║
307
+ ╚═══════════════════════════╝
308
+ RUBBER SHELL
309
+
310
+
311
+ SIDE VIEW (cross-section):
312
+
313
+ ┌─────────────────────────────┐
314
+ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ ← Rubber
315
+ │░┌───────────────────────┐░░░│
316
+ │░│ ════════════════════ │░░░│ ← Sail 0 (coiled)
317
+ │░│ ════════════════════ │░░░│ ← Sail 1 (coiled)
318
+ │░│ ╔═══════════════╗ │░░░│
319
+ │░│ ║ SPOOL DRUMS ║ │░░░│
320
+ │░│ ║ ┌──┐┌──┐ ║ │░░░│
321
+ │░│ ║ │◎ ││◎ │ ║ │░░░│ ← Individual drums
322
+ │░│ ║ └──┘└──┘ ║ │░░░│ per sail cable
323
+ │░│ ╚═══════════════╝ │░░░│
324
+ │░│ ════════════════════ │░░░│ ← Sail 2 (coiled)
325
+ │░│ ════════════════════ │░░░│ ← Sail 3 (coiled)
326
+ │░└───────────────────────┘░░░│
327
+ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
328
+ └─────────────────────────────┘
329
+ ```
330
+
331
+ ### 5.2 Centrifugal Unfurling
332
+
333
+ As the shell petals separate, they carry sail tips outward. Then **spin + upward motion** unfurls:
334
+
335
+ ```
336
+ UNFURL SEQUENCE (Top view, time series)
337
+ ════════════════════════════════════════
338
+
339
+ T=0ms T=30ms T=100ms T=200ms
340
+ Shell splits Petals eject Cables pay out Full extension
341
+
342
+ ╱ ╲ \ / ⛵ ⛵ ⛵ ⛵
343
+ │ ◎ │ \ / \ / \ /
344
+ ╲ ╱ ◎ \ / \ /
345
+ │ ┌──┴──┐ \ /
346
+ ╱ ╲ │CORE │ ┌───┴───┐
347
+ └──┬──┘ │ SPOOL │
348
+ │ └───┬───┘
349
+ ⛵ ⛵ │
350
+ ⛵──┼──⛵
351
+
352
+
353
+ CABLE PAYOUT MECHANISM:
354
+ ═══════════════════════
355
+
356
+ Each sail is on a DRUM that pays out cable as centrifugal force pulls:
357
+
358
+ ┌────────────────────────────────────────────┐
359
+ │ │
360
+ │ DRUM (wound) DRUM (paying out) │
361
+ │ │
362
+ │ ┌──────┐ ┌──────┐ │
363
+ │ │◎◎◎◎◎◎│ │◎◎◎ │───────► │ ← Cable
364
+ │ │◎◎◎◎◎◎│ ═══► │◎◎ │ │ unreeling
365
+ │ │◎◎◎◎◎◎│ │◎ │ │
366
+ │ └──────┘ └──────┘ │
367
+ │ │
368
+ │ INITIAL MID-DEPLOY │
369
+ │ (all cable wound) (cable out) │
370
+ │ │
371
+ └────────────────────────────────────────────┘
372
+
373
+
374
+ Centrifugal force at R = 1m, ω = 3 rad/s:
375
+
376
+ F_cent = m * ω² * R
377
+ = 0.2 kg * (3)² * 1
378
+ = 1.8 N per sail
379
+
380
+ This pulls cables out smoothly!
381
+ ```
382
+
383
+ ---
384
+
385
+ ## 6. PHYSICS MODEL
386
+
387
+ ### 6.1 Impact Dynamics
388
+
389
+ ```python
390
+ # Ground impact model
391
+ def compute_bounce(v_impact, restitution=0.87, mass=1.5):
392
+ """
393
+ Calculate bounce velocity from impact.
394
+
395
+ Args:
396
+ v_impact: Impact velocity (m/s, positive = downward)
397
+ restitution: Coefficient of restitution (0.87 for superball rubber)
398
+ mass: Ball mass (kg)
399
+
400
+ Returns:
401
+ v_rebound: Rebound velocity (m/s, positive = upward)
402
+ g_force: Peak G-force during impact
403
+ contact_time: Ground contact duration (s)
404
+ """
405
+ # Rebound velocity (energy preserved * restitution)
406
+ v_rebound = v_impact * restitution
407
+
408
+ # Contact time (Hertzian contact approximation)
409
+ # For rubber ball ~10-20ms
410
+ contact_time = 0.015 # 15ms typical
411
+
412
+ # Peak deceleration
413
+ delta_v = v_impact + v_rebound # Total velocity change
414
+ a_peak = delta_v / contact_time
415
+ g_force = a_peak / 9.81
416
+
417
+ return v_rebound, g_force, contact_time
418
+
419
+
420
+ # Example: 20 m/s throw (hard overhand)
421
+ v_rebound, g_force, t_contact = compute_bounce(20.0)
422
+ # v_rebound ≈ 17.4 m/s (upward!)
423
+ # g_force ≈ 255 G (definitely triggers latch!)
424
+ # t_contact ≈ 15 ms
425
+ ```
426
+
427
+ ### 6.2 Trigger Threshold
428
+
429
+ ```python
430
+ # G-force latch parameters
431
+ TRIGGER_G_MIN = 80 # Minimum G to trigger (prevents accidental)
432
+ TRIGGER_G_MAX = 500 # Max survivable G for electronics
433
+
434
+ # For various throw speeds:
435
+ # 10 m/s (gentle toss): ~127 G ← TRIGGERS
436
+ # 15 m/s (medium throw): ~191 G ← TRIGGERS
437
+ # 20 m/s (hard throw): ~255 G ← TRIGGERS
438
+ # 25 m/s (very hard): ~319 G ← TRIGGERS
439
+ # 5 m/s (drop): ~64 G ← NO TRIGGER (too soft)
440
+ ```
441
+
442
+ ### 6.3 Sail Deployment Dynamics
443
+
444
+ ```python
445
+ def sail_unfurl_physics(
446
+ bounce_velocity: float, # m/s upward
447
+ spin_rate: float, # rad/s (imparted by throw)
448
+ num_sails: int = 4,
449
+ sail_mass: float = 0.15, # kg per sail
450
+ cable_length: float = 2.0, # m max extension
451
+ ):
452
+ """
453
+ Model sail deployment during upward bounce.
454
+ """
455
+ # Centrifugal acceleration pulls sails outward
456
+ # a_cent = ω² * r
457
+
458
+ # Time to full extension (approximate)
459
+ # Using F = ma, where F = centrifugal
460
+ # Simplified: t ≈ sqrt(2 * cable_length / a_cent)
461
+
462
+ # At spin_rate = 3 rad/s, r = 1m:
463
+ # a_cent = 9 m/s²
464
+ # t_deploy ≈ sqrt(2 * 2.0 / 9) ≈ 0.67 seconds
465
+
466
+ # During this time, ball rises:
467
+ # h = v * t - 0.5 * g * t²
468
+ # h = 17 * 0.67 - 0.5 * 9.81 * 0.67²
469
+ # h ≈ 9.2 m (above bounce point!)
470
+
471
+ return {
472
+ 'deploy_time': 0.67,
473
+ 'deploy_altitude': 9.2,
474
+ 'final_sail_velocity': spin_rate * cable_length, # tangential
475
+ }
476
+ ```
477
+
478
+ ---
479
+
480
+ ## 7. CHAMPION BRAIN ACTIVATION
481
+
482
+ ### 7.1 Boot Sequence
483
+
484
+ The Champion brain (DreamerV3) activates on impact detection:
485
+
486
+ ```
487
+ BRAIN ACTIVATION TIMELINE
488
+ ═════════════════════════
489
+
490
+ ──────────────────────────────────────────────────────────────────►
491
+ time
492
+
493
+ │ DORMANT │ IMPACT │ BOOT │ CALIBRATE │ SAIL CONTROL │ MARIONETTE │
494
+ │ │ │ │ │ │ MODE │
495
+ │ │ │ │ │ │ │
496
+ │ zzz... │ !!! │ ████ │ ◎ ◎ ◎ ◎ │ ~~~~ ⛵ │ FLYING │
497
+ │ │ │ │ │ │ │
498
+ ├─────────┼────────┼──────┼───────────┼──────────────┼────────────┤
499
+ -∞ T=0 50ms 100ms 300ms 500ms+
500
+
501
+
502
+ BOOT SEQUENCE DETAIL:
503
+ ═════════════════════
504
+
505
+ T+0ms: Accelerometer detects >80G → WAKE signal
506
+ T+10ms: IMU initialization
507
+ T+20ms: Tension sensors online (all drums)
508
+ T+30ms: RSSM state estimator starts
509
+ T+50ms: First control output (brake drums to prevent overshoot)
510
+ T+100ms: Sail positions estimated from cable tensions
511
+ T+200ms: Aerodynamic model engaged
512
+ T+300ms: Full marionette control active
513
+ T+500ms: Champion brain has authority
514
+ ```
515
+
516
+ ### 7.2 Mid-Air Orientation Recovery
517
+
518
+ After chaotic bounce + deploy, the system must stabilize:
519
+
520
+ ```
521
+ STABILIZATION CHALLENGE
522
+ ═══════════════════════
523
+
524
+ POST-BOUNCE (Chaotic): TARGET (Stable):
525
+
526
+ ⛵ ⛵ ⛵
527
+ / \ /
528
+ / ⛵ \ /
529
+ │ / ┌──┴──┐
530
+ ◎──/ ← Tumbling, │CORE │ ← Level,
531
+ \ tangled? └──┬──┘ symmetric
532
+ \ │
533
+ ⛵ ⛵──┼──⛵
534
+
535
+ Champion uses:
536
+ 1. Differential cable tension → torque for rotation
537
+ 2. Sail pitch modulation → aerodynamic moments
538
+ 3. Collective pitch → altitude control
539
+ ```
540
+
541
+ ---
542
+
543
+ ## 8. OPERATIONAL ENVELOPE
544
+
545
+ ### 8.1 Throw Parameters
546
+
547
+ | Parameter | Min | Optimal | Max | Notes |
548
+ |-----------|-----|---------|-----|-------|
549
+ | Throw Speed | 8 m/s | 15-20 m/s | 30 m/s | Harder = higher bounce |
550
+ | Throw Angle | -90° (straight down) | -60° to -45° | 0° (horizontal) | Steep = clean bounce |
551
+ | Spin Imparted | 0 rad/s | 2-4 rad/s | 10 rad/s | Spin aids deployment |
552
+ | Release Height | 1.0 m | 1.5-2.0 m | 3.0 m | Higher = more time |
553
+
554
+ ### 8.2 Environmental Requirements
555
+
556
+ ```
557
+ SURFACE REQUIREMENTS:
558
+ ═════════════════════
559
+
560
+ ✓ GOOD SURFACES ✗ BAD SURFACES
561
+ ────────────── ─────────────
562
+ • Concrete • Sand (absorbs energy)
563
+ • Asphalt • Grass (unpredictable)
564
+ • Hard-packed dirt • Water (no bounce lol)
565
+ • Gym floor • Mud
566
+ • Metal deck • Foam/carpet
567
+
568
+
569
+ WIND CONDITIONS:
570
+ ════════════════
571
+
572
+ Optimal: 3-8 m/s (light breeze)
573
+ - Enough wind for sail control
574
+ - Not so much it tumbles the ball pre-impact
575
+
576
+ Maximum: 15 m/s
577
+ - Beyond this, deploy timing unreliable
578
+ ```
579
+
580
+ ---
581
+
582
+ ## 9. INTEGRATION WITH MARIONETTE SPOOL
583
+
584
+ This system IS the marionette spool, just with a bouncy ball deployment shell:
585
+
586
+ ```
587
+ SYSTEM EQUIVALENCE
588
+ ══════════════════
589
+
590
+ STANDARD MARIONETTE RICOCHET DEPLOYMENT
591
+ (hand throw) (bouncy ball)
592
+
593
+ ┌───────┐ ┌───────────────┐
594
+ │SPOOL │ │ SUPERBALL │
595
+ │ │ ← Human │ ┌───────────┐ │
596
+ │ BRAIN │ throws │ │ SPOOL │ │ ← Human
597
+ │ │ │ │ BRAIN │ │ throws
598
+ └───────┘ │ └───────────┘ │
599
+ │ └───────┬───────┘
600
+ │ │
601
+ ▼ ▼
602
+ UNRAVEL by UNRAVEL by
603
+ centrifugal force IMPACT + BOUNCE
604
+ from throw spin + centrifugal
605
+ │ │
606
+ ▼ ▼
607
+ ┌───────────────┐ ┌───────────────┐
608
+ │ MARIONETTE │ │ MARIONETTE │
609
+ │ FLIGHT MODE │ ═════ │ FLIGHT MODE │
610
+ └───────────────┘ └───────────────┘
611
+
612
+ SAME SYSTEM!
613
+ (different deployment trigger)
614
+ ```
615
+
616
+ ---
617
+
618
+ ## 10. VARIANT: BOLA MODE CASCADE
619
+
620
+ For the full bola experience - throw MULTIPLE balls that link mid-air:
621
+
622
+ ```
623
+ MULTI-BALL BOLA ERUPTION
624
+ ════════════════════════
625
+
626
+ THROW: BOUNCE: LINK:
627
+
628
+ ○ ○ ○ ⛵ ⛵ ⛵ ⛵ ⛵───────⛵
629
+ \ │ / \│/ \│/ \ /
630
+ \ │ / ◎──��────◎ \ /
631
+ \│/ │ │ \ /
632
+ ▼ ↑ ↑ \ /
633
+ ═══════════ ═══════════════════ ════╋════
634
+
635
+ MAGNETIC ⛵───┼───⛵
636
+ DOCKING │
637
+ ════╬════
638
+
639
+ FULL BOLA
640
+ CONSTELLATION!
641
+ ```
642
+
643
+ ---
644
+
645
+ ## 11. IMPLEMENTATION NOTES
646
+
647
+ ### Python Module Location
648
+ `src/physics/ricochet_deployment.py`
649
+
650
+ ### Key Classes
651
+ - `SuperballShell` - Rubber shell physics + latch mechanism
652
+ - `ImpactTrigger` - G-force detection and timing
653
+ - `BounceDynamics` - Restitution and rebound calculation
654
+ - `EruptionSequencer` - Shell separation + sail payout timing
655
+ - `RicochetMarionette` - Full integrated system
656
+
657
+ ### Integration Points
658
+ - `src/physics/marionette_spool.py` - Core spool mechanics
659
+ - `src/physics/slingshot_dynamics.py` - Bola physics
660
+ - `src/physics/tether_dynamics.py` - Cable payout
661
+ - `src/ai/dreamer_interface.py` - Champion brain
662
+
663
+ ---
664
+
665
+ ## 12. FUTURE WORK
666
+
667
+ 1. **Multi-bounce recovery** - What if first bounce fails to trigger?
668
+ 2. **Angle compensation** - Non-vertical bounces, spin correction
669
+ 3. **Water landing variant** - Buoyant shell for maritime ops
670
+ 4. **Sound-triggered variant** - Clap or whistle to deploy (no ground needed)
671
+
672
+ ---
673
+
674
+ *"The ground is not the enemy. The ground is the launch pad."*
675
+
676
+ **— RICOCHET-001 Design Philosophy**
src/physics/__init__.py CHANGED
@@ -40,6 +40,15 @@ from .cable_geometry import (
40
  OPERATIONAL_SECTORS
41
  )
42
 
 
 
 
 
 
 
 
 
 
43
  __all__ = [
44
  # Tether
45
  'TetherConstraint',
@@ -64,4 +73,11 @@ __all__ = [
64
  'OperationalSector',
65
  'TangleState',
66
  'OPERATIONAL_SECTORS',
 
 
 
 
 
 
 
67
  ]
 
40
  OPERATIONAL_SECTORS
41
  )
42
 
43
+ from .ricochet_deployment import (
44
+ RicochetMarionette,
45
+ DeploymentState,
46
+ BounceDynamics,
47
+ ImpactTrigger,
48
+ ShellSeparator,
49
+ SailPayout,
50
+ )
51
+
52
  __all__ = [
53
  # Tether
54
  'TetherConstraint',
 
73
  'OperationalSector',
74
  'TangleState',
75
  'OPERATIONAL_SECTORS',
76
+ # Ricochet Deployment (Bouncy Ball Bola)
77
+ 'RicochetMarionette',
78
+ 'DeploymentState',
79
+ 'BounceDynamics',
80
+ 'ImpactTrigger',
81
+ 'ShellSeparator',
82
+ 'SailPayout',
83
  ]
src/physics/ricochet_deployment.py ADDED
@@ -0,0 +1,936 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Ricochet Deployment System
3
+ ==========================
4
+ BOUNCY BALL BOLA ERUPTION - Throw it at the ground, it bounces, SAILS DEPLOY!
5
+
6
+ The RICOCHET-001 system uses a high-restitution rubber shell to:
7
+ 1. Store the marionette spool system in compressed form
8
+ 2. Trigger deployment on ground impact (G-force activated latch)
9
+ 3. Harvest bounce energy as FREE upward launch velocity
10
+ 4. Unfurl sails during the upward trajectory
11
+
12
+ Physics:
13
+ - Polybutadiene rubber shell (coefficient of restitution ~0.87)
14
+ - G-force triggered latch (80-500G operating range)
15
+ - Centrifugal sail unfurling during bounce
16
+ - Champion brain activates mid-flight
17
+
18
+ THROW → IMPACT → BOUNCE → ERUPTION → MARIONETTE MODE
19
+ ◉ ▄◉▄ ◉ ⛵⛵⛵ ⛵ ⛵
20
+ │ ═════ ↑ \│/ \ /
21
+ ▼ ◉ \ /
22
+ ═════ ┌──┴──┐
23
+ │BRAIN│
24
+ └─────┘
25
+
26
+ Source: blueprints/RICOCHET-001_bouncy_ball_deployment.md
27
+ """
28
+
29
+ import numpy as np
30
+ from dataclasses import dataclass, field
31
+ from typing import Dict, List, Tuple, Optional, Callable
32
+ from enum import Enum, auto
33
+
34
+
35
+ class DeploymentState(Enum):
36
+ """Ricochet system operational states."""
37
+ DORMANT = auto() # Pre-throw, all systems inactive
38
+ BALLISTIC_DOWN = auto() # In flight, approaching ground
39
+ IMPACT = auto() # Ground contact, compression phase
40
+ BOUNCE = auto() # Rebound phase, latch triggered
41
+ ERUPTING = auto() # Shell separating, sails deploying
42
+ UNFURLING = auto() # Cables paying out, sails extending
43
+ STABILIZING = auto() # Champion brain taking control
44
+ MARIONETTE = auto() # Full autonomous flight
45
+
46
+
47
+ class ShellState(Enum):
48
+ """Rubber shell integrity states."""
49
+ INTACT = auto() # Closed, protecting payload
50
+ TRIGGERED = auto() # Latch released
51
+ SEPARATING = auto() # Petals opening
52
+ DETACHED = auto() # Petals fully separated
53
+
54
+
55
+ class LatchState(Enum):
56
+ """G-force triggered latch states."""
57
+ ARMED = auto() # Ready to trigger
58
+ TRIGGERED = auto() # G-force threshold exceeded
59
+ RELEASED = auto() # Latch mechanism opened
60
+
61
+
62
+ @dataclass
63
+ class RubberShellConfig:
64
+ """Configuration for the superball rubber shell."""
65
+ # Geometry
66
+ diameter: float = 0.20 # meters (20cm default)
67
+ shell_thickness: float = 0.008 # meters (8mm rubber)
68
+
69
+ # Material properties (polybutadiene rubber)
70
+ restitution_coefficient: float = 0.87 # Bounce efficiency
71
+ rubber_density: float = 920.0 # kg/m³
72
+
73
+ # Mass budget
74
+ shell_mass: float = 0.35 # kg (rubber shell only)
75
+ payload_mass: float = 1.15 # kg (spool + sails + brain)
76
+
77
+ @property
78
+ def total_mass(self) -> float:
79
+ return self.shell_mass + self.payload_mass
80
+
81
+ @property
82
+ def inner_diameter(self) -> float:
83
+ return self.diameter - 2 * self.shell_thickness
84
+
85
+
86
+ @dataclass
87
+ class LatchConfig:
88
+ """Configuration for the G-force triggered latch."""
89
+ trigger_g_min: float = 80.0 # Minimum G to trigger (prevents accidental)
90
+ trigger_g_max: float = 500.0 # Maximum survivable G
91
+ trigger_duration: float = 0.001 # Seconds of sustained G required
92
+ release_time: float = 0.005 # Seconds for mechanical release
93
+
94
+
95
+ @dataclass
96
+ class SailDeployConfig:
97
+ """Configuration for sail deployment during bounce."""
98
+ num_sails: int = 4
99
+ sail_mass: float = 0.15 # kg per sail
100
+ cable_length: float = 2.0 # meters max extension
101
+ cable_stiffness: float = 5000 # N/m
102
+ drum_brake_torque: float = 0.5 # N*m max braking
103
+
104
+ # Sail geometry
105
+ sail_area: float = 0.3 # m² per sail
106
+ sail_aspect_ratio: float = 4.0 # span²/area
107
+
108
+
109
+ @dataclass
110
+ class BrainBootConfig:
111
+ """Configuration for Champion brain activation."""
112
+ wake_delay: float = 0.010 # seconds after impact detection
113
+ imu_init_time: float = 0.020 # seconds for IMU calibration
114
+ estimator_start: float = 0.030 # seconds to start state estimation
115
+ first_control: float = 0.050 # seconds to first control output
116
+ full_authority: float = 0.300 # seconds to full marionette control
117
+
118
+
119
+ @dataclass
120
+ class ImpactEvent:
121
+ """Captured data from ground impact."""
122
+ timestamp: float # seconds
123
+ position: np.ndarray # world position of impact
124
+ velocity_in: np.ndarray # velocity at impact (m/s)
125
+ velocity_out: np.ndarray # velocity after bounce (m/s)
126
+ peak_g_force: float # maximum G experienced
127
+ contact_duration: float # seconds on ground
128
+ surface_normal: np.ndarray # ground surface normal
129
+ latch_triggered: bool # did G exceed threshold?
130
+
131
+
132
+ @dataclass
133
+ class DeploymentTelemetry:
134
+ """Real-time deployment status."""
135
+ state: DeploymentState
136
+ shell_state: ShellState
137
+ latch_state: LatchState
138
+
139
+ # Position/velocity
140
+ position: np.ndarray = field(default_factory=lambda: np.zeros(3))
141
+ velocity: np.ndarray = field(default_factory=lambda: np.zeros(3))
142
+ angular_velocity: np.ndarray = field(default_factory=lambda: np.zeros(3))
143
+
144
+ # Deployment progress
145
+ shell_separation_fraction: float = 0.0 # 0=closed, 1=fully open
146
+ cable_payout_fractions: np.ndarray = field(default_factory=lambda: np.zeros(4))
147
+
148
+ # Brain status
149
+ brain_booted: bool = False
150
+ brain_authority: float = 0.0 # 0=none, 1=full control
151
+
152
+ # Impact data
153
+ last_impact: Optional[ImpactEvent] = None
154
+
155
+
156
+ class ImpactTrigger:
157
+ """
158
+ G-force based latch trigger system.
159
+
160
+ Monitors acceleration and triggers when sustained G-force
161
+ exceeds threshold. Prevents accidental triggers from handling.
162
+ """
163
+
164
+ def __init__(self, config: LatchConfig):
165
+ self.config = config
166
+ self.state = LatchState.ARMED
167
+
168
+ # Detection state
169
+ self._g_history: List[Tuple[float, float]] = [] # (time, g_force)
170
+ self._trigger_start_time: Optional[float] = None
171
+
172
+ def reset(self):
173
+ """Reset to armed state."""
174
+ self.state = LatchState.ARMED
175
+ self._g_history.clear()
176
+ self._trigger_start_time = None
177
+
178
+ def update(self, acceleration: np.ndarray, dt: float, current_time: float) -> bool:
179
+ """
180
+ Update trigger with current acceleration.
181
+
182
+ Args:
183
+ acceleration: Current acceleration vector (m/s²)
184
+ dt: Time step
185
+ current_time: Current simulation time
186
+
187
+ Returns:
188
+ True if latch just triggered this frame
189
+ """
190
+ if self.state == LatchState.RELEASED:
191
+ return False
192
+
193
+ # Calculate G-force magnitude
194
+ g_force = np.linalg.norm(acceleration) / 9.81
195
+
196
+ # Store in history
197
+ self._g_history.append((current_time, g_force))
198
+
199
+ # Trim old history (keep last 50ms)
200
+ cutoff = current_time - 0.050
201
+ self._g_history = [(t, g) for t, g in self._g_history if t > cutoff]
202
+
203
+ # Check trigger condition
204
+ if g_force >= self.config.trigger_g_min:
205
+ if self._trigger_start_time is None:
206
+ self._trigger_start_time = current_time
207
+
208
+ # Check if sustained long enough
209
+ # For impact events, we treat single-frame high-G as valid trigger
210
+ sustained_time = current_time - self._trigger_start_time
211
+ if sustained_time >= self.config.trigger_duration or g_force >= self.config.trigger_g_min * 1.5:
212
+ # Either sustained long enough OR so strong it triggers immediately
213
+ self.state = LatchState.TRIGGERED
214
+ return True
215
+ else:
216
+ # Reset trigger timer if G dropped
217
+ self._trigger_start_time = None
218
+
219
+ return False
220
+
221
+ def release(self):
222
+ """Complete latch release after trigger."""
223
+ self.state = LatchState.RELEASED
224
+
225
+ def get_peak_g(self) -> float:
226
+ """Get peak G-force from history."""
227
+ if not self._g_history:
228
+ return 0.0
229
+ return max(g for _, g in self._g_history)
230
+
231
+
232
+ class BounceDynamics:
233
+ """
234
+ Physics engine for rubber ball impact and bounce.
235
+
236
+ Models:
237
+ - Hertzian contact mechanics (compression)
238
+ - Viscoelastic rebound (energy return)
239
+ - Surface normal reflection
240
+ """
241
+
242
+ def __init__(self, shell_config: RubberShellConfig):
243
+ self.config = shell_config
244
+
245
+ def compute_bounce(self,
246
+ velocity_in: np.ndarray,
247
+ surface_normal: np.ndarray = None) -> Tuple[np.ndarray, float, float]:
248
+ """
249
+ Calculate bounce velocity from impact.
250
+
251
+ Args:
252
+ velocity_in: Impact velocity vector (m/s)
253
+ surface_normal: Ground normal (default: [0, 0, 1] = flat ground)
254
+
255
+ Returns:
256
+ velocity_out: Rebound velocity vector (m/s)
257
+ peak_g: Peak G-force during impact
258
+ contact_time: Ground contact duration (s)
259
+ """
260
+ if surface_normal is None:
261
+ surface_normal = np.array([0.0, 0.0, 1.0])
262
+ surface_normal = surface_normal / np.linalg.norm(surface_normal)
263
+
264
+ # Decompose velocity into normal and tangential
265
+ v_normal_mag = np.dot(velocity_in, surface_normal)
266
+ v_normal = v_normal_mag * surface_normal
267
+ v_tangent = velocity_in - v_normal
268
+
269
+ # Only bounce if approaching ground (negative normal component)
270
+ if v_normal_mag >= 0:
271
+ # Not approaching ground, no bounce
272
+ return velocity_in.copy(), 0.0, 0.0
273
+
274
+ # Reflect normal component with restitution loss
275
+ v_normal_out = -v_normal * self.config.restitution_coefficient
276
+
277
+ # Tangential component mostly preserved (slight friction loss)
278
+ friction_factor = 0.95
279
+ v_tangent_out = v_tangent * friction_factor
280
+
281
+ # Combine for output velocity
282
+ velocity_out = v_normal_out + v_tangent_out
283
+
284
+ # Contact time (Hertzian approximation for rubber)
285
+ # Typically 10-20ms for rubber ball
286
+ impact_speed = abs(v_normal_mag)
287
+ contact_time = self._estimate_contact_time(impact_speed)
288
+
289
+ # Peak G-force
290
+ delta_v = abs(v_normal_mag) + np.linalg.norm(v_normal_out)
291
+ peak_acceleration = delta_v / contact_time
292
+ peak_g = peak_acceleration / 9.81
293
+
294
+ return velocity_out, peak_g, contact_time
295
+
296
+ def _estimate_contact_time(self, impact_speed: float) -> float:
297
+ """
298
+ Estimate ground contact duration based on impact speed.
299
+
300
+ Faster impacts = shorter contact (ball compresses faster).
301
+ """
302
+ # Empirical model for rubber ball
303
+ # Base contact time ~15ms, decreases with speed
304
+ base_time = 0.015
305
+ speed_factor = 1.0 / (1.0 + impact_speed / 20.0)
306
+ return base_time * speed_factor + 0.005 # Minimum 5ms
307
+
308
+ def compute_compression(self, impact_speed: float) -> float:
309
+ """
310
+ Calculate shell compression during impact.
311
+
312
+ Returns fraction of diameter compressed (0-0.5).
313
+ """
314
+ # Simplified spring model
315
+ # Higher speed = more compression
316
+ max_compression = 0.4 # 40% of diameter max
317
+ compression = min(max_compression, impact_speed / 50.0)
318
+ return compression
319
+
320
+
321
+ class ShellSeparator:
322
+ """
323
+ Models shell petal separation after latch release.
324
+
325
+ The shell splits along pre-scored seams like a blooming flower.
326
+ Petals carry sail tips outward as they separate.
327
+ """
328
+
329
+ def __init__(self, num_petals: int = 4):
330
+ self.num_petals = num_petals
331
+ self.state = ShellState.INTACT
332
+
333
+ # Separation dynamics
334
+ self.separation_fraction = 0.0
335
+ self.separation_velocity = 0.0 # m/s radial
336
+
337
+ # Petal states
338
+ self.petal_angles: np.ndarray = np.linspace(0, 2*np.pi, num_petals, endpoint=False)
339
+ self.petal_positions: List[np.ndarray] = [np.zeros(3) for _ in range(num_petals)]
340
+
341
+ # Physics params
342
+ self.spring_force = 50.0 # N initial separation spring
343
+ self.petal_mass = 0.08 # kg per petal
344
+ self.drag_coefficient = 0.5 # Air resistance on petals
345
+
346
+ def trigger(self):
347
+ """Initiate shell separation."""
348
+ if self.state == ShellState.INTACT:
349
+ self.state = ShellState.TRIGGERED
350
+ self.separation_velocity = 2.0 # Initial separation speed m/s
351
+
352
+ def update(self, dt: float, airspeed: float = 0.0):
353
+ """
354
+ Update shell separation physics.
355
+
356
+ Args:
357
+ dt: Time step
358
+ airspeed: Current airspeed for drag calculation
359
+ """
360
+ if self.state == ShellState.INTACT:
361
+ return
362
+
363
+ if self.state == ShellState.TRIGGERED:
364
+ self.state = ShellState.SEPARATING
365
+
366
+ if self.state == ShellState.SEPARATING:
367
+ # Spring force decreases with separation
368
+ spring_remaining = max(0, 1.0 - self.separation_fraction)
369
+ spring_accel = (self.spring_force * spring_remaining) / self.petal_mass
370
+
371
+ # Air drag on petals (opposes motion)
372
+ drag_accel = 0.5 * 1.225 * self.drag_coefficient * 0.01 * \
373
+ self.separation_velocity**2 / self.petal_mass
374
+
375
+ # Net acceleration
376
+ accel = spring_accel - drag_accel
377
+
378
+ # Integrate
379
+ self.separation_velocity += accel * dt
380
+ self.separation_velocity = max(0, self.separation_velocity) # No negative
381
+
382
+ # Update separation fraction (assuming 0.5m max separation)
383
+ max_separation = 0.5 # meters
384
+ self.separation_fraction += (self.separation_velocity * dt) / max_separation
385
+
386
+ if self.separation_fraction >= 1.0:
387
+ self.separation_fraction = 1.0
388
+ self.state = ShellState.DETACHED
389
+
390
+ def get_petal_positions(self, core_position: np.ndarray,
391
+ core_orientation: np.ndarray = None) -> List[np.ndarray]:
392
+ """Get world positions of all petals."""
393
+ if self.state == ShellState.INTACT:
394
+ return [core_position.copy() for _ in range(self.num_petals)]
395
+
396
+ # Petals move radially outward
397
+ separation_distance = self.separation_fraction * 0.5 # meters
398
+
399
+ positions = []
400
+ for i, angle in enumerate(self.petal_angles):
401
+ # Radial direction in XY plane
402
+ direction = np.array([np.cos(angle), np.sin(angle), 0.0])
403
+ petal_pos = core_position + direction * separation_distance
404
+ positions.append(petal_pos)
405
+
406
+ return positions
407
+
408
+
409
+ class SailPayout:
410
+ """
411
+ Manages cable payout from drum during deployment.
412
+
413
+ Centrifugal force pulls sails outward, paying out cable
414
+ from spring-loaded drums. Champion brain can brake drums
415
+ to control payout rate.
416
+ """
417
+
418
+ def __init__(self, config: SailDeployConfig):
419
+ self.config = config
420
+
421
+ # Per-sail state
422
+ self.cable_lengths = np.zeros(config.num_sails) # Current payout
423
+ self.cable_rates = np.zeros(config.num_sails) # Payout velocity
424
+ self.sail_positions = [np.zeros(3) for _ in range(config.num_sails)]
425
+ self.sail_velocities = [np.zeros(3) for _ in range(config.num_sails)]
426
+
427
+ # Sail angles (radially distributed)
428
+ self.sail_angles = np.linspace(0, 2*np.pi, config.num_sails, endpoint=False)
429
+
430
+ # Drum braking (controlled by Champion brain)
431
+ self.drum_brakes = np.zeros(config.num_sails) # 0=free, 1=full brake
432
+
433
+ def update(self,
434
+ core_position: np.ndarray,
435
+ core_velocity: np.ndarray,
436
+ angular_velocity: np.ndarray,
437
+ dt: float):
438
+ """
439
+ Update sail positions based on centrifugal unfurling.
440
+
441
+ Args:
442
+ core_position: Spool core world position
443
+ core_velocity: Spool core world velocity
444
+ angular_velocity: Core angular velocity (rad/s)
445
+ dt: Time step
446
+ """
447
+ spin_rate = np.linalg.norm(angular_velocity)
448
+
449
+ for i in range(self.config.num_sails):
450
+ # Current radial position
451
+ r = self.cable_lengths[i] + 0.1 # Minimum radius 10cm
452
+
453
+ # Centrifugal acceleration
454
+ a_cent = spin_rate**2 * r
455
+
456
+ # Sail drag (opposes radial motion)
457
+ v_radial = self.cable_rates[i]
458
+ drag_force = 0.5 * 1.225 * 0.5 * self.config.sail_area * v_radial**2
459
+ drag_accel = drag_force / self.config.sail_mass
460
+ if v_radial > 0:
461
+ drag_accel = -drag_accel # Opposes motion
462
+
463
+ # Drum brake force
464
+ brake_accel = self.drum_brakes[i] * self.config.drum_brake_torque / \
465
+ (self.config.sail_mass * r + 0.01)
466
+
467
+ # Cable tension (if at max length)
468
+ cable_limit_accel = 0.0
469
+ if self.cable_lengths[i] >= self.config.cable_length:
470
+ # Spring back
471
+ overshoot = self.cable_lengths[i] - self.config.cable_length
472
+ cable_limit_accel = -self.config.cable_stiffness * overshoot / self.config.sail_mass
473
+
474
+ # Net radial acceleration
475
+ a_radial = a_cent + drag_accel - brake_accel + cable_limit_accel
476
+
477
+ # Integrate cable payout
478
+ self.cable_rates[i] += a_radial * dt
479
+ self.cable_rates[i] = max(0, self.cable_rates[i]) # No negative payout
480
+
481
+ self.cable_lengths[i] += self.cable_rates[i] * dt
482
+ self.cable_lengths[i] = np.clip(self.cable_lengths[i], 0, self.config.cable_length * 1.05)
483
+
484
+ # Update sail world position
485
+ angle = self.sail_angles[i]
486
+ radial_dir = np.array([np.cos(angle), np.sin(angle), 0.0])
487
+ self.sail_positions[i] = core_position + radial_dir * (self.cable_lengths[i] + 0.1)
488
+
489
+ # Sail velocity = core velocity + tangential velocity from spin
490
+ tangent_dir = np.array([-np.sin(angle), np.cos(angle), 0.0])
491
+ tangent_speed = spin_rate * (self.cable_lengths[i] + 0.1)
492
+ self.sail_velocities[i] = core_velocity + tangent_dir * tangent_speed
493
+
494
+ def get_deployment_fraction(self) -> float:
495
+ """Get average deployment fraction across all sails."""
496
+ return np.mean(self.cable_lengths) / self.config.cable_length
497
+
498
+ def apply_brake(self, sail_index: int, brake_force: float):
499
+ """Apply brake to specific sail drum."""
500
+ self.drum_brakes[sail_index] = np.clip(brake_force, 0, 1)
501
+
502
+ def release_all_brakes(self):
503
+ """Release all drum brakes for free unfurling."""
504
+ self.drum_brakes[:] = 0.0
505
+
506
+
507
+ class ChampionBootSequencer:
508
+ """
509
+ Manages Champion brain boot sequence during deployment.
510
+
511
+ The brain activates on impact detection and progressively
512
+ gains authority over the marionette system.
513
+ """
514
+
515
+ def __init__(self, config: BrainBootConfig):
516
+ self.config = config
517
+
518
+ # Boot state
519
+ self.boot_started = False
520
+ self.boot_complete = False
521
+ self.boot_start_time = 0.0
522
+
523
+ # Subsystem status
524
+ self.imu_ready = False
525
+ self.estimator_ready = False
526
+ self.control_ready = False
527
+
528
+ # Authority ramp (0 = no control, 1 = full authority)
529
+ self.authority = 0.0
530
+
531
+ def start_boot(self, current_time: float):
532
+ """Begin boot sequence on impact detection."""
533
+ if not self.boot_started:
534
+ self.boot_started = True
535
+ self.boot_start_time = current_time
536
+
537
+ def update(self, current_time: float) -> Dict[str, bool]:
538
+ """
539
+ Update boot sequence progress.
540
+
541
+ Returns:
542
+ Status dict of subsystem readiness
543
+ """
544
+ if not self.boot_started:
545
+ return {'booting': False, 'imu': False, 'estimator': False, 'control': False}
546
+
547
+ elapsed = current_time - self.boot_start_time
548
+
549
+ # IMU initialization
550
+ if elapsed >= self.config.imu_init_time:
551
+ self.imu_ready = True
552
+
553
+ # State estimator
554
+ if elapsed >= self.config.estimator_start:
555
+ self.estimator_ready = True
556
+
557
+ # Control system
558
+ if elapsed >= self.config.first_control:
559
+ self.control_ready = True
560
+
561
+ # Authority ramp-up (linear from first_control to full_authority)
562
+ if elapsed >= self.config.first_control:
563
+ ramp_duration = self.config.full_authority - self.config.first_control
564
+ ramp_progress = (elapsed - self.config.first_control) / ramp_duration
565
+ self.authority = np.clip(ramp_progress, 0, 1)
566
+
567
+ if elapsed >= self.config.full_authority:
568
+ self.boot_complete = True
569
+
570
+ return {
571
+ 'booting': True,
572
+ 'imu': self.imu_ready,
573
+ 'estimator': self.estimator_ready,
574
+ 'control': self.control_ready,
575
+ 'authority': self.authority
576
+ }
577
+
578
+
579
+ class RicochetMarionette:
580
+ """
581
+ Complete RICOCHET-001 bouncy ball deployment system.
582
+
583
+ Integrates:
584
+ - Rubber shell physics (bounce)
585
+ - G-force triggered latch
586
+ - Shell separation
587
+ - Sail payout
588
+ - Champion brain boot
589
+
590
+ Usage:
591
+ ricochet = RicochetMarionette()
592
+ ricochet.throw(position, velocity, spin)
593
+
594
+ while simulating:
595
+ ricochet.step(dt)
596
+ telemetry = ricochet.get_telemetry()
597
+ """
598
+
599
+ def __init__(self,
600
+ shell_config: RubberShellConfig = None,
601
+ latch_config: LatchConfig = None,
602
+ sail_config: SailDeployConfig = None,
603
+ brain_config: BrainBootConfig = None):
604
+
605
+ # Configs
606
+ self.shell_config = shell_config or RubberShellConfig()
607
+ self.latch_config = latch_config or LatchConfig()
608
+ self.sail_config = sail_config or SailDeployConfig()
609
+ self.brain_config = brain_config or BrainBootConfig()
610
+
611
+ # Subsystems
612
+ self.bounce_physics = BounceDynamics(self.shell_config)
613
+ self.trigger = ImpactTrigger(self.latch_config)
614
+ self.shell = ShellSeparator(num_petals=4)
615
+ self.sails = SailPayout(self.sail_config)
616
+ self.brain_boot = ChampionBootSequencer(self.brain_config)
617
+
618
+ # State
619
+ self.state = DeploymentState.DORMANT
620
+ self.position = np.zeros(3)
621
+ self.velocity = np.zeros(3)
622
+ self.angular_velocity = np.zeros(3)
623
+ self.orientation = np.eye(3) # Rotation matrix
624
+
625
+ # Simulation
626
+ self.time = 0.0
627
+ self.gravity = np.array([0.0, 0.0, -9.81])
628
+
629
+ # Impact history
630
+ self.impacts: List[ImpactEvent] = []
631
+
632
+ # Ground plane (default: z=0)
633
+ self.ground_height = 0.0
634
+ self.ground_normal = np.array([0.0, 0.0, 1.0])
635
+
636
+ def throw(self,
637
+ position: np.ndarray,
638
+ velocity: np.ndarray,
639
+ angular_velocity: np.ndarray = None):
640
+ """
641
+ Initialize throw of the bouncy ball.
642
+
643
+ Args:
644
+ position: Starting position (m)
645
+ velocity: Initial velocity vector (m/s)
646
+ angular_velocity: Initial spin (rad/s)
647
+ """
648
+ self.position = np.array(position, dtype=np.float64)
649
+ self.velocity = np.array(velocity, dtype=np.float64)
650
+ if angular_velocity is None:
651
+ self.angular_velocity = np.array([0, 0, 3.0], dtype=np.float64)
652
+ else:
653
+ self.angular_velocity = np.array(angular_velocity, dtype=np.float64)
654
+
655
+ self.state = DeploymentState.BALLISTIC_DOWN
656
+ self.time = 0.0
657
+
658
+ # Reset subsystems
659
+ self.trigger.reset()
660
+ self.shell = ShellSeparator(num_petals=4)
661
+ self.sails = SailPayout(self.sail_config)
662
+ self.brain_boot = ChampionBootSequencer(self.brain_config)
663
+ self.impacts.clear()
664
+
665
+ def step(self, dt: float):
666
+ """
667
+ Advance simulation by one time step.
668
+
669
+ Args:
670
+ dt: Time step in seconds
671
+ """
672
+ self.time += dt
673
+
674
+ if self.state == DeploymentState.DORMANT:
675
+ return
676
+
677
+ # Ballistic motion (before and during bounce)
678
+ if self.state in [DeploymentState.BALLISTIC_DOWN, DeploymentState.BOUNCE]:
679
+ self._step_ballistic(dt)
680
+
681
+ # Check for ground impact
682
+ if self.state == DeploymentState.BALLISTIC_DOWN:
683
+ if self.position[2] <= self.ground_height + self.shell_config.diameter / 2:
684
+ self._handle_impact()
685
+
686
+ # Shell separation
687
+ if self.state in [DeploymentState.ERUPTING, DeploymentState.UNFURLING]:
688
+ self.shell.update(dt, np.linalg.norm(self.velocity))
689
+
690
+ # Sail deployment physics (in air after bounce)
691
+ if self.state in [DeploymentState.UNFURLING, DeploymentState.STABILIZING]:
692
+ self._step_unfurling(dt)
693
+ self.sails.update(self.position, self.velocity, self.angular_velocity, dt)
694
+
695
+ # Check if fully deployed
696
+ if self.sails.get_deployment_fraction() > 0.95:
697
+ if self.state == DeploymentState.UNFURLING:
698
+ self.state = DeploymentState.STABILIZING
699
+
700
+ # Brain boot sequence
701
+ if self.state in [DeploymentState.BOUNCE, DeploymentState.ERUPTING,
702
+ DeploymentState.UNFURLING, DeploymentState.STABILIZING]:
703
+ status = self.brain_boot.update(self.time)
704
+ if status.get('authority', 0) >= 1.0:
705
+ self.state = DeploymentState.MARIONETTE
706
+
707
+ # Marionette mode physics (simplified - full physics in marionette_spool.py)
708
+ if self.state == DeploymentState.MARIONETTE:
709
+ self._step_marionette(dt)
710
+
711
+ def _step_ballistic(self, dt: float):
712
+ """Simple ballistic motion under gravity."""
713
+ # Velocity integration
714
+ self.velocity += self.gravity * dt
715
+
716
+ # Air drag (simplified)
717
+ speed = np.linalg.norm(self.velocity)
718
+ if speed > 0.1:
719
+ drag_coeff = 0.4
720
+ area = np.pi * (self.shell_config.diameter / 2)**2
721
+ drag_force = 0.5 * 1.225 * drag_coeff * area * speed**2
722
+ drag_accel = drag_force / self.shell_config.total_mass
723
+ drag_dir = -self.velocity / speed
724
+ self.velocity += drag_dir * drag_accel * dt
725
+
726
+ # Position integration
727
+ self.position += self.velocity * dt
728
+
729
+ def _step_unfurling(self, dt: float):
730
+ """
731
+ Physics during sail unfurling phase (post-bounce).
732
+
733
+ Sails are deploying, providing some drag/lift, but not yet
734
+ fully controlled by Champion brain.
735
+ """
736
+ # Gravity
737
+ self.velocity += self.gravity * dt
738
+
739
+ # Partial sail drag (increases with deployment)
740
+ deploy_frac = self.sails.get_deployment_fraction()
741
+ effective_area = deploy_frac * self.sail_config.num_sails * self.sail_config.sail_area
742
+
743
+ speed = np.linalg.norm(self.velocity)
744
+ if speed > 0.1 and effective_area > 0:
745
+ # Drag from deploying sails
746
+ drag_coeff = 0.8 # Partially deployed sails are draggy
747
+ drag_force = 0.5 * 1.225 * drag_coeff * effective_area * speed**2
748
+ drag_accel = drag_force / self.shell_config.payload_mass
749
+ drag_dir = -self.velocity / speed
750
+ self.velocity += drag_dir * drag_accel * dt
751
+
752
+ # Partial lift (if moving upward or wind present)
753
+ if self.velocity[2] > 0:
754
+ # Ascending - sails can catch air
755
+ lift_coeff = 0.3 * deploy_frac
756
+ lift_force = 0.5 * 1.225 * lift_coeff * effective_area * speed**2
757
+ lift_accel = lift_force / self.shell_config.payload_mass
758
+ self.velocity[2] += lift_accel * dt * 0.5 # Partial lift
759
+
760
+ # Position integration
761
+ self.position += self.velocity * dt
762
+
763
+ # Floor clamp (don't go through ground)
764
+ min_height = self.ground_height + 0.5 # Sails need clearance
765
+ if self.position[2] < min_height:
766
+ self.position[2] = min_height
767
+ self.velocity[2] = max(0, self.velocity[2])
768
+
769
+ # Spin decay
770
+ self.angular_velocity *= 0.999
771
+
772
+ def _handle_impact(self):
773
+ """Process ground impact event."""
774
+ self.state = DeploymentState.IMPACT
775
+
776
+ # Compute bounce
777
+ v_out, peak_g, contact_time = self.bounce_physics.compute_bounce(
778
+ self.velocity, self.ground_normal
779
+ )
780
+
781
+ # Check G-force trigger
782
+ # Simulate acceleration spike
783
+ accel_spike = np.array([0, 0, peak_g * 9.81])
784
+ triggered = self.trigger.update(accel_spike, contact_time, self.time)
785
+
786
+ # Record impact
787
+ impact = ImpactEvent(
788
+ timestamp=self.time,
789
+ position=self.position.copy(),
790
+ velocity_in=self.velocity.copy(),
791
+ velocity_out=v_out.copy(),
792
+ peak_g_force=peak_g,
793
+ contact_duration=contact_time,
794
+ surface_normal=self.ground_normal.copy(),
795
+ latch_triggered=triggered
796
+ )
797
+ self.impacts.append(impact)
798
+
799
+ # Apply bounce
800
+ self.velocity = v_out
801
+ self.position[2] = self.ground_height + self.shell_config.diameter / 2
802
+
803
+ # State transition
804
+ if triggered:
805
+ self.state = DeploymentState.BOUNCE
806
+ self.trigger.release()
807
+ self.shell.trigger()
808
+ self.brain_boot.start_boot(self.time)
809
+
810
+ # Quick transition to erupting
811
+ self.state = DeploymentState.ERUPTING
812
+
813
+ # Then to unfurling
814
+ self.state = DeploymentState.UNFURLING
815
+ else:
816
+ # Bounce but no trigger - go back to ballistic
817
+ self.state = DeploymentState.BALLISTIC_DOWN
818
+
819
+ def _step_marionette(self, dt: float):
820
+ """
821
+ Simplified marionette physics.
822
+
823
+ Full implementation should integrate with marionette_spool.py
824
+ """
825
+ # Basic gravity + lift
826
+ lift_coefficient = 0.3 * self.sails.get_deployment_fraction()
827
+ wind = np.array([5.0, 0.0, 0.0]) # Assume constant wind
828
+
829
+ # Aerodynamic lift from sails
830
+ total_sail_area = self.sail_config.num_sails * self.sail_config.sail_area
831
+ wind_speed = np.linalg.norm(wind - self.velocity)
832
+ lift_force = 0.5 * 1.225 * lift_coefficient * total_sail_area * wind_speed**2
833
+ lift_accel = np.array([0, 0, lift_force / self.shell_config.payload_mass])
834
+
835
+ # Apply forces
836
+ self.velocity += (self.gravity + lift_accel) * dt
837
+ self.position += self.velocity * dt
838
+
839
+ # Angular damping
840
+ self.angular_velocity *= 0.99
841
+
842
+ def get_telemetry(self) -> DeploymentTelemetry:
843
+ """Get current deployment status."""
844
+ return DeploymentTelemetry(
845
+ state=self.state,
846
+ shell_state=self.shell.state,
847
+ latch_state=self.trigger.state,
848
+ position=self.position.copy(),
849
+ velocity=self.velocity.copy(),
850
+ angular_velocity=self.angular_velocity.copy(),
851
+ shell_separation_fraction=self.shell.separation_fraction,
852
+ cable_payout_fractions=self.sails.cable_lengths / self.sail_config.cable_length,
853
+ brain_booted=self.brain_boot.boot_complete,
854
+ brain_authority=self.brain_boot.authority,
855
+ last_impact=self.impacts[-1] if self.impacts else None
856
+ )
857
+
858
+ def get_sail_positions(self) -> List[np.ndarray]:
859
+ """Get world positions of all sails."""
860
+ return [pos.copy() for pos in self.sails.sail_positions]
861
+
862
+ def get_petal_positions(self) -> List[np.ndarray]:
863
+ """Get world positions of shell petals."""
864
+ return self.shell.get_petal_positions(self.position)
865
+
866
+
867
+ # =============================================================================
868
+ # DEMO / TEST
869
+ # =============================================================================
870
+
871
+ def demo_ricochet_deployment():
872
+ """
873
+ Demonstrate bouncy ball deployment.
874
+ """
875
+ print("=" * 60)
876
+ print("RICOCHET-001: BOUNCY BALL BOLA DEPLOYMENT DEMO")
877
+ print("=" * 60)
878
+
879
+ # Create system
880
+ ricochet = RicochetMarionette()
881
+
882
+ # Throw parameters - HARD throw at the ground!
883
+ start_pos = np.array([0.0, 0.0, 2.0]) # 2m above ground
884
+ throw_vel = np.array([5.0, 0.0, -20.0]) # Forward and DOWN hard
885
+ throw_spin = np.array([0.0, 0.0, 10.0]) # STRONG spin for centrifugal unfurl
886
+
887
+ print(f"\nThrow from: {start_pos}")
888
+ print(f"Velocity: {throw_vel} m/s")
889
+ print(f"Spin: {throw_spin} rad/s")
890
+
891
+ # Execute throw
892
+ ricochet.throw(start_pos, throw_vel, throw_spin)
893
+
894
+ # Simulate with progress reporting
895
+ dt = 0.001 # 1ms time step
896
+ max_time = 2.0
897
+
898
+ print("\nSimulating...")
899
+
900
+ state_changes = []
901
+
902
+ while ricochet.time < max_time:
903
+ prev_state = ricochet.state
904
+ ricochet.step(dt)
905
+
906
+ if ricochet.state != prev_state:
907
+ state_changes.append((ricochet.time, ricochet.state))
908
+ print(f" T={ricochet.time:.3f}s: {prev_state.name} → {ricochet.state.name}")
909
+
910
+ # Final status
911
+ telemetry = ricochet.get_telemetry()
912
+ print("\n" + "=" * 60)
913
+ print("FINAL STATUS")
914
+ print("=" * 60)
915
+ print(f"State: {telemetry.state.name}")
916
+ print(f"Position: {telemetry.position}")
917
+ print(f"Velocity: {telemetry.velocity}")
918
+ print(f"Shell separation: {telemetry.shell_separation_fraction:.1%}")
919
+ print(f"Sail deployment: {telemetry.cable_payout_fractions}")
920
+ print(f"Brain authority: {telemetry.brain_authority:.1%}")
921
+
922
+ if telemetry.last_impact:
923
+ print(f"\nImpact details:")
924
+ print(f" Peak G-force: {telemetry.last_impact.peak_g_force:.1f} G")
925
+ print(f" Contact time: {telemetry.last_impact.contact_duration*1000:.1f} ms")
926
+ print(f" Latch triggered: {telemetry.last_impact.latch_triggered}")
927
+
928
+ print("\nSail positions:")
929
+ for i, pos in enumerate(ricochet.get_sail_positions()):
930
+ print(f" Sail {i}: {pos}")
931
+
932
+ return ricochet
933
+
934
+
935
+ if __name__ == "__main__":
936
+ demo_ricochet_deployment()