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()
|