peiti commited on
Commit
1bf89d9
·
verified ·
1 Parent(s): 535eb68

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. embodied/__init__.py +0 -0
  2. embodied/anti_stasis.py +16 -0
  3. embodied/gemini_commander.py +416 -0
  4. embodied/goal_encoder.py +146 -0
  5. embodied/interactive_objects.py +374 -0
  6. embodied/intrinsic.py +290 -0
  7. embodied/language_goal_bridge.py +263 -0
  8. embodied/media_objects.py +276 -0
  9. embodied/observer.py +135 -0
  10. embodied/phantom_acc.py +110 -0
  11. embodied/phantom_biocor.py +120 -0
  12. embodied/phantom_cortex.py +121 -0
  13. embodied/phantom_memory.py +152 -0
  14. embodied/physics_world.py +651 -0
  15. embodied/pretrain/__init__.py +0 -0
  16. embodied/pretrain/bc_driving.py +416 -0
  17. embodied/pretrain/bc_ice_driving.py +419 -0
  18. embodied/pretrain/bc_pretrain.py +253 -0
  19. embodied/pretrain/motion_dataset.py +346 -0
  20. embodied/rl_loop.py +503 -0
  21. embodied/run_embodied.py +246 -0
  22. embodied/tasks/__init__.py +12 -0
  23. embodied/tasks/base.py +34 -0
  24. embodied/tasks/basketball.py +403 -0
  25. embodied/tasks/basketball_1v1.py +284 -0
  26. embodied/tasks/basketball_npc.py +287 -0
  27. embodied/tasks/drive_city.py +413 -0
  28. embodied/tasks/drive_city_echo.py +68 -0
  29. embodied/tasks/drive_city_polaris.py +107 -0
  30. embodied/tasks/household_ball.py +66 -0
  31. embodied/tasks/household_door.py +63 -0
  32. embodied/tasks/household_pick.py +99 -0
  33. embodied/tasks/household_sit.py +68 -0
  34. embodied/tasks/household_stack.py +117 -0
  35. embodied/tasks/household_sweep.py +78 -0
  36. embodied/tasks/ice_driving.py +358 -0
  37. embodied/tasks/ice_driving_polaris.py +164 -0
  38. embodied/tasks/maze.py +36 -0
  39. embodied/tasks/media_watch.py +96 -0
  40. embodied/tasks/obstacle_course.py +132 -0
  41. embodied/tasks/polaris_server_task.py +151 -0
  42. embodied/tasks/reach.py +57 -0
  43. embodied/tasks/reach_arm.py +61 -0
  44. embodied/tasks/shape_sorter.py +75 -0
  45. embodied/tasks/stairs.py +30 -0
  46. embodied/tasks/stand.py +27 -0
  47. embodied/tasks/terminator_factory.py +30 -0
  48. embodied/tasks/terminator_stand.py +92 -0
  49. embodied/tasks/terminator_tools.py +446 -0
  50. embodied/tasks/timed_nav.py +371 -0
embodied/__init__.py ADDED
File without changes
embodied/anti_stasis.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stable VoxForge re-export for anti-stasis control.
2
+
3
+ This wrapper intentionally avoids importing from a bundled top-level ``cogcore``
4
+ package. It resolves an external CogCore implementation when present and falls
5
+ back to ``voxforge_cogcore_compat`` when VoxForge is used alone.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from voxforge_cogcore_compat.resolver import export_anti_stasis_symbols, load_anti_stasis_api
10
+
11
+ _api = load_anti_stasis_api()
12
+ globals().update(_api.exports())
13
+ ANTI_STASIS_API_SOURCE = _api.source
14
+ ANTI_STASIS_API_DIAGNOSTICS = _api.to_dict()
15
+
16
+ __all__ = sorted(set(export_anti_stasis_symbols().keys()) | {"ANTI_STASIS_API_SOURCE", "ANTI_STASIS_API_DIAGNOSTICS"})
embodied/gemini_commander.py ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GeminiCommander — multimodální jazyková oblast pro Phantom embodied agenta.
3
+
4
+ Architektura (mozková analogie):
5
+ ┌──────────────┐ obrázky světa + instrukce ┌──────────────────┐
6
+ │ UŽIVATEL │ ─────────────────────────────── ▶│ GEMINI │
7
+ │ │ │ (jazyková oblast│
8
+ │ │ ◀─────────────────────────────── │ Broca/Wernicke)│
9
+ │ │ narrace + goal_vec[32] └────────┬─────────┘
10
+ └──────────────┘ │ goal_vec
11
+ ▼
12
+ ┌──────────────────────┐
13
+ │ RL AGENT │
14
+ │ (hippocampus / PFC) │
15
+ │ state[26+32] = [58] │
16
+ │ ExperienceReplay │
17
+ │ VerticalPentaMemory │
18
+ └──────────────────────┘
19
+
20
+ Multimodalita:
21
+ - Gemini dostane: pohled shora (PNG) + póza těla (PNG) + text instrukce
22
+ - Gemini vrátí: strukturovaný JSON → goal_vec[32] + naraci v češtině
23
+
24
+ Použití:
25
+ export GEMINI_API_KEY="..."
26
+ python -m embodied.gemini_commander --report
27
+ python -m embodied.gemini_commander "Zvedni hrnek a dej ho na pult"
28
+ python -m embodied.gemini_commander # interaktivní chat
29
+ """
30
+ from __future__ import annotations
31
+ import os, json, sys, argparse, logging, base64, io
32
+ from pathlib import Path
33
+ from typing import Optional, TYPE_CHECKING
34
+ import numpy as np
35
+
36
+ from embodied.goal_encoder import GoalSpec, encode_goal, GOAL_DIM, goal_for_task
37
+
38
+ if TYPE_CHECKING:
39
+ from PIL import Image as PILImage
40
+
41
+ log = logging.getLogger("phantom.gemini")
42
+
43
+ # ── Gemini REST client (přímé volání bez gRPC — funguje i v kontejnerech) ────
44
+
45
+ _GEMINI_REST_URL = (
46
+ "https://generativelanguage.googleapis.com/v1beta/models/"
47
+ "{model}:generateContent"
48
+ )
49
+ _DEFAULT_MODEL = "gemini-2.5-flash"
50
+
51
+
52
+ def _call_gemini(api_key: str, parts: list, model: str = _DEFAULT_MODEL) -> str:
53
+ """
54
+ Přímé volání Gemini REST API přes requests.
55
+ Obchází gRPC + SSL problémy v kontejnerech (verify=False).
56
+ parts může obsahovat stringy i inline_data dict (obrázky).
57
+ """
58
+ import requests, urllib3
59
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
60
+
61
+ content_parts = []
62
+ for p in parts:
63
+ if isinstance(p, str):
64
+ if p.strip():
65
+ content_parts.append({"text": p})
66
+ elif isinstance(p, dict) and "inline_data" in p:
67
+ content_parts.append({"inline_data": p["inline_data"]})
68
+
69
+ payload = {"contents": [{"parts": content_parts}]}
70
+ url = _GEMINI_REST_URL.format(model=model)
71
+
72
+ resp = requests.post(
73
+ url,
74
+ json=payload,
75
+ params={"key": api_key},
76
+ verify=False,
77
+ timeout=45,
78
+ )
79
+ resp.raise_for_status()
80
+ data = resp.json()
81
+ return data["candidates"][0]["content"]["parts"][0]["text"]
82
+
83
+
84
+ def _get_api_key() -> str:
85
+ key = os.environ.get("GEMINI_API_KEY", "")
86
+ if not key:
87
+ raise ValueError("Nastav GEMINI_API_KEY env proměnnou")
88
+ return key
89
+
90
+
91
+ # ── Systémový kontext ─────────────────────────────────────────────────────────
92
+
93
+ _SYSTEM_PROMPT = """
94
+ Jsi expertní koordinátor pro Phantom Learning — systém, kde virtuální humanoidní
95
+ robot učený metodami zpětnovazebního učení (RL) operuje ve fyzikálním simulátoru.
96
+
97
+ ARCHITEKTURA:
98
+ Ty (Gemini) = jazyková oblast mozku (Broca/Wernicke)
99
+ RL agent = hippocampus + prefrontální kůra (PFC)
100
+ Motorická kůra = 10-DOF pohybový systém se prsty
101
+
102
+ ROBOT:
103
+ - 22 segmentů: hlava, krk, trup, pánev, kyčle/kolena/chodidla (L+R),
104
+ ramena/lokty/ruce (L+R), 4 prsty na každé ruce
105
+ - Akce[10]: kyčle L/R, kolena L/R, ramena L/R, lokty L/R, grip L/R
106
+ - grip < -0.25 = uchopení objektu (pokud je ruka do 35 cm)
107
+
108
+ FYZIKA:
109
+ - Hrnek (keramika): rozbije se při nárazu > 4.5 m/s (pád z výšky > 1 m)
110
+ - Blok (dřevo): rozbije se při > 8.0 m/s
111
+ - Kovová klika: prakticky nezničitelná (> 14 m/s)
112
+
113
+ DOSTUPNÉ ÚLOHY:
114
+ stand, reach, walk, stairs, maze,
115
+ household_pick, household_door, household_stack, household_sweep, household_sit
116
+
117
+ DOMÁCNOST (household terrain):
118
+ - Stůl: x=10-14, z=14-18, povrch y=3 (hrnek zde)
119
+ - Pult: x=20-26, z=3-7, povrch y=4 (cíl pro household_pick)
120
+ - Židle: x=8-10, z=20-22, y=3
121
+ - Polička: x=2-4, z=8-22, y=1-7
122
+ - Dveře (mezera): x=13-17, z=0 (jih)
123
+
124
+ Vždy odpovídej ČESKY pokud není řečeno jinak.
125
+ """
126
+
127
+ _GOAL_SCHEMA_PROMPT = """
128
+ Na základě obrázků a instrukce vrať POUZE platný JSON v tomto formátu:
129
+ {
130
+ "target": "<cup|block|door|chair|table|counter|shelf|sink|none|any>",
131
+ "action": "<pick|carry|sweep|open|sit|walk|stand>",
132
+ "destination": "<counter|table|door|chair|floor|none>",
133
+ "phase": "<approach|grasp|carry|place>",
134
+ "urgency": <0.0-1.0>,
135
+ "grip": <-1.0 (pevný stisk) až 1.0 (otevřená ruka)>,
136
+ "speed": <0.0 (pomalu) až 1.0 (rychle)>,
137
+ "description": "<česky: co agent má dělat, max 1 věta>"
138
+ }
139
+ """
140
+
141
+
142
+ # ── GoalSpec parser ───────────────────────────────────────────────────────────
143
+
144
+ def _parse_goal_json(text: str) -> GoalSpec:
145
+ """Extrahuj JSON ze stringu (Gemini někdy přidá markdown)."""
146
+ text = text.strip()
147
+ # Odstraň markdown code block pokud existuje
148
+ if "```" in text:
149
+ start = text.find("{")
150
+ end = text.rfind("}") + 1
151
+ text = text[start:end]
152
+ try:
153
+ d = json.loads(text)
154
+ return GoalSpec(
155
+ target=str(d.get("target", "none")),
156
+ action=str(d.get("action", "walk")),
157
+ destination=str(d.get("destination", "none")),
158
+ phase=str(d.get("phase", "approach")),
159
+ urgency=float(d.get("urgency", 0.5)),
160
+ grip=float(d.get("grip", 0.0)),
161
+ speed=float(d.get("speed", 0.5)),
162
+ description=str(d.get("description", "")),
163
+ )
164
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
165
+ log.warning("Nepodařilo se parsovat GoalSpec JSON: %s\nText: %s", e, text[:200])
166
+ return GoalSpec(description=text[:100])
167
+
168
+
169
+ # ── Gemini Commander ──────────────────────────────────────────────────────────
170
+
171
+ class GeminiCommander:
172
+ """
173
+ Multimodální jazyková vrstva — Gemini jako jazyková oblast mozku.
174
+ Přijímá FPV obrázky světa + instrukci → vrací goal_vec[32] + naraci.
175
+ Používá přímé REST API (bez gRPC), funguje i v síťově izolovaných kontejnerech.
176
+ """
177
+
178
+ def __init__(self, log_path: Path = Path("logs/embodied_training.jsonl")):
179
+ self._api_key = _get_api_key()
180
+ self.log_path = log_path
181
+ self._history: list[dict] = []
182
+
183
+ # ── Multimodální goal generation ──────────────────────────────────────────
184
+
185
+ def get_goal(self,
186
+ instruction: str,
187
+ fpv_img=None,
188
+ minimap_img=None,
189
+ agent_pos=None,
190
+ target_pos=None,
191
+ ) -> tuple[GoalSpec, np.ndarray]:
192
+ """
193
+ Hlavní metoda: instrukce + obrázky → (GoalSpec, goal_vec[32]).
194
+
195
+ Posílá Gemini multimodálně:
196
+ 1. FPV obraz (PNG) — co agent SKUTEČNĚ VIDÍ ze svých očí
197
+ voxelový raycaster, ruce viditelné dole
198
+ 2. Minimap (PNG) — pohled shora pro prostorový kontext
199
+ 3. Text instrukce — co má agent dělat
200
+
201
+ Vrátí:
202
+ GoalSpec — strukturovaný cíl v češtině
203
+ np.ndarray[32] — goal vektor pro RL agenta (hippocampus/PFC)
204
+ """
205
+ from embodied.world_renderer import image_to_gemini_part
206
+ parts = [_SYSTEM_PROMPT, "\n\nINSTRUKCE: " + instruction, "\n"]
207
+
208
+ if fpv_img is not None:
209
+ parts.insert(1, image_to_gemini_part(fpv_img))
210
+ parts.insert(2,
211
+ "\n[POHLED Z OČÍ AGENTA — voxelový FPV raycaster. "
212
+ "Červené objekty = hrnky, oranžové = bloky, "
213
+ "zelené ruce = pravá ruka agenta (viditelná dole).]\n")
214
+
215
+ if minimap_img is not None:
216
+ parts.append(image_to_gemini_part(minimap_img))
217
+ parts.append(
218
+ "\n[MAPA SHORA — prostorový kontext. "
219
+ "Modrá tečka = agent, šipka = směr pohledu, "
220
+ "zelené X = cíl.]\n")
221
+
222
+ parts.append(_GOAL_SCHEMA_PROMPT)
223
+
224
+ try:
225
+ text = _call_gemini(self._api_key, parts)
226
+ spec = _parse_goal_json(text)
227
+ except Exception as e:
228
+ log.warning("Gemini goal generation selhal: %s", e)
229
+ spec = GoalSpec(description=f"fallback: {e}")
230
+
231
+ # Doplň relativní offset pokud víme kde je cíl a agent
232
+ if agent_pos is not None and target_pos is not None:
233
+ spec.rel_x = float(target_pos[0]) - float(agent_pos[0])
234
+ spec.rel_y = float(target_pos[1]) - float(agent_pos[1])
235
+ spec.rel_z = float(target_pos[2]) - float(agent_pos[2])
236
+
237
+ vec = encode_goal(spec)
238
+ return spec, vec
239
+
240
+ # ── Narativní popis stavu ──────────────────────────────────────────────────
241
+
242
+ def narrate(self,
243
+ stats: dict,
244
+ topdown_img=None,
245
+ episode: int = 0,
246
+ ) -> str:
247
+ """
248
+ Na základě obrázku světa + statistik epizody vygeneruj českou naraci.
249
+ Simuluje roli jazykové oblasti — interpretuje co se stalo motoricky.
250
+ """
251
+ stats_str = json.dumps(stats, ensure_ascii=False, indent=2)
252
+ parts = [
253
+ _SYSTEM_PROMPT,
254
+ f"\n\nStatistiky epizody {episode}:\n{stats_str}\n",
255
+ "\nPopiš česky co agent v této epizodě dělal, co se mu povedlo/nepovedlo, "
256
+ "a dej konkrétní doporučení pro příští epizodu. Max 3 věty.",
257
+ ]
258
+ if topdown_img is not None:
259
+ from embodied.world_renderer import image_to_gemini_part
260
+ parts.insert(2, image_to_gemini_part(topdown_img))
261
+ parts.insert(3, "\n[Finální stav světa]\n")
262
+
263
+ try:
264
+ return _call_gemini(self._api_key, parts).strip()
265
+ except Exception as e:
266
+ return f"[Narrace nedostupná: {e}]"
267
+
268
+ # ── Zpráva o tréninku ────────────────────────────────────────────────────
269
+
270
+ def report(self) -> str:
271
+ summary = _load_training_summary(self.log_path)
272
+ parts = [
273
+ _SYSTEM_PROMPT,
274
+ f"\n\nTréninkové data:\n{summary}\n\n"
275
+ "Napiš česky podrobnou zprávu: co se agent naučil, kde je slabý, "
276
+ "a navrhni 3 konkrétní zlepšení.",
277
+ ]
278
+ try:
279
+ return _call_gemini(self._api_key, parts).strip()
280
+ except Exception as e:
281
+ return f"[Report nedostupný: {e}]\n\n{summary}"
282
+
283
+ # ── Konverzační chat ─────────────────────────────────────────────────────
284
+
285
+ def chat(self, user_input: str, fpv_img=None) -> str:
286
+ summary = _load_training_summary(self.log_path)
287
+ parts = [
288
+ _SYSTEM_PROMPT,
289
+ f"\n\nAktuální data tréninku:\n{summary}\n",
290
+ ]
291
+ if fpv_img is not None:
292
+ from embodied.world_renderer import image_to_gemini_part
293
+ parts.append(image_to_gemini_part(fpv_img))
294
+ parts.append("\n[Aktuální FPV pohled agenta]\n")
295
+
296
+ parts.append(f"\nUžitel: {user_input}")
297
+ try:
298
+ answer = _call_gemini(self._api_key, parts).strip()
299
+ except Exception as e:
300
+ answer = f"[Chyba: {e}]"
301
+
302
+ self._history.append({"user": user_input, "assistant": answer})
303
+ return answer
304
+
305
+
306
+ # ── Samostatný goal_vec bez Gemini (fallback) ─────────────────────────────────
307
+
308
+ def get_goal_vec_fallback(task_name: str,
309
+ agent_pos=None, target_pos=None) -> np.ndarray:
310
+ """Vrátí předdefinovaný goal_vec bez volání Gemini API."""
311
+ spec = goal_for_task(task_name)
312
+ if agent_pos is not None and target_pos is not None:
313
+ spec.rel_x = float(target_pos[0]) - float(agent_pos[0])
314
+ spec.rel_y = float(target_pos[1]) - float(agent_pos[1])
315
+ spec.rel_z = float(target_pos[2]) - float(agent_pos[2])
316
+ return encode_goal(spec)
317
+
318
+
319
+ # ── Log helper ────────────────────────────────────────────────────────────────
320
+
321
+ def _load_training_summary(log_path: Path) -> str:
322
+ if not log_path.exists():
323
+ return "Žádný tréninkový log nenalezen."
324
+ lines = [json.loads(l) for l in log_path.read_text().splitlines() if l.strip()]
325
+ if not lines:
326
+ return "Log je prázdný."
327
+ from collections import defaultdict
328
+ tasks: dict[str, list] = defaultdict(list)
329
+ for e in lines:
330
+ tasks[e["task"]].append(e)
331
+ parts = [f"Celkem epizod: {len(lines)}\n"]
332
+ for tn, eps in tasks.items():
333
+ rewards = [e["total_reward"] for e in eps]
334
+ suc = sum(1 for e in eps if e.get("success"))
335
+ avg = sum(rewards) / len(rewards)
336
+ parts.append(
337
+ f" {tn}: {len(eps)} ep avg={avg:.1f} max={max(rewards):.1f} "
338
+ f"úspěch={suc} šum {eps[0]['noise']:.3f}→{eps[-1]['noise']:.3f}"
339
+ )
340
+ return "\n".join(parts)
341
+
342
+
343
+ # ── CLI ───────────────────────────────────────────────────────────────────────
344
+
345
+ def main():
346
+ logging.basicConfig(level=logging.WARNING)
347
+ parser = argparse.ArgumentParser(
348
+ description="Gemini multimodální commander pro Phantom agenta"
349
+ )
350
+ parser.add_argument("command", nargs="?",
351
+ help="Přirozený jazyk příkaz / otázka")
352
+ parser.add_argument("--report", action="store_true",
353
+ help="Vygeneruj zprávu o tréninku")
354
+ parser.add_argument("--goal", action="store_true",
355
+ help="Vygeneruj goal_vec pro danou instrukci")
356
+ parser.add_argument("--render", action="store_true",
357
+ help="Vykresli a ulož scénu (demo bez Gemini)")
358
+ parser.add_argument("--log", default="logs/embodied_training.jsonl")
359
+ parser.add_argument("--task", default="household_pick",
360
+ help="Úloha pro --goal bez Gemini")
361
+ args = parser.parse_args()
362
+
363
+ if args.render:
364
+ _demo_render()
365
+ return
366
+
367
+ try:
368
+ commander = GeminiCommander(log_path=Path(args.log))
369
+ except (ImportError, ValueError) as e:
370
+ print(f"Chyba inicializace Gemini: {e}")
371
+ print("Tip: export GEMINI_API_KEY='váš_klíč'")
372
+ sys.exit(1)
373
+
374
+ if args.report:
375
+ print(commander.report())
376
+ elif args.goal and args.command:
377
+ spec, vec = commander.get_goal(args.command)
378
+ print(f"GoalSpec: {spec}")
379
+ print(f"goal_vec shape: {vec.shape}")
380
+ print(f"goal_vec: {np.round(vec, 3).tolist()}")
381
+ elif args.command:
382
+ print(commander.chat(args.command))
383
+ else:
384
+ print("Phantom Gemini Commander (multimodální) — Ctrl+C pro ukončení\n")
385
+ while True:
386
+ try:
387
+ user_input = input("Ty: ").strip()
388
+ if not user_input:
389
+ continue
390
+ print(f"\nGemini: {commander.chat(user_input)}\n")
391
+ except KeyboardInterrupt:
392
+ print("\nKonec.")
393
+ break
394
+
395
+
396
+ def _demo_render():
397
+ """Demo: vykresli scénu bez Gemini (pro testování rendereru)."""
398
+ import sys; sys.path.insert(0, ".")
399
+ from embodied.physics_world import WorldEnv
400
+ from embodied.virtual_body import VirtualBody
401
+ from embodied.world_renderer import save_scene
402
+ from engine.physics.rigid_body import vec3
403
+
404
+ print("Inicializuji household svět...")
405
+ world = WorldEnv(seed=0, terrain_type="household")
406
+ body = VirtualBody(world.phys, origin=None)
407
+ body.reset(vec3(8.0, 2.0, 4.0))
408
+ world.add_interactive_object("cup", "cup", vec3(12.0, 3.5, 16.0),
409
+ size=vec3(0.08, 0.14, 0.08), mass=0.3)
410
+ paths = save_scene(world, body, "/tmp/phantom_scene",
411
+ target_pos=vec3(23.0, 4.5, 5.0))
412
+ print(f"Uloženo: {paths[0]}, {paths[1]}")
413
+
414
+
415
+ if __name__ == "__main__":
416
+ main()
embodied/goal_encoder.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GoalEncoder — převod strukturovaného výstupu Gemini na 32-dim float vektor.
3
+
4
+ Architektura (PFC/hippocampus analogie):
5
+ Gemini (jazyková oblast) → goal_vec[32] → RL agent (PFC/hippocampus)
6
+
7
+ Vektor má pevnou strukturu:
8
+ [0-9] : cílový objekt (one-hot 10 tříd)
9
+ [10-16] : typ akce (one-hot 7 tříd)
10
+ [17-22] : cílové místo (one-hot 6 tříd)
11
+ [23] : naléhavost (0–1)
12
+ [24-26] : relativní offset cíle od agenta (normalizovaný na ±1)
13
+ [27-29] : fáze úlohy (soft one-hot: approach/grasp/carry/place)
14
+ [30] : doporučená sila stisku (-1 = pevně, +1 = povolně)
15
+ [31] : faktor rychlosti (0 = pomalu, 1 = rychle)
16
+ """
17
+ from __future__ import annotations
18
+ import numpy as np
19
+ from dataclasses import dataclass, field
20
+ from typing import Optional
21
+
22
+
23
+ GOAL_DIM = 32
24
+
25
+ # ── Mapovací tabulky ──────────────────────────────────────────────────────────
26
+
27
+ _OBJECTS = ["cup", "block", "door", "chair", "table", "counter", "shelf", "sink", "none", "any"]
28
+ _ACTIONS = ["pick", "carry", "sweep", "open", "sit", "walk", "stand"]
29
+ _DESTS = ["counter", "table", "door", "chair", "floor", "none"]
30
+ _PHASES = ["approach", "grasp", "carry", "place"]
31
+
32
+
33
+ @dataclass
34
+ class GoalSpec:
35
+ """Strukturovaný cíl vrácený Gemini."""
36
+ target: str = "none" # cílový objekt
37
+ action: str = "walk" # akce
38
+ destination: str = "none" # kam
39
+ phase: str = "approach" # fáze
40
+ urgency: float = 0.5 # 0–1
41
+ grip: float = -0.5 # -1 (pevný) → +1 (povolný)
42
+ speed: float = 0.5 # 0 (pomalu) → 1 (rychle)
43
+ # Relativní offset cíle od agenta (volitelné, naplní WorldEnv)
44
+ rel_x: float = 0.0
45
+ rel_y: float = 0.0
46
+ rel_z: float = 0.0
47
+ # Popis pro logování
48
+ description: str = ""
49
+
50
+
51
+ def encode_goal(spec: GoalSpec) -> np.ndarray:
52
+ """GoalSpec → float32[32]."""
53
+ vec = np.zeros(GOAL_DIM, dtype=np.float32)
54
+
55
+ # [0-9] target object one-hot
56
+ obj_idx = _OBJECTS.index(spec.target) if spec.target in _OBJECTS else _OBJECTS.index("none")
57
+ vec[obj_idx] = 1.0
58
+
59
+ # [10-16] action one-hot
60
+ act_idx = _ACTIONS.index(spec.action) if spec.action in _ACTIONS else 0
61
+ vec[10 + act_idx] = 1.0
62
+
63
+ # [17-22] destination one-hot
64
+ dst_idx = _DESTS.index(spec.destination) if spec.destination in _DESTS else _DESTS.index("none")
65
+ vec[17 + dst_idx] = 1.0
66
+
67
+ # [23] urgency
68
+ vec[23] = float(np.clip(spec.urgency, 0.0, 1.0))
69
+
70
+ # [24-26] relative offset (normalized)
71
+ vec[24] = float(np.clip(spec.rel_x / 20.0, -1.0, 1.0))
72
+ vec[25] = float(np.clip(spec.rel_y / 5.0, -1.0, 1.0))
73
+ vec[26] = float(np.clip(spec.rel_z / 20.0, -1.0, 1.0))
74
+
75
+ # [27-29] fáze (soft — okolní fáze dostanou malou váhu)
76
+ ph_idx = _PHASES.index(spec.phase) if spec.phase in _PHASES else 0
77
+ for i, _ in enumerate(_PHASES):
78
+ dist = abs(i - ph_idx)
79
+ vec[27 + min(i, 2)] += max(0.0, 1.0 - dist * 0.4)
80
+ vec[27:30] /= (vec[27:30].sum() + 1e-8)
81
+
82
+ # [30] grip recommendation
83
+ vec[30] = float(np.clip(spec.grip, -1.0, 1.0))
84
+
85
+ # [31] speed
86
+ vec[31] = float(np.clip(spec.speed, 0.0, 1.0))
87
+
88
+ return vec
89
+
90
+
91
+ def decode_goal(vec: np.ndarray) -> GoalSpec:
92
+ """float32[32] → GoalSpec (pro debugging)."""
93
+ obj_idx = int(np.argmax(vec[0:10]))
94
+ act_idx = int(np.argmax(vec[10:17]))
95
+ dst_idx = int(np.argmax(vec[17:23]))
96
+ ph_idx = int(np.argmax(vec[27:30]))
97
+ return GoalSpec(
98
+ target=_OBJECTS[obj_idx],
99
+ action=_ACTIONS[act_idx],
100
+ destination=_DESTS[dst_idx],
101
+ phase=_PHASES[ph_idx],
102
+ urgency=float(vec[23]),
103
+ rel_x=float(vec[24]) * 20.0,
104
+ rel_y=float(vec[25]) * 5.0,
105
+ rel_z=float(vec[26]) * 20.0,
106
+ grip=float(vec[30]),
107
+ speed=float(vec[31]),
108
+ )
109
+
110
+
111
+ # ── Předdefinované cíle pro každou úlohu ─────────────────────────────────────
112
+
113
+ def goal_for_task(task_name: str) -> GoalSpec:
114
+ """Fallback goal_vec bez Gemini — pro trénink bez API klíče."""
115
+ defaults = {
116
+ "stand": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.3, grip=0.5, speed=0.2),
117
+ "reach": GoalSpec(target="none", action="walk", destination="none", phase="approach", urgency=0.6, grip=-0.2, speed=0.5),
118
+ "walk": GoalSpec(target="none", action="walk", destination="none", phase="approach", urgency=0.7, grip=0.3, speed=0.8),
119
+ "stairs": GoalSpec(target="none", action="walk", destination="none", phase="approach", urgency=0.8, grip=0.2, speed=0.6),
120
+ "maze": GoalSpec(target="none", action="walk", destination="none", phase="approach", urgency=0.9, grip=0.3, speed=0.7),
121
+ "household_pick": GoalSpec(target="cup", action="pick", destination="counter", phase="approach", urgency=0.8, grip=-0.8, speed=0.4),
122
+ "household_door": GoalSpec(target="door", action="open", destination="door", phase="approach", urgency=0.7, grip=-0.6, speed=0.5),
123
+ "household_stack": GoalSpec(target="block", action="pick", destination="table", phase="approach", urgency=0.8, grip=-0.8, speed=0.4),
124
+ "household_sweep": GoalSpec(target="cup", action="sweep", destination="floor", phase="approach", urgency=0.9, grip=0.5, speed=0.9),
125
+ "household_sit": GoalSpec(target="chair", action="sit", destination="chair", phase="approach", urgency=0.6, grip=0.3, speed=0.4),
126
+ "reach_arm": GoalSpec(target="none", action="pick", destination="none", phase="grasp", urgency=0.9, grip=-0.5, speed=0.3),
127
+ "yoga_mountain": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.5, grip=0.5, speed=0.2),
128
+ "yoga_tree": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.6, grip=0.5, speed=0.2),
129
+ "yoga_warrior1": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.7, grip=0.3, speed=0.3),
130
+ "yoga_warrior2": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.7, grip=0.3, speed=0.3),
131
+ "yoga_triangle": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.7, grip=0.3, speed=0.3),
132
+ "yoga_child": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.6, grip=0.5, speed=0.2),
133
+ "yoga_downdog": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.7, grip=0.4, speed=0.3),
134
+ "yoga_cobra": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.6, grip=0.5, speed=0.2),
135
+ "yoga_all": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.6, grip=0.4, speed=0.2),
136
+ "yoga_random": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.7, grip=0.4, speed=0.3),
137
+ "household_ball": GoalSpec(target="any", action="carry", destination="floor", phase="approach", urgency=0.7, grip=-0.6, speed=0.6),
138
+ "shape_sorter": GoalSpec(target="block", action="pick", destination="none", phase="approach", urgency=0.8, grip=-0.8, speed=0.4),
139
+ "media_watch": GoalSpec(target="none", action="stand", destination="none", phase="approach", urgency=0.5, grip=0.5, speed=0.2),
140
+ "drive_city": GoalSpec(target="none", action="walk", destination="none", phase="approach", urgency=0.9, grip=0.5, speed=0.9),
141
+ "dribble": GoalSpec(target="ball", action="carry", destination="floor", phase="grasp", urgency=0.8, grip=-0.6, speed=0.7),
142
+ "shoot": GoalSpec(target="ball", action="pick", destination="counter", phase="grasp", urgency=0.9, grip=-0.7, speed=0.5),
143
+ "layup": GoalSpec(target="ball", action="carry", destination="counter", phase="approach", urgency=1.0, grip=-0.7, speed=0.9),
144
+ "basketball": GoalSpec(target="ball", action="carry", destination="counter", phase="approach", urgency=0.9, grip=-0.6, speed=0.8),
145
+ }
146
+ return defaults.get(task_name, GoalSpec())
embodied/interactive_objects.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ InteractiveObjects — fyzické předměty pro haptické a kognitivní úlohy.
3
+
4
+ BallObject – gumový míč s vysokou elasticitou
5
+ WaterCupObject – hrnek s hladinou vody (kloní se = přelévá)
6
+ RubiksCubeObject – zjednodušená Rubikova kostka (fyzické těleso + logický stav)
7
+ ShapeSorterWall – zeď s otvory (čtverec, trojúhelník, hvězda) + odpovídající tvar
8
+ """
9
+ from __future__ import annotations
10
+ import sys, os
11
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
12
+
13
+ import numpy as np
14
+ from dataclasses import dataclass, field
15
+ from typing import Dict, List, Optional, Tuple
16
+
17
+ from engine.physics.rigid_body import (
18
+ PhysicsWorld, RigidBody, BodyType,
19
+ vec3, Vec3, length, normalize,
20
+ )
21
+
22
+
23
+ # ── Základní třída ────────────────────────────────────────────────────────────
24
+
25
+ class PhysicsObject:
26
+ """Základní třída pro interaktivní objekty."""
27
+
28
+ def __init__(self, phys: PhysicsWorld, obj_id: str):
29
+ self.phys = phys
30
+ self.obj_id = obj_id
31
+ self.body: Optional[RigidBody] = None
32
+
33
+ @property
34
+ def position(self) -> Vec3:
35
+ return self.body.position if self.body else vec3()
36
+
37
+ @property
38
+ def velocity(self) -> Vec3:
39
+ return self.body.velocity if self.body else vec3()
40
+
41
+ def distance_to(self, pos: Vec3) -> float:
42
+ return float(length(self.position - pos))
43
+
44
+
45
+ # ── Míč ───────────────────────────────────────────────────────────────────────
46
+
47
+ class BallObject(PhysicsObject):
48
+ """
49
+ Gumový míč — malé těleso s vysokou elasticitou a nízkou hmotností.
50
+ Odraz: restitution=0.82 (≈ gumový míč, ne super-ball).
51
+ """
52
+ RADIUS = 0.12 # m
53
+ MASS = 0.25 # kg
54
+
55
+ def __init__(self, phys: PhysicsWorld, pos: Vec3, obj_id: str = "ball"):
56
+ super().__init__(phys, obj_id)
57
+ self.body = phys.create_body(
58
+ obj_id, BodyType.DYNAMIC,
59
+ position=pos,
60
+ size=vec3(self.RADIUS * 2, self.RADIUS * 2, self.RADIUS * 2),
61
+ mass=self.MASS,
62
+ restitution=0.82,
63
+ friction=0.35,
64
+ )
65
+ self._spawn_pos = pos
66
+
67
+ def reset(self, pos: Optional[Vec3] = None):
68
+ p = pos if pos is not None else self._spawn_pos
69
+ self.body.position = p
70
+ self.body.velocity = vec3()
71
+ self.body.angular_velocity = vec3()
72
+ self.body.rotation = vec3()
73
+
74
+ def get_state(self) -> np.ndarray:
75
+ """6 floats: [pos_x, pos_y, pos_z, vel_x, vel_y, vel_z]"""
76
+ p = self.body.position
77
+ v = self.body.velocity
78
+ return np.array([p[0], p[1], p[2], v[0], v[1], v[2]], dtype=np.float32)
79
+
80
+
81
+ # ── Hrnek s vodou ─────────────────────────────────────────────────────────────
82
+
83
+ class WaterCupObject(PhysicsObject):
84
+ """
85
+ Keramický hrnek s hladinou vody.
86
+ water_level klesá, když je hrnek nakloněný (tilt > threshold).
87
+ Použití: agent se musí naučit nosit hrnek opatrně.
88
+ """
89
+ MASS = 0.35
90
+ SIZE = vec3(0.09, 0.12, 0.09)
91
+ TILT_SPILL_THRESH = 0.40 # rad od svislé osy
92
+ SPILL_RATE = 0.03 # úbytek hladiny za krok při náklonu
93
+
94
+ def __init__(self, phys: PhysicsWorld, pos: Vec3, obj_id: str = "water_cup"):
95
+ super().__init__(phys, obj_id)
96
+ self.body = phys.create_body(
97
+ obj_id, BodyType.DYNAMIC,
98
+ position=pos,
99
+ size=self.SIZE,
100
+ mass=self.MASS,
101
+ restitution=0.08,
102
+ friction=0.70,
103
+ )
104
+ self._spawn_pos = pos
105
+ self.water_level = 1.0 # 0.0 = prázdný, 1.0 = plný
106
+ self._spilled = False
107
+
108
+ def step(self):
109
+ """Volat každý fyzikální krok."""
110
+ if self.water_level <= 0.0:
111
+ return
112
+ rot = self.body.rotation
113
+ tilt = float(np.sqrt(rot[0] ** 2 + rot[2] ** 2))
114
+ if tilt > self.TILT_SPILL_THRESH:
115
+ self.water_level = max(0.0, self.water_level - self.SPILL_RATE)
116
+ if self.water_level == 0.0:
117
+ self._spilled = True
118
+
119
+ def reset(self, pos: Optional[Vec3] = None):
120
+ p = pos if pos is not None else self._spawn_pos
121
+ self.body.position = p
122
+ self.body.velocity = vec3()
123
+ self.body.angular_velocity = vec3()
124
+ self.body.rotation = vec3()
125
+ self.water_level = 1.0
126
+ self._spilled = False
127
+
128
+ def get_state(self) -> np.ndarray:
129
+ """4 floats: [pos_y_rel_spawn, tilt, water_level, spilled]"""
130
+ rot = self.body.rotation
131
+ tilt = float(np.sqrt(float(rot[0]) ** 2 + float(rot[2]) ** 2))
132
+ pos_y = float(self.body.position[1])
133
+ return np.array([pos_y, tilt, self.water_level, float(self._spilled)],
134
+ dtype=np.float32)
135
+
136
+
137
+ # ── Rubikova kostka ───────────────────────────────────────────────────────────
138
+
139
+ # Každá stěna má 9 políček → 6 stěn × 9 = 54 hodnot (0-5 = 6 barev)
140
+ # Zjednodušení: jedna fyzická krychle, logický stav jako int array
141
+ _SOLVED_FACES = np.array([
142
+ [i] * 9 for i in range(6) # každá stěna má jednu barvu
143
+ ], dtype=np.int8).flatten()
144
+
145
+
146
+ class RubiksCubeObject(PhysicsObject):
147
+ """
148
+ Fyzická krychle s logickým stavem 6×9 políček (6 barev).
149
+ Rotace stěn: rotate_face(face_idx, clockwise).
150
+ Stav: stupeň zamíchanosti (0=vyřešeno, 1=přeskupeno).
151
+ """
152
+ SIZE = vec3(0.12, 0.12, 0.12)
153
+ MASS = 0.15
154
+
155
+ # Indexy stěn
156
+ FRONT, BACK, LEFT, RIGHT, TOP, BOTTOM = 0, 1, 2, 3, 4, 5
157
+
158
+ # Otáčení: stěna 0-5 → seznam indexů políček (zjednodušeno — ring rotation)
159
+ _FACE_RING = {
160
+ 0: ([6,7,8],[0,3,6],[2,1,0],[8,5,2], [BACK:=1,LEFT:=2,TOP:=4,RIGHT:=3]),
161
+ }
162
+
163
+ def __init__(self, phys: PhysicsWorld, pos: Vec3, obj_id: str = "rubiks"):
164
+ super().__init__(phys, obj_id)
165
+ self.body = phys.create_body(
166
+ obj_id, BodyType.DYNAMIC,
167
+ position=pos,
168
+ size=self.SIZE,
169
+ mass=self.MASS,
170
+ restitution=0.10,
171
+ friction=0.80,
172
+ )
173
+ self._spawn_pos = pos
174
+ self.faces = _SOLVED_FACES.copy() # 54 políček
175
+ self._moves = 0
176
+
177
+ def scramble(self, n_moves: int = 20, rng: np.random.Generator = None):
178
+ if rng is None:
179
+ rng = np.random.default_rng()
180
+ for _ in range(n_moves):
181
+ face = int(rng.integers(0, 6))
182
+ self._rotate_face_logical(face, bool(rng.integers(0, 2)))
183
+ self._moves = 0
184
+
185
+ def _rotate_face_logical(self, face: int, clockwise: bool):
186
+ """Otočí políčka na jedné stěně o 90°."""
187
+ f = self.faces[face * 9:(face + 1) * 9].copy().reshape(3, 3)
188
+ if clockwise:
189
+ f = np.rot90(f, k=3)
190
+ else:
191
+ f = np.rot90(f, k=1)
192
+ self.faces[face * 9:(face + 1) * 9] = f.flatten()
193
+ self._moves += 1
194
+
195
+ def is_solved(self) -> bool:
196
+ return np.array_equal(self.faces, _SOLVED_FACES)
197
+
198
+ def solve_fraction(self) -> float:
199
+ """Poměr správně umístěných políček (0.0–1.0)."""
200
+ return float(np.mean(self.faces == _SOLVED_FACES))
201
+
202
+ def reset(self, pos: Optional[Vec3] = None, scramble_n: int = 0,
203
+ rng: np.random.Generator = None):
204
+ p = pos if pos is not None else self._spawn_pos
205
+ self.body.position = p
206
+ self.body.velocity = vec3()
207
+ self.body.angular_velocity = vec3()
208
+ self.body.rotation = vec3()
209
+ self.faces = _SOLVED_FACES.copy()
210
+ self._moves = 0
211
+ if scramble_n > 0:
212
+ self.scramble(scramble_n, rng)
213
+
214
+ def get_state(self) -> np.ndarray:
215
+ """3 floats: [solve_fraction, n_moves_norm, is_held_approx(pos_y)]"""
216
+ return np.array([
217
+ self.solve_fraction(),
218
+ min(1.0, self._moves / 100.0),
219
+ float(np.clip((float(self.body.position[1]) - 0.5) / 1.5, 0.0, 1.0)),
220
+ ], dtype=np.float32)
221
+
222
+
223
+ # ── Třídicí zeď ───────────────────────────────────────────────────────────────
224
+
225
+ SHAPE_SQUARE = 0
226
+ SHAPE_TRIANGLE = 1
227
+ SHAPE_STAR = 2
228
+
229
+ # Fyzické rozměry tvarových předmětů
230
+ _SHAPE_SIZES = {
231
+ SHAPE_SQUARE: vec3(0.09, 0.09, 0.04),
232
+ SHAPE_TRIANGLE: vec3(0.09, 0.09, 0.04),
233
+ SHAPE_STAR: vec3(0.10, 0.10, 0.04),
234
+ }
235
+
236
+ # Barvy tvarů (R, G, B) pro renderer
237
+ SHAPE_COLORS = {
238
+ SHAPE_SQUARE: (0.9, 0.2, 0.2), # červená
239
+ SHAPE_TRIANGLE: (0.2, 0.8, 0.2), # zelená
240
+ SHAPE_STAR: (0.9, 0.8, 0.1), # žlutá
241
+ }
242
+
243
+ # Pozice otvorů (x-offset od středu zdi)
244
+ _HOLE_X_OFFSETS = {
245
+ SHAPE_SQUARE: -0.35,
246
+ SHAPE_TRIANGLE: 0.00,
247
+ SHAPE_STAR: 0.35,
248
+ }
249
+
250
+ # Tolerance vložení (vzdálenost + orientace)
251
+ _INSERT_DIST_OK = 0.15 # m — střed tvaru musí být do 15 cm od otvoru
252
+ _INSERT_TILT_OK = 0.30 # rad — maximální náklon tvaru při vkládání
253
+
254
+
255
+ @dataclass
256
+ class ShapePiece:
257
+ shape: int # SHAPE_* konstanta
258
+ body: RigidBody
259
+ inserted: bool = False
260
+ spawn_pos: Vec3 = field(default_factory=lambda: vec3())
261
+
262
+
263
+ class ShapeSorterWall:
264
+ """
265
+ Statická zeď s třemi otvory (čtverec, trojúhelník, hvězda).
266
+ Tvarové předměty jsou dynamické tělesa, agent je musí vložit do správného otvoru.
267
+ Kontrola vložení: vzdálenost od otvoru < 15 cm + přibližně správná orientace.
268
+ """
269
+ WALL_POS = vec3(0.0, 1.0, -2.0) # zeď je v záporném Z (před agentem)
270
+ WALL_SIZE = vec3(1.2, 1.0, 0.08)
271
+ HOLE_Y = 1.0 # výška středu otvorů (v absolutních souřadnicích)
272
+
273
+ def __init__(self, phys: PhysicsWorld, origin: Vec3 = None, rng=None):
274
+ self.phys = phys
275
+ self._rng = rng if rng is not None else np.random.default_rng()
276
+ self.origin = origin if origin is not None else vec3()
277
+ self._wall_body: Optional[RigidBody] = None
278
+ self.pieces: List[ShapePiece] = []
279
+ self._spawn()
280
+
281
+ def _spawn(self):
282
+ wall_pos = self.origin + self.WALL_POS
283
+ self._wall_body = self.phys.create_body(
284
+ "shape_wall", BodyType.STATIC,
285
+ position=wall_pos,
286
+ size=self.WALL_SIZE,
287
+ mass=0.0, restitution=0.05, friction=0.90,
288
+ )
289
+
290
+ # Spawn tvarové předměty před zdi
291
+ spawn_z = float(self.WALL_POS[2]) + 1.2 # 1.2 m před zdi
292
+ shapes = [SHAPE_SQUARE, SHAPE_TRIANGLE, SHAPE_STAR]
293
+ # Zamíchej pořadí spawnu
294
+ order = self._rng.permutation(len(shapes))
295
+ x_slots = [-0.35, 0.0, 0.35]
296
+ for i, shape_idx in enumerate(order):
297
+ shape = shapes[shape_idx]
298
+ pos = self.origin + vec3(
299
+ x_slots[i],
300
+ 0.80, # výška stolu/podlahy
301
+ spawn_z,
302
+ )
303
+ body = self.phys.create_body(
304
+ f"shape_{shape}_{i}", BodyType.DYNAMIC,
305
+ position=pos,
306
+ size=_SHAPE_SIZES[shape],
307
+ mass=0.08, restitution=0.10, friction=0.75,
308
+ )
309
+ self.pieces.append(ShapePiece(shape=shape, body=body, spawn_pos=pos.copy()))
310
+
311
+ def hole_position(self, shape: int) -> Vec3:
312
+ """Absolutní pozice středu otvoru pro daný tvar."""
313
+ wx = float(self.WALL_POS[0]) + float(self.origin[0])
314
+ wz = float(self.WALL_POS[2]) + float(self.origin[2]) + 0.05
315
+ return vec3(wx + _HOLE_X_OFFSETS[shape], self.HOLE_Y, wz)
316
+
317
+ def check_insertions(self) -> int:
318
+ """Zkontroluje vložení předmětů; vrací počet správně vložených."""
319
+ count = 0
320
+ for piece in self.pieces:
321
+ if piece.inserted:
322
+ count += 1
323
+ continue
324
+ hole = self.hole_position(piece.shape)
325
+ dist = float(length(piece.body.position - hole))
326
+ rot = piece.body.rotation
327
+ tilt = float(np.sqrt(float(rot[0]) ** 2 + float(rot[1]) ** 2))
328
+ if dist < _INSERT_DIST_OK and tilt < _INSERT_TILT_OK:
329
+ piece.inserted = True
330
+ # Zafixuj předmět na místě
331
+ piece.body.body_type = BodyType.STATIC
332
+ piece.body.velocity = vec3()
333
+ count += 1
334
+ return count
335
+
336
+ def all_inserted(self) -> bool:
337
+ return all(p.inserted for p in self.pieces)
338
+
339
+ def nearest_piece(self, hand_pos: Vec3) -> Optional[ShapePiece]:
340
+ """Vrátí nejbližší nevložený předmět k ruce agenta."""
341
+ best_d = float("inf")
342
+ best_p = None
343
+ for p in self.pieces:
344
+ if p.inserted:
345
+ continue
346
+ d = float(length(p.body.position - hand_pos))
347
+ if d < best_d:
348
+ best_d, best_p = d, p
349
+ return best_p
350
+
351
+ def reset(self):
352
+ for piece in self.pieces:
353
+ piece.inserted = False
354
+ piece.body.body_type = BodyType.DYNAMIC
355
+ piece.body.position = piece.spawn_pos.copy()
356
+ piece.body.velocity = vec3()
357
+ piece.body.angular_velocity = vec3()
358
+ piece.body.rotation = vec3()
359
+
360
+ def get_state(self) -> np.ndarray:
361
+ """
362
+ 9 floats: pro každý předmět (3×):
363
+ [dist_to_hole_norm, inserted, piece_pos_y_norm]
364
+ """
365
+ out = []
366
+ for piece in self.pieces:
367
+ hole = self.hole_position(piece.shape)
368
+ dist = float(length(piece.body.position - hole))
369
+ out.extend([
370
+ float(np.clip(dist / 3.0, 0.0, 1.0)),
371
+ float(piece.inserted),
372
+ float(np.clip(float(piece.body.position[1]) / 2.0, 0.0, 1.0)),
373
+ ])
374
+ return np.array(out, dtype=np.float32)
embodied/intrinsic.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Intrinsická motivace agenta — implementace 5 strategií proti
3
+ "Local Optimum of Doing Nothing".
4
+
5
+ 1. MetabolicNeeds — biologický tlak (hunger/energy decay)
6
+ 2. CuriositySignal — η kanál: bonus za novelty (predikční chyba)
7
+ 3. BoredomTracker — penalizace za opakování stejných pozic
8
+ 4. EventRewardBus — mikro-odměny za změnu stavu světa
9
+ """
10
+ from __future__ import annotations
11
+ import numpy as np
12
+ from collections import deque
13
+ from typing import Optional
14
+
15
+
16
+ # ══════════════════════════════════════════════════════════════════════════════
17
+ # 1. Metabolic Needs (biologický tlak)
18
+ # ══════════════════════════════════════════════════════════════════════════════
19
+
20
+ class MetabolicNeeds:
21
+ """
22
+ Stání na místě = jistá smrt.
23
+
24
+ hunger cresce každý tick, energy klesá.
25
+ Penalizace roste nelineárně — čím déle agent stojí, tím víc ho to bolí.
26
+ Nahrazuje flat alive-bonus, který dovoloval nicnedělání.
27
+ """
28
+ HUNGER_RATE = 0.0010 # hunger per step (dosáhne 1.0 za ~1000 kroků)
29
+ ENERGY_RATE = 0.0008 # energy drain per step
30
+ DEATH_PENALTY = -4.0 # jednorázová penalizace při hunger >= 1.0, pak plný reset
31
+ HUNGER_RESET = 0.0 # plný reset — nová šance
32
+
33
+ def __init__(self):
34
+ self.hunger = 0.0
35
+ self.energy = 1.0
36
+ self._step = 0
37
+
38
+ def reset(self):
39
+ self.hunger = 0.0
40
+ self.energy = 1.0
41
+ self._step = 0
42
+
43
+ def feed(self, amount: float = 0.3):
44
+ """Agent dostal jídlo/energii (nasbíral reward)."""
45
+ self.hunger = max(0.0, self.hunger - amount * 0.5)
46
+ self.energy = min(1.0, self.energy + amount * 0.3)
47
+
48
+ def step(self, task_reward: float) -> float:
49
+ """
50
+ Vrátí metabolický příspěvek k odměně (záporný = tlak).
51
+ Volej po výpočtu task_reward.
52
+ """
53
+ self._step += 1
54
+
55
+ # Krmení z jakékoli kladné task odměny
56
+ if task_reward > 0.0:
57
+ self.feed(task_reward * 0.12)
58
+
59
+ self.hunger = min(1.0, self.hunger + self.HUNGER_RATE)
60
+ self.energy = max(0.0, self.energy - self.ENERGY_RATE)
61
+
62
+ # Smrt hladem → penalizace + reset (ne trvalá smrt v -8 smyčce)
63
+ if self.hunger >= 1.0:
64
+ self.hunger = self.HUNGER_RESET
65
+ return self.DEATH_PENALTY
66
+
67
+ # Metabolický tlak — koef 0.15 aby nepřebil task reward
68
+ pressure = -(self.hunger ** 2) * 0.15
69
+ energy_bonus = self.energy * 0.012
70
+ return pressure + energy_bonus
71
+
72
+
73
+ # ══════════════════════════════════════════════════════════════════════════════
74
+ # 2. Curiosity Signal (η kanál — predikční chyba)
75
+ # ══════════════════════════════════════════════════════════════════════════════
76
+
77
+ class CuriositySignal:
78
+ """
79
+ Intrinsická odměna za překvapení (Prediction Error).
80
+
81
+ Udržuje exponenciální klouzavý průměr (EMA) stavu jako "predikci".
82
+ Pokud aktuální stav výrazně odchyluje od predikce → vysoká curiosity.
83
+ To motivuje agenta hledat nové situace místo opakovat bezpečné smyčky.
84
+
85
+ Odpovídá η (Eta) kanálu v MnemoLattice.
86
+ """
87
+ ALPHA = 0.15 # rychlost učení prediktoru
88
+ SCALE = 0.25 # škálování curiosity bonusu
89
+ MAX_BONUS = 0.40 # strop bonusu za jeden krok
90
+
91
+ def __init__(self, state_dim: int = 24):
92
+ self._pred = None # exponenciální predikce dalšího stavu
93
+ self._sdim = state_dim
94
+ self._eta = 0.0 # aktuální curiosity hodnota
95
+
96
+ def reset(self):
97
+ self._pred = None
98
+ self._eta = 0.0
99
+
100
+ def step(self, state: np.ndarray) -> float:
101
+ """
102
+ Spočítá curiosity bonus pro aktuální stav.
103
+ Vysoká hodnota = agent viděl něco neobvyklého.
104
+ """
105
+ if self._pred is None:
106
+ self._pred = state.copy()
107
+ return 0.0
108
+
109
+ # Predikční chyba — jak moc se lišíme od očekávaného stavu
110
+ err = float(np.mean((state - self._pred) ** 2))
111
+ # Aktualizuj predikci (EMA — jednoduchý 1-krokový forward model)
112
+ self._pred = (1 - self.ALPHA) * self._pred + self.ALPHA * state
113
+
114
+ self._eta = float(np.clip(err * self.SCALE, 0.0, self.MAX_BONUS))
115
+ return self._eta
116
+
117
+ @property
118
+ def eta(self) -> float:
119
+ return self._eta
120
+
121
+
122
+ # ══════════��═══════════════════════════════════════════════════════════════════
123
+ # 3. Boredom Tracker (penalizace za opakování pozic)
124
+ # ══════════════════════════════════════════════════════════════════════════════
125
+
126
+ class BoredomTracker:
127
+ """
128
+ Penalizace za nudné chování — opakování stejných míst.
129
+
130
+ Mřížka 1m × 1m, udrží historii N posledních navštívených buněk.
131
+ Revisit té samé buňky stojí energii.
132
+ Propojuje se s FractalHexaPentaMemory přes memory pressure.
133
+ """
134
+ CELL_SIZE = 1.0 # rozlišení mřížky (m)
135
+ HISTORY_LEN = 60 # kolik kroků pamatuje
136
+ REVISIT_PEN = -0.12 # penalizace za revisit (per krok v té buňce)
137
+ NOVEL_BONUS = 0.08 # bonus za novou buňku
138
+
139
+ def __init__(self):
140
+ self._history: deque = deque(maxlen=self.HISTORY_LEN)
141
+ self._visited: set = set() # buňky navštívené za epizodu
142
+ self._total_novel = 0
143
+
144
+ def reset(self):
145
+ self._history.clear()
146
+ self._visited.clear()
147
+ self._total_novel = 0
148
+
149
+ def _cell(self, x: float, z: float) -> tuple:
150
+ return (int(x / self.CELL_SIZE), int(z / self.CELL_SIZE))
151
+
152
+ def step(self, x: float, z: float) -> float:
153
+ cell = self._cell(x, z)
154
+ recent_count = list(self._history).count(cell)
155
+ self._history.append(cell)
156
+
157
+ is_novel = cell not in self._visited
158
+ if is_novel:
159
+ self._visited.add(cell)
160
+ self._total_novel += 1
161
+ return self.NOVEL_BONUS
162
+
163
+ # Penalizace roste s počtem recentních revisitů
164
+ if recent_count >= 3:
165
+ return self.REVISIT_PEN * min(recent_count / 3, 2.0)
166
+ return 0.0
167
+
168
+ @property
169
+ def exploration_score(self) -> float:
170
+ """Kolik různých buněk agent prozkoumala — metrika pro logging."""
171
+ return float(self._total_novel)
172
+
173
+
174
+ # ══════════════════════════════════════════════════════════════════════════════
175
+ # 4. Event Reward Bus (mikro-odměny za interakci se světem)
176
+ # ══════════════════════════════════════════════════════════════════════════════
177
+
178
+ class EventRewardBus:
179
+ """
180
+ Analogie EventBus z VoxForge — mikro-odměny za VOXEL_DESTROYED,
181
+ OBJECT_TOUCHED, GRASP_ATTEMPT.
182
+
183
+ Agenta odměňuje za to, že dělá cokoliv se světem, ne za stání.
184
+ Postupně tyto mikro-odměny snižuj (decay) aby agent přešel na task reward.
185
+ """
186
+ TOUCH_BONUS = 0.20 # za dotek s objektem
187
+ GRASP_BONUS = 0.50 # za sevření předmětu
188
+ MOVE_BONUS = 0.04 # za pohyb těla (norma velocity)
189
+ DECAY = 0.9998 # decay per step → po 3000 krocích na 55%
190
+
191
+ def __init__(self):
192
+ self._scale = 1.0
193
+ self._last_grip = False
194
+
195
+ def reset(self):
196
+ self._scale = 1.0
197
+ self._last_grip = False
198
+
199
+ def step(self, touched: bool, grasped: bool,
200
+ body_velocity_norm: float) -> float:
201
+ self._scale *= self.DECAY
202
+ r = 0.0
203
+
204
+ if touched:
205
+ r += self.TOUCH_BONUS * self._scale
206
+ if grasped and not self._last_grip:
207
+ r += self.GRASP_BONUS * self._scale # jen při prvním sevření
208
+ self._last_grip = grasped
209
+
210
+ # Bonus za pohyb — bojuje přímo s "stojím a nedělám nic"
211
+ move_r = min(body_velocity_norm, 2.0) * self.MOVE_BONUS * self._scale
212
+ r += move_r
213
+
214
+ return r
215
+
216
+
217
+ # ══════════════════════════════════════════════════════════════════════════════
218
+ # 5. ε-greedy Motor Noise (injekce šumu do motoriky)
219
+ # ══════════════════════════════════════════════════════════════════════════════
220
+
221
+ class EpsilonGreedyMotor:
222
+ """
223
+ ε-greedy strategie pro výstup motorické sítě.
224
+
225
+ S pravděpodobností ε přepiš výstup sítě náhodnou akcí.
226
+ ε klesá exponenciálně — brzy agent přestane být náhodný.
227
+ """
228
+ EPS_START = 0.35 # počáteční pravděpodobnost náhodné akce
229
+ EPS_MIN = 0.03 # minimální ε (nikdy úplně nevypínat)
230
+ EPS_DECAY = 0.9992 # decay per episode
231
+
232
+ def __init__(self, action_dim: int = 10, seed: int = 0):
233
+ self._eps = self.EPS_START
234
+ self._adim = action_dim
235
+ self._rng = np.random.default_rng(seed)
236
+ self._ep = 0
237
+
238
+ def reset_episode(self):
239
+ self._ep += 1
240
+ self._eps = max(self.EPS_MIN, self._eps * self.EPS_DECAY)
241
+
242
+ def apply(self, action: np.ndarray) -> np.ndarray:
243
+ """Případně nahraď akci náhodnou — 'shodí agenta z kopce nicnedělání'."""
244
+ if self._rng.random() < self._eps:
245
+ # Náhodná akce ale biased toward movement (ne nulová akce)
246
+ noise = self._rng.uniform(-1.0, 1.0, self._adim).astype(np.float32)
247
+ return np.clip(noise, -1.0, 1.0)
248
+ return action
249
+
250
+ @property
251
+ def epsilon(self) -> float:
252
+ return self._eps
253
+
254
+
255
+ # ══════════════════════════════════════════════════════════════════════════════
256
+ # Kompozit: IntrinsicMotivation (všechny 4 složky najednou)
257
+ # ══════════════════════════════════════════════════════════════════════════════
258
+
259
+ class IntrinsicMotivation:
260
+ """
261
+ Balíček všech intrinsických složek odměny.
262
+ Použití v step_reward():
263
+
264
+ im_bonus = self.intrinsic.step(
265
+ state=cortex_output, pos_x=x, pos_z=z,
266
+ touched=touched, grasped=grasped,
267
+ body_vel=vel_norm, task_reward=task_r
268
+ )
269
+ total_reward = task_r + im_bonus
270
+ """
271
+ def __init__(self, state_dim: int = 24, action_dim: int = 10):
272
+ self.meta = MetabolicNeeds()
273
+ self.curiosity = CuriositySignal(state_dim)
274
+ self.boredom = BoredomTracker()
275
+ self.events = EventRewardBus()
276
+
277
+ def reset(self):
278
+ self.meta.reset()
279
+ self.curiosity.reset()
280
+ self.boredom.reset()
281
+ self.events.reset()
282
+
283
+ def step(self, state: np.ndarray, pos_x: float, pos_z: float,
284
+ touched: bool, grasped: bool, body_vel: float,
285
+ task_reward: float) -> float:
286
+ meta_r = self.meta.step(task_reward)
287
+ curious_r = self.curiosity.step(state)
288
+ bored_r = self.boredom.step(pos_x, pos_z)
289
+ event_r = self.events.step(touched, grasped, body_vel)
290
+ return meta_r + curious_r + bored_r + event_r
embodied/language_goal_bridge.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LanguageGoalBridge — obousměrný překlad mezi přirozeným jazykem a goal vektorem.
3
+
4
+ Architektura:
5
+ Vstup (CZ/EN text) → text_to_goal() → GoalSpec → encode_goal() → vec[32]
6
+ Výstup ← goal_to_text() ← decode_goal() ← vec[32] + body_state
7
+
8
+ Vnitřní "jazyk agenta":
9
+ - goal_vec[32]: kontinuální sémantický prostor (co chce)
10
+ - workspace tokeny: diskrétní sémantické tokeny (co si "všímá")
11
+ - body_state[73]: proprioceptivní stav (jak se cítí)
12
+
13
+ Překlad je pravidlový (bez externího API) — funguje offline.
14
+ """
15
+ from __future__ import annotations
16
+ import re
17
+ import numpy as np
18
+ from typing import Optional
19
+ from dataclasses import dataclass, field
20
+
21
+ from embodied.goal_encoder import GoalSpec, encode_goal, decode_goal, GOAL_DIM
22
+
23
+
24
+ # ── Slovníky pro parsování ────────────────────────────────────────────────────
25
+
26
+ _OBJECT_KEYWORDS: dict[str, list[str]] = {
27
+ "cup": ["hrníček", "hrnek", "šálek", "cup", "mug"],
28
+ "block": ["blok", "kostka", "kvádr", "block", "cube"],
29
+ "door": ["dveře", "dvéře", "door"],
30
+ "chair": ["židle", "křeslo", "chair"],
31
+ "table": ["stůl", "stůl", "table"],
32
+ "counter": ["pult", "deska", "counter", "countertop"],
33
+ "shelf": ["polička", "police", "shelf"],
34
+ "ball": ["míč", "míček", "ball"],
35
+ }
36
+
37
+ _ACTION_KEYWORDS: dict[str, list[str]] = {
38
+ "pick": ["vezmi", "zvedni", "uchop", "chyť", "pick", "grab", "take", "lift"],
39
+ "carry": ["přines", "dones", "přesuň", "carry", "bring", "move"],
40
+ "sweep": ["smeteň", "shoď", "smeť", "sweep", "knock", "push off"],
41
+ "open": ["otevři", "open"],
42
+ "sit": ["sedni", "posaď", "sit", "sit down"],
43
+ "walk": ["jdi", "choď", "dojdi", "walk", "go", "move to"],
44
+ "stand": ["stůj", "postůj", "drž se", "stand", "stay", "hold"],
45
+ "place": ["polož", "dej", "umísti", "place", "put", "set down"],
46
+ }
47
+
48
+ _DEST_KEYWORDS: dict[str, list[str]] = {
49
+ "counter": ["pult", "desku", "na pult", "counter", "on counter"],
50
+ "table": ["stůl", "stůl", "na stůl", "table", "on table", "cílové místo", "target"],
51
+ "floor": ["podlahu", "zem", "dolů", "floor", "ground", "down"],
52
+ "door": ["dveře", "ke dveřím", "door"],
53
+ "chair": ["židli", "na židli", "chair"],
54
+ }
55
+
56
+ _URGENCY_KEYWORDS: dict[float, list[str]] = {
57
+ 0.9: ["rychle", "hned", "okamžitě", "fast", "quickly", "now", "immediately"],
58
+ 0.3: ["pomalu", "opatrně", "slowly", "carefully", "gently"],
59
+ }
60
+
61
+ _GRIP_KEYWORDS: dict[float, list[str]] = {
62
+ -0.8: ["pevně", "silně", "tight", "firmly", "hard"],
63
+ 0.8: ["povolně", "jemně", "loose", "gently", "softly"],
64
+ }
65
+
66
+
67
+ # ── Překladové tabulky (goal_vec → CZ text) ──────────────────────────────────
68
+
69
+ _OBJECT_CZ = {"cup": "hrníček", "block": "blok", "door": "dveře",
70
+ "chair": "židli", "table": "stůl", "counter": "pult",
71
+ "shelf": "poličku", "sink": "dřez", "none": "nic", "any": "objekt"}
72
+ _ACTION_CZ = {"pick": "Uchopuji", "carry": "Přenáším", "sweep": "Smetám",
73
+ "open": "Otevírám", "sit": "Sedám", "walk": "Jdu",
74
+ "stand": "Stojím", "place": "Pokládám"}
75
+ _DEST_CZ = {"counter": "na pult", "table": "na stůl", "door": "ke dveřím",
76
+ "chair": "na židli", "floor": "na zem", "none": ""}
77
+ _PHASE_CZ = {"approach": "přibližování", "grasp": "uchopení",
78
+ "carry": "přenos", "place": "pokládání"}
79
+
80
+
81
+ # ── Parsování textu → GoalSpec ────────────────────────────────────────────────
82
+
83
+ def _find_keyword(text_lower: str, kw_dict: dict) -> Optional[str]:
84
+ """Najde první shodu klíčového slova v textu."""
85
+ for key, keywords in kw_dict.items():
86
+ for kw in keywords:
87
+ if kw in text_lower:
88
+ return key
89
+ return None
90
+
91
+
92
+ def text_to_goal(text: str, task_name: str = "") -> GoalSpec:
93
+ """
94
+ Přirozený jazyk (CZ/EN) → GoalSpec.
95
+
96
+ Příklady:
97
+ "vezmi hrníček a dej ho na pult"
98
+ → GoalSpec(target='cup', action='pick', destination='counter', grip=-0.8)
99
+
100
+ "rychle jdi ke dveřím"
101
+ → GoalSpec(target='door', action='walk', destination='door', urgency=0.9)
102
+ """
103
+ t = text.lower().strip()
104
+
105
+ target = _find_keyword(t, _OBJECT_KEYWORDS) or "none"
106
+ action_raw = _find_keyword(t, _ACTION_KEYWORDS) or "walk"
107
+ destination = _find_keyword(t, _DEST_KEYWORDS) or "none"
108
+
109
+ # Normalizace: "place" → "carry" (agent nosí, pak pokládá)
110
+ action = "carry" if action_raw == "place" else action_raw
111
+
112
+ # Urgency
113
+ urgency = 0.6
114
+ for val, kws in _URGENCY_KEYWORDS.items():
115
+ if any(kw in t for kw in kws):
116
+ urgency = val
117
+ break
118
+
119
+ # Grip: manipulační akce → záporný grip (uchopit)
120
+ grip = 0.3 # default: neutrální
121
+ if action in ("pick", "carry", "open"):
122
+ grip = -0.7
123
+ for val, kws in _GRIP_KEYWORDS.items():
124
+ if any(kw in t for kw in kws):
125
+ grip = val
126
+ break
127
+
128
+ # Fáze: odvoď z akce
129
+ phase_map = {"pick": "grasp", "carry": "carry", "place": "place",
130
+ "open": "grasp", "sweep": "approach"}
131
+ phase = phase_map.get(action, "approach")
132
+
133
+ # Speed: urgency → speed
134
+ speed = float(np.clip(urgency * 0.9, 0.2, 1.0))
135
+
136
+ return GoalSpec(
137
+ target=target,
138
+ action=action,
139
+ destination=destination,
140
+ phase=phase,
141
+ urgency=urgency,
142
+ grip=grip,
143
+ speed=speed,
144
+ description=text[:120],
145
+ )
146
+
147
+
148
+ # ── Dekódování stavu → CZ text ────────────────────────────────────────────────
149
+
150
+ @dataclass
151
+ class AgentThought:
152
+ """Čitelná reprezentace vnitřního stavu agenta."""
153
+ action_sentence: str = ""
154
+ phase_label: str = ""
155
+ object_dist: float = 0.0
156
+ grip_value: float = 0.0
157
+ energy_pct: float = 1.0
158
+ noise_pct: float = 0.0
159
+ workspace_tokens: list = field(default_factory=list)
160
+ raw_goal: GoalSpec = field(default_factory=GoalSpec)
161
+
162
+
163
+ def goal_to_text(goal_vec: np.ndarray,
164
+ body_state: Optional[np.ndarray] = None,
165
+ object_dist: float = -1.0,
166
+ energy: float = 1.0,
167
+ noise: float = 0.0,
168
+ workspace_tokens: Optional[list] = None) -> str:
169
+ """
170
+ goal_vec[32] + volitelný kontext → čitelná česká věta.
171
+
172
+ Výstup:
173
+ "Uchopuji hrníček → na pult | fáze: uchopení | vzdálenost: 0.4m
174
+ grip: -0.7 (pevný) | energie: 85% | explorace: 18%"
175
+ """
176
+ spec = decode_goal(goal_vec)
177
+
178
+ action_cz = _ACTION_CZ.get(spec.action, spec.action)
179
+ object_cz = _OBJECT_CZ.get(spec.target, spec.target)
180
+ dest_cz = _DEST_CZ.get(spec.destination, "")
181
+ phase_cz = _PHASE_CZ.get(spec.phase, spec.phase)
182
+
183
+ # Hlavní věta (přeskočí "nic" když target=none)
184
+ obj_part = "" if spec.target == "none" else f" {object_cz}"
185
+ dest_part = f" {dest_cz}" if dest_cz else ""
186
+ main = f"{action_cz}{obj_part}{dest_part}".strip()
187
+
188
+ parts = [main, f"fáze: {phase_cz}"]
189
+
190
+ if object_dist >= 0:
191
+ parts.append(f"vzdálenost: {object_dist:.2f}m")
192
+
193
+ # Grip slovně
194
+ g = spec.grip
195
+ grip_word = "pevný" if g < -0.5 else ("povolný" if g > 0.5 else "střední")
196
+ parts.append(f"grip: {g:+.1f} ({grip_word})")
197
+
198
+ if energy < 1.0:
199
+ parts.append(f"energie: {energy*100:.0f}%")
200
+
201
+ if noise > 0.0:
202
+ parts.append(f"explorace: {noise*100:.0f}%")
203
+
204
+ # Workspace tokeny (cogcore.emergent)
205
+ if workspace_tokens:
206
+ tok_strs = []
207
+ for tok in workspace_tokens[:3]:
208
+ if hasattr(tok, 'key') and hasattr(tok, 'salience'):
209
+ tok_strs.append(f"{tok.key.split('.')[-2]}↑{tok.salience:.2f}")
210
+ if tok_strs:
211
+ parts.append("workspace: [" + ", ".join(tok_strs) + "]")
212
+
213
+ return " | ".join(parts)
214
+
215
+
216
+ def state_to_thought(goal_vec: np.ndarray,
217
+ body_state: np.ndarray,
218
+ task_name: str = "",
219
+ object_dist: float = -1.0,
220
+ energy: float = 1.0,
221
+ noise: float = 0.0,
222
+ workspace_tokens: Optional[list] = None,
223
+ task_reward: float = 0.0) -> AgentThought:
224
+ """Plný vnitřní stav → AgentThought dataclass."""
225
+ spec = decode_goal(goal_vec)
226
+ thought = AgentThought(
227
+ action_sentence=goal_to_text(goal_vec, body_state, object_dist,
228
+ energy, noise, workspace_tokens),
229
+ phase_label=_PHASE_CZ.get(spec.phase, spec.phase),
230
+ object_dist=object_dist,
231
+ grip_value=spec.grip,
232
+ energy_pct=energy,
233
+ noise_pct=noise,
234
+ workspace_tokens=workspace_tokens or [],
235
+ raw_goal=spec,
236
+ )
237
+ return thought
238
+
239
+
240
+ # ── Rychlý test ───────────────────────────────────────────────────────────────
241
+
242
+ def _demo():
243
+ examples = [
244
+ "vezmi hrníček a dej ho na pult",
245
+ "rychle jdi ke dveřím",
246
+ "pomalu a pevně uchop blok a přesuň ho na stůl",
247
+ "take the cup and carry it to the counter",
248
+ "sweep all cups off the table quickly",
249
+ "stand still",
250
+ ]
251
+ print("=== text_to_goal demo ===")
252
+ for ex in examples:
253
+ spec = text_to_goal(ex)
254
+ vec = encode_goal(spec)
255
+ back = decode_goal(vec)
256
+ print(f"\nVstup : {ex!r}")
257
+ print(f"Spec : action={spec.action}, target={spec.target}, "
258
+ f"dest={spec.destination}, grip={spec.grip:+.1f}, urgency={spec.urgency:.1f}")
259
+ print(f"→ Text: {goal_to_text(vec, object_dist=0.8, energy=0.72, noise=0.15)}")
260
+
261
+
262
+ if __name__ == "__main__":
263
+ _demo()
embodied/media_objects.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MediaObjects — obrazovka a reproduktor pro multimediální učení agenta.
3
+
4
+ MonitorObject – plochá statická plocha s video buffrem; FPV raycaster ji maluje
5
+ SpeakerObject – bodový zdroj zvuku; vydává frekvenci slyšitelnou jako stavová veličina
6
+ MediaRoom – pomocná třída: room s monitorem + reproduktorem + lavicí předmětů
7
+ """
8
+ from __future__ import annotations
9
+ import sys, os
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
11
+
12
+ import numpy as np
13
+ from dataclasses import dataclass, field
14
+ from typing import List, Optional, Tuple
15
+
16
+ from engine.physics.rigid_body import (
17
+ PhysicsWorld, RigidBody, BodyType,
18
+ vec3, Vec3, length,
19
+ )
20
+
21
+ # ── Monitor ───────────────────────────────────────────────────────────────────
22
+
23
+ # Rozlišení obrazovky (logické — FPV renderer na to mapuje pixely)
24
+ MONITOR_W = 64
25
+ MONITOR_H = 48
26
+
27
+ # Barvy pro syntetické video (R, G, B 0-255)
28
+ _PALETTE = np.array([
29
+ [220, 50, 50], # červená
30
+ [ 50, 200, 50], # zelená
31
+ [ 50, 80, 220], # modrá
32
+ [230, 210, 30], # žlutá
33
+ [180, 50, 220], # fialová
34
+ [230, 130, 30], # oranžová
35
+ [240, 240, 240], # bílá
36
+ [ 20, 20, 20], # černá
37
+ ], dtype=np.uint8)
38
+
39
+
40
+ def make_color_frame(color_idx: int, w: int = MONITOR_W, h: int = MONITOR_H) -> np.ndarray:
41
+ """Jednobarevný snímek (pro jednoduché asociační testy)."""
42
+ frame = np.zeros((h, w, 3), dtype=np.uint8)
43
+ frame[:] = _PALETTE[color_idx % len(_PALETTE)]
44
+ return frame
45
+
46
+
47
+ def make_pattern_frame(pattern: str, t: int,
48
+ w: int = MONITOR_W, h: int = MONITOR_H) -> np.ndarray:
49
+ """
50
+ Syntetický snímek pro různé typy videí.
51
+ pattern: "stripe" | "circle" | "checkers" | "noise" | "arrow_left" | "arrow_right"
52
+ t: čas (číslo snímku)
53
+ """
54
+ frame = np.zeros((h, w, 3), dtype=np.uint8)
55
+ xs = np.arange(w)
56
+ ys = np.arange(h)
57
+ xg, yg = np.meshgrid(xs, ys)
58
+
59
+ if pattern == "stripe":
60
+ mask = ((xg // 8 + t // 4) % 2 == 0)
61
+ frame[mask] = _PALETTE[0]
62
+ frame[~mask] = _PALETTE[6]
63
+ elif pattern == "checkers":
64
+ mask = ((xg // 8 + yg // 8) % 2 == 0)
65
+ frame[mask] = _PALETTE[1]
66
+ frame[~mask] = _PALETTE[2]
67
+ elif pattern == "circle":
68
+ cx, cy = w // 2, h // 2
69
+ r = 10 + int(8 * np.sin(t * 0.1))
70
+ dist = np.sqrt((xg - cx) ** 2 + (yg - cy) ** 2)
71
+ frame[dist <= r] = _PALETTE[3]
72
+ frame[dist > r] = _PALETTE[7]
73
+ elif pattern == "noise":
74
+ rng = np.random.default_rng(t)
75
+ frame[:] = rng.integers(0, 255, (h, w, 3), dtype=np.uint8)
76
+ elif pattern == "arrow_left":
77
+ mid = h // 2
78
+ for i in range(w // 2):
79
+ span = i // 2 + 1
80
+ frame[max(0, mid - span):mid + span, i] = _PALETTE[4]
81
+ frame[mid - 2:mid + 2, w // 4:3 * w // 4] = _PALETTE[4]
82
+ elif pattern == "arrow_right":
83
+ mid = h // 2
84
+ for i in range(w // 2, w):
85
+ span = (w - i) // 2 + 1
86
+ frame[max(0, mid - span):mid + span, i] = _PALETTE[5]
87
+ frame[mid - 2:mid + 2, w // 4:3 * w // 4] = _PALETTE[5]
88
+ else:
89
+ frame[:] = _PALETTE[7]
90
+ return frame
91
+
92
+
93
+ class MonitorObject:
94
+ """
95
+ Statická obrazovka montovaná na zeď.
96
+ Drží aktuální snímek (numpy array H×W×3 uint8).
97
+ FPV raycaster (world_renderer.py) ji rozpozná podle obj_id="monitor_*"
98
+ a obarvuje zasažené pixely z self.current_frame.
99
+ """
100
+ SIZE = vec3(0.80, 0.60, 0.04) # šířka × výška × hloubka (m)
101
+
102
+ def __init__(self, phys: PhysicsWorld, pos: Vec3, obj_id: str = "monitor"):
103
+ self.obj_id = obj_id
104
+ self.body = phys.create_body(
105
+ obj_id, BodyType.STATIC,
106
+ position=pos,
107
+ size=self.SIZE,
108
+ mass=0.0, restitution=0.0, friction=0.9,
109
+ )
110
+ self.current_frame: np.ndarray = make_color_frame(7) # černo
111
+ self._playlist: List[np.ndarray] = []
112
+ self._frame_idx = 0
113
+ self._step_count = 0
114
+ self._fps_steps = 4 # každých N fyzikálních kroků se snímek změní
115
+ self.is_on = True
116
+
117
+ # ── Video ─────────────────────────────────────────────────────────────
118
+
119
+ def load_color_sequence(self, n_frames: int = 60):
120
+ """Naplní playlist střídajícími se barevnými snímky (asociační učení)."""
121
+ self._playlist = [
122
+ make_color_frame(i % len(_PALETTE)) for i in range(n_frames)
123
+ ]
124
+ self._frame_idx = 0
125
+
126
+ def load_pattern_sequence(self, pattern: str = "stripe", n_frames: int = 60):
127
+ """Naplní playlist syntetickým pohybovým vzorem."""
128
+ self._playlist = [make_pattern_frame(pattern, t) for t in range(n_frames)]
129
+ self._frame_idx = 0
130
+
131
+ def step(self):
132
+ """Posune playlist o jeden snímek (volat každý fyzikální krok)."""
133
+ if not self.is_on or not self._playlist:
134
+ return
135
+ self._step_count += 1
136
+ if self._step_count % self._fps_steps == 0:
137
+ self._frame_idx = (self._frame_idx + 1) % len(self._playlist)
138
+ self.current_frame = self._playlist[self._frame_idx]
139
+
140
+ # ── Viditelnost ───────────────────────────────────────────────────────
141
+
142
+ def visibility_from(self, eye_pos: Vec3, forward: Vec3) -> float:
143
+ """
144
+ Odhadne, jak moc je monitor v zorném poli agenta.
145
+ Vrátí 0.0–1.0 (0 = mimo pohled / za agentem, 1 = přímý pohled).
146
+ """
147
+ to_monitor = self.body.position - eye_pos
148
+ dist = float(length(to_monitor))
149
+ if dist < 0.01:
150
+ return 0.0
151
+ to_norm = to_monitor * (1.0 / dist)
152
+ dot = float(
153
+ forward[0] * to_norm[0] +
154
+ forward[1] * to_norm[1] +
155
+ forward[2] * to_norm[2]
156
+ )
157
+ if dot <= 0.0:
158
+ return 0.0
159
+ # Vzdálenostní útlum (1/r, max 5 m)
160
+ dist_fac = float(np.clip(1.0 - dist / 5.0, 0.0, 1.0))
161
+ return float(np.clip(dot * dist_fac, 0.0, 1.0))
162
+
163
+ def get_state(self) -> np.ndarray:
164
+ """
165
+ 3 floats: [is_on, frame_progress (0-1), mean_brightness (0-1)]
166
+ """
167
+ brightness = float(np.mean(self.current_frame)) / 255.0
168
+ return np.array([
169
+ float(self.is_on),
170
+ self._frame_idx / max(1, len(self._playlist) - 1),
171
+ brightness,
172
+ ], dtype=np.float32)
173
+
174
+
175
+ # ── Reproduktor ───────────────────────────────────────────────────────────────
176
+
177
+ # Frekvenční pásma (Hz) → normalizovaný index 0-1
178
+ _FREQ_BANDS = np.array([
179
+ 100, 200, 400, 800, 1600, 3200, 6400
180
+ ], dtype=np.float32)
181
+
182
+
183
+ class SpeakerObject:
184
+ """
185
+ Bodový zdroj zvuku. Nemá fyzické těleso — je to jen pozice + stav.
186
+ Vydává frekvenci, agent ji "slyší" jako stavovou veličinu (vzdálenostní útlum).
187
+ Tónové sekvence → asociační učení (Pavlovovo podmínění).
188
+ """
189
+ MAX_DIST = 8.0 # m — za touto vzdáleností není slyšet
190
+
191
+ def __init__(self, pos: Vec3, obj_id: str = "speaker"):
192
+ self.obj_id = obj_id
193
+ self.position = pos
194
+ self.freq_hz = 440.0 # A4
195
+ self.volume = 1.0 # 0.0–1.0
196
+ self.is_on = True
197
+
198
+ self._sequence: List[float] = [] # sekvence frekvencí
199
+ self._seq_idx = 0
200
+ self._steps_per_note = 8
201
+ self._step_count = 0
202
+
203
+ def load_tone_sequence(self, freqs_hz: List[float], steps_per_note: int = 8):
204
+ """Načte sekvenci tónů (analogie k učebnímu pořadu)."""
205
+ self._sequence = freqs_hz
206
+ self._seq_idx = 0
207
+ self._steps_per_note = steps_per_note
208
+ self._step_count = 0
209
+ if freqs_hz:
210
+ self.freq_hz = freqs_hz[0]
211
+
212
+ def step(self):
213
+ if not self.is_on or not self._sequence:
214
+ return
215
+ self._step_count += 1
216
+ if self._step_count % self._steps_per_note == 0:
217
+ self._seq_idx = (self._seq_idx + 1) % len(self._sequence)
218
+ self.freq_hz = self._sequence[self._seq_idx]
219
+
220
+ def heard_by(self, listener_pos: Vec3) -> Tuple[float, float]:
221
+ """
222
+ Vrátí (heard_freq_norm, amplitude) pro posluchače na dané pozici.
223
+ heard_freq_norm: 0.0–1.0 (normalizovaná frekvence)
224
+ amplitude: 0.0–1.0 (útlum se vzdáleností)
225
+ """
226
+ if not self.is_on:
227
+ return 0.0, 0.0
228
+ from engine.physics.rigid_body import length
229
+ dist = float(length(listener_pos - self.position))
230
+ amplitude = float(np.clip(1.0 - dist / self.MAX_DIST, 0.0, 1.0)) * self.volume
231
+ freq_norm = float(np.clip(
232
+ np.log2(self.freq_hz / 100.0) / np.log2(6400.0 / 100.0),
233
+ 0.0, 1.0,
234
+ ))
235
+ return freq_norm, amplitude
236
+
237
+ def get_state(self, listener_pos: Vec3) -> np.ndarray:
238
+ """2 floats: [freq_norm, amplitude]"""
239
+ fn, amp = self.heard_by(listener_pos)
240
+ return np.array([fn, amp], dtype=np.float32)
241
+
242
+
243
+ # ── MediaRoom — místnost s médii ──────────────────────────────────────────────
244
+
245
+ class MediaRoom:
246
+ """
247
+ Pomocná třída: přidá do světa monitor + reproduktor + lavici předmětů.
248
+ Volej media_room.step() každý fyzikální krok.
249
+ """
250
+
251
+ def __init__(self, phys: PhysicsWorld, origin: Vec3 = None,
252
+ pattern: str = "stripe"):
253
+ o = origin if origin is not None else vec3()
254
+ self.monitor = MonitorObject(phys,
255
+ pos=o + vec3(0.0, 1.4, -3.0), # zeď v záporném Z
256
+ obj_id="monitor_main",
257
+ )
258
+ self.speaker = SpeakerObject(
259
+ pos=o + vec3(0.5, 1.4, -3.0), # vedle monitoru
260
+ obj_id="speaker_main",
261
+ )
262
+ # Načti výchozí sekvence
263
+ self.monitor.load_pattern_sequence(pattern, n_frames=120)
264
+ self.speaker.load_tone_sequence(
265
+ [262, 330, 392, 523, 392, 330], # C-E-G-C'-G-E (arpeggio)
266
+ steps_per_note=12,
267
+ )
268
+
269
+ def step(self):
270
+ self.monitor.step()
271
+ self.speaker.step()
272
+
273
+ def reset(self, pattern: str = "stripe"):
274
+ self.monitor.load_pattern_sequence(pattern, n_frames=120)
275
+ self.speaker._seq_idx = 0
276
+ self.speaker._step_count = 0
embodied/observer.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Observer: reads the JSONL training log and reports interesting findings.
3
+ Can be run while training is live or as a post-analysis.
4
+ """
5
+ from __future__ import annotations
6
+ import json, os, sys
7
+ from pathlib import Path
8
+ from typing import List, Dict, Any
9
+ import numpy as np
10
+
11
+ RESULTS = Path(os.getenv("PHANTOM_RESULTS", "results"))
12
+
13
+
14
+ def load_log(log_path: Path) -> List[Dict]:
15
+ if not log_path.exists():
16
+ return []
17
+ rows = []
18
+ with log_path.open() as f:
19
+ for line in f:
20
+ line = line.strip()
21
+ if line:
22
+ try:
23
+ rows.append(json.loads(line))
24
+ except Exception:
25
+ pass
26
+ return rows
27
+
28
+
29
+ def analyse(rows: List[Dict]) -> Dict[str, Any]:
30
+ if not rows:
31
+ return {}
32
+
33
+ by_task: Dict[str, List[Dict]] = {}
34
+ for r in rows:
35
+ by_task.setdefault(r["task"], []).append(r)
36
+
37
+ report = {"total_episodes": len(rows), "tasks": {}}
38
+
39
+ for task, eps in by_task.items():
40
+ rewards = [e["total_reward"] for e in eps]
41
+ successes = [e["success"] for e in eps]
42
+ falls = [e["fallen"] for e in eps]
43
+ steps = [e["steps"] for e in eps]
44
+ noises = [e["noise"] for e in eps]
45
+
46
+ # Learning curve: improvement in last 20% vs first 20%
47
+ n = len(rewards)
48
+ split = max(1, n // 5)
49
+ early_avg = float(np.mean(rewards[:split]))
50
+ late_avg = float(np.mean(rewards[-split:]))
51
+ improvement = late_avg - early_avg
52
+
53
+ # First success episode
54
+ first_success = next((e["episode"] for e in eps if e["success"]), None)
55
+
56
+ # Interesting events across all episodes
57
+ all_interesting = [ev for e in eps for ev in e.get("interesting", [])]
58
+ event_counts: Dict[str, int] = {}
59
+ for ev in all_interesting:
60
+ key = ev.split("@")[0]
61
+ event_counts[key] = event_counts.get(key, 0) + 1
62
+
63
+ report["tasks"][task] = {
64
+ "n_episodes": n,
65
+ "success_rate": round(float(np.mean(successes)), 3),
66
+ "fall_rate": round(float(np.mean(falls)), 3),
67
+ "avg_reward": round(float(np.mean(rewards)), 3),
68
+ "max_reward": round(float(np.max(rewards)), 3),
69
+ "early_avg": round(early_avg, 3),
70
+ "late_avg": round(late_avg, 3),
71
+ "improvement": round(improvement, 3),
72
+ "avg_steps": round(float(np.mean(steps)), 1),
73
+ "final_noise": round(float(noises[-1]), 4),
74
+ "first_success_ep": first_success,
75
+ "event_counts": event_counts,
76
+ }
77
+
78
+ # Global learning trend (reward moving avg across all episodes)
79
+ all_rewards = [r["total_reward"] for r in rows]
80
+ window = max(1, len(all_rewards) // 10)
81
+ moving_avg = [float(np.mean(all_rewards[max(0,i-window):i+1]))
82
+ for i in range(len(all_rewards))]
83
+ report["global_moving_avg"] = [round(v, 3) for v in moving_avg[::max(1,len(moving_avg)//50)]]
84
+
85
+ return report
86
+
87
+
88
+ def print_report(report: Dict):
89
+ print("\n" + "═" * 65)
90
+ print(" PHANTOM EMBODIED TRAINING — OBSERVER REPORT")
91
+ print("═" * 65)
92
+ print(f" Total episodes: {report.get('total_episodes', 0)}")
93
+
94
+ for task, info in report.get("tasks", {}).items():
95
+ print(f"\n ── {task.upper()} ───────────────────────────────────────")
96
+ print(f" Episodes: {info['n_episodes']}")
97
+ print(f" Success rate: {info['success_rate']*100:.1f}%")
98
+ print(f" Fall rate: {info['fall_rate']*100:.1f}%")
99
+ print(f" Avg reward: {info['avg_reward']:.3f} (max {info['max_reward']:.3f})")
100
+ print(f" Learning: {info['early_avg']:.3f} → {info['late_avg']:.3f} "
101
+ f"(Δ {info['improvement']:+.3f})")
102
+ print(f" Avg steps: {info['avg_steps']:.0f}")
103
+ print(f" Final noise σ: {info['final_noise']:.4f}")
104
+ if info['first_success_ep'] is not None:
105
+ print(f" First SUCCESS: episode {info['first_success_ep']}")
106
+ if info["event_counts"]:
107
+ print(f" Events: {info['event_counts']}")
108
+
109
+ trend = report.get("global_moving_avg", [])
110
+ if trend:
111
+ print(f"\n ── GLOBAL REWARD TREND (sampled) ─────────────────────")
112
+ bar_len = 40
113
+ mn, mx = min(trend), max(trend)
114
+ rng = mx - mn if mx != mn else 1.0
115
+ for i, v in enumerate(trend):
116
+ bar = int((v - mn) / rng * bar_len)
117
+ print(f" [{i:3d}] {'█'*bar}{'·'*(bar_len-bar)} {v:.2f}")
118
+ print("\n" + "═" * 65)
119
+
120
+
121
+ def save_report(report: Dict, path: Path):
122
+ path.write_text(json.dumps(report, indent=2, default=str))
123
+
124
+
125
+ if __name__ == "__main__":
126
+ import argparse
127
+ parser = argparse.ArgumentParser()
128
+ parser.add_argument("--log", default="logs/embodied_training.jsonl")
129
+ parser.add_argument("--out", default="results/embodied_report.json")
130
+ args = parser.parse_args()
131
+
132
+ rows = load_log(Path(args.log))
133
+ report = analyse(rows)
134
+ print_report(report)
135
+ save_report(report, Path(args.out))
embodied/phantom_acc.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PhantomACC — ACCConflictDetector wrapper.
3
+
4
+ Detects conflicts between:
5
+ - current intent (task name)
6
+ - sensory percepts (hand/torso from body state)
7
+ - emotional state (from BioCor)
8
+
9
+ When conflict is high → triggers exploration boost.
10
+ When safety_bypass=True → vetoes current action and resets exploration.
11
+ """
12
+ from __future__ import annotations
13
+ import numpy as np
14
+ import logging
15
+ from typing import Optional
16
+
17
+ log = logging.getLogger("phantom.acc")
18
+
19
+ try:
20
+ from cogcore.routing.acc import ACCConflictDetector, ConflictReport
21
+ _HAS_ACC = True
22
+ except ImportError:
23
+ _HAS_ACC = False
24
+ log.warning("cogcore.routing.acc not available — ACC in passthrough mode")
25
+
26
+ # Score at which the ACC recommends an exploration boost
27
+ _EXPLORATION_TRIGGER = 0.25
28
+
29
+
30
+ class PhantomACC:
31
+ """Wraps ACCConflictDetector; exposes conflict_score and exploration signals."""
32
+
33
+ def __init__(self):
34
+ if _HAS_ACC:
35
+ self._acc = ACCConflictDetector(
36
+ intent_emotion_threshold=0.30,
37
+ modality_intent_threshold=0.50,
38
+ domain_intent_threshold=0.40,
39
+ discord_threshold=0.30,
40
+ safety_bypass_threshold=0.88,
41
+ )
42
+ else:
43
+ self._acc = None
44
+ self._last_score: float = 0.0
45
+ self._safety_bypass: bool = False
46
+
47
+ # ── public ────────────────────────────────────────────────────────────
48
+
49
+ def detect(self, task_name: str, body_state: np.ndarray,
50
+ bio_vec: np.ndarray) -> float:
51
+ """
52
+ Run conflict detection; returns conflict score in [0, 1].
53
+ Also updates self.should_explore and self.safety_bypass.
54
+ """
55
+ if not _HAS_ACC or self._acc is None:
56
+ return 0.0
57
+
58
+ percepts = self._make_percepts(body_state)
59
+ emotion = self._make_emotion(bio_vec)
60
+ report: ConflictReport = self._acc.detect(
61
+ intent=task_name,
62
+ percepts=percepts,
63
+ emotion=emotion,
64
+ )
65
+ self._last_score = float(report.score)
66
+ self._safety_bypass = bool(report.safety_bypass)
67
+
68
+ if report.safety_bypass:
69
+ log.warning("ACC safety_bypass! intent=%s score=%.2f", task_name, report.score)
70
+ elif report.score >= _EXPLORATION_TRIGGER:
71
+ log.debug("ACC conflict=%.2f (%s) → exploration boost", report.score, report.strongest)
72
+
73
+ return self._last_score
74
+
75
+ @property
76
+ def score(self) -> float:
77
+ return self._last_score
78
+
79
+ @property
80
+ def should_explore(self) -> bool:
81
+ return self._last_score >= _EXPLORATION_TRIGGER
82
+
83
+ @property
84
+ def safety_bypass(self) -> bool:
85
+ return self._safety_bypass
86
+
87
+ def noise_multiplier(self) -> float:
88
+ """Maps conflict score to noise multiplier (1.0 – 2.0)."""
89
+ return float(1.0 + np.clip(self._last_score * 2.0, 0.0, 1.0))
90
+
91
+ # ── internal ──────────────────────────────────────────────────────────
92
+
93
+ @staticmethod
94
+ def _make_percepts(body_state: np.ndarray) -> dict:
95
+ bs = body_state
96
+ return {
97
+ "visual": float(np.clip(abs(float(bs[18])) if len(bs) > 18 else 0.5, 0.0, 1.0)),
98
+ "motor": float(np.clip(1.0 - abs(float(bs[12])) if len(bs) > 12 else 0.5, 0.0, 1.0)),
99
+ "balance": float(np.clip(1.0 - abs(float(bs[0])) if len(bs) > 0 else 0.5, 0.0, 1.0)),
100
+ }
101
+
102
+ @staticmethod
103
+ def _make_emotion(bio_vec: np.ndarray) -> dict:
104
+ # bio_vec = [pfc, hpc, acc, ins, amy]
105
+ amy = float(bio_vec[4]) if len(bio_vec) > 4 else 0.16
106
+ ins = float(bio_vec[3]) if len(bio_vec) > 3 else 0.18
107
+ return {
108
+ "urgency": float(np.clip(amy * 3.0, 0.0, 1.0)),
109
+ "valence": float(np.clip(1.0 - ins * 2.0, 0.0, 1.0)),
110
+ }
embodied/phantom_biocor.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PhantomBioCor — FiveBioEngine safety wrapper (BioCor layer).
3
+
4
+ Roles:
5
+ 1. Biological state tracker: pfc / hpc / acc / ins / amy (5 floats appended to state)
6
+ 2. Safety gate: if bio detects unsafe goal_vec from external LLM (Gemini),
7
+ clamp or zero urgency to protect RL policy from black-box manipulation
8
+ 3. Neurotransmitter modulation: DA/NE scale exploration noise, OXT scales proximity
9
+
10
+ Output slice appended to state vector: [pfc, hpc, acc, ins, amy] → 5 floats
11
+ """
12
+ from __future__ import annotations
13
+ import numpy as np
14
+ import logging
15
+ from typing import Dict, Any, Optional
16
+
17
+ log = logging.getLogger("phantom.biocor")
18
+
19
+ try:
20
+ from cogcore.bio.engine import FiveBioEngine, FiveBioConfig
21
+ _HAS_BIO = True
22
+ except ImportError:
23
+ _HAS_BIO = False
24
+ log.warning("cogcore.bio not available — BioCor in passthrough mode")
25
+
26
+ BIO_DIM = 5 # pfc, hpc, acc, ins, amy
27
+
28
+ # Urgency clamp threshold: if Amy spike is too high, distrust external goal
29
+ _AMY_SAFETY_THRESHOLD = 0.75
30
+ # Maximum allowed urgency from Gemini before BioCor overrides it
31
+ _MAX_ALLOWED_URGENCY = 0.92
32
+
33
+
34
+ class PhantomBioCor:
35
+ """
36
+ FiveBioEngine wrapper.
37
+ Call step() every RL step; the 5-dim bio slice is appended to agent state.
38
+ Call moderate_goal() to safety-filter Gemini goal vectors.
39
+ """
40
+
41
+ def __init__(self):
42
+ if _HAS_BIO:
43
+ self._engine = FiveBioEngine(FiveBioConfig(
44
+ pfc_tonic=0.32, hpc_tonic=0.25,
45
+ acc_tonic=0.18, ins_tonic=0.18, amy_tonic=0.16,
46
+ sentiment_tau=30.0, memory_tau=50.0, drift_tau=24.0,
47
+ ))
48
+ else:
49
+ self._engine = None
50
+ self._last: Optional[Dict[str, Any]] = None
51
+
52
+ # ── public ────────────────────────────────────────────────────────────
53
+
54
+ def step(self, reward: float, memory_pressure: float,
55
+ conflict_score: float, task_name: str) -> np.ndarray:
56
+ """
57
+ Update bio state and return float32[5] = [pfc, hpc, acc, ins, amy].
58
+ signals mapping: reward → sentiment, memory_pressure → memory_load,
59
+ conflict_score → drift
60
+ """
61
+ if self._engine is None:
62
+ return np.zeros(BIO_DIM, np.float32)
63
+
64
+ signals: Dict[str, float] = {
65
+ "sentiment": float(np.clip(reward, -1.0, 1.0)),
66
+ "memory_load": float(np.clip(memory_pressure, 0.0, 1.0)),
67
+ "drift": float(np.clip(conflict_score, 0.0, 1.0)),
68
+ }
69
+ result = self._engine.step(signals, topic="motor", intent=task_name)
70
+ self._last = result
71
+ return np.array(
72
+ [result["pfc"], result["hpc"], result["acc"],
73
+ result["ins"], result["amy"]], dtype=np.float32
74
+ )
75
+
76
+ def moderate_goal(self, goal_vec: np.ndarray) -> np.ndarray:
77
+ """
78
+ BioCor safety gate: inspect Gemini goal_vec and clamp if unsafe.
79
+ goal_vec[23] = urgency; [30] = grip; [31] = speed.
80
+ Returns a (possibly modified) copy of goal_vec.
81
+ """
82
+ if self._last is None:
83
+ return goal_vec
84
+ vec = goal_vec.copy()
85
+ amy = float(self._last.get("amy", 0.0))
86
+ pfc = float(self._last.get("pfc", 0.5))
87
+
88
+ if amy > _AMY_SAFETY_THRESHOLD:
89
+ # Amygdala overactivation: distrust external urgency, defer to PFC
90
+ vec[23] = float(np.clip(vec[23], 0.0, pfc))
91
+ log.debug("BioCor: amy=%.2f — urgency clamped to %.2f", amy, vec[23])
92
+
93
+ # Hard cap on urgency from black-box transformer
94
+ if vec[23] > _MAX_ALLOWED_URGENCY:
95
+ vec[23] = _MAX_ALLOWED_URGENCY
96
+ log.debug("BioCor: urgency hard-cap applied → %.2f", vec[23])
97
+
98
+ return vec
99
+
100
+ def noise_scale(self) -> float:
101
+ """Return DA/NE-based multiplier for exploration noise (0.5 – 1.5)."""
102
+ if self._last is None:
103
+ return 1.0
104
+ da = float(self._last.get("DA", 0.25))
105
+ ne = float(self._last.get("NE", 0.20))
106
+ # High DA → more exploration; high NE alone → more caution
107
+ scale = 0.5 + 2.0 * da + 1.0 * ne
108
+ return float(np.clip(scale, 0.5, 1.5))
109
+
110
+ def proximity_scale(self) -> float:
111
+ """OXT (oxytocin) boosts social/approach reward scaling (0.8 – 1.3)."""
112
+ if self._last is None:
113
+ return 1.0
114
+ oxt = float(self._last.get("OXT", 0.27))
115
+ return float(np.clip(0.8 + oxt * 1.8, 0.8, 1.3))
116
+
117
+ def reset(self):
118
+ self._last = None
119
+ if self._engine is not None and hasattr(self._engine, 'reset'):
120
+ self._engine.reset()
embodied/phantom_cortex.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PhantomCortex — complete cognitive architecture integration.
3
+
4
+ State vector (73 dims):
5
+ [0:28] body state (VirtualBody) — incl. gyro [24:27] + abs_height [27]
6
+ [28:36] memory scalars (4-tier × 2: p1,e1,p2,e2,p3,e3,consensus,centrality)
7
+ [36:68] goal vector (GoalSpec → 32-dim, from Gemini or fallback)
8
+ [68:73] bio state (pfc, hpc, acc, ins, amy)
9
+
10
+ Data flow per step:
11
+ 1. Memory.step(mem_inputs) → mem_scalars[8]
12
+ 2. ACC.detect(...) → conflict_score
13
+ 3. BioCor.step(...) → bio_vec[5]
14
+ 4. BioCor.moderate_goal(goal) → safety-filtered goal_vec
15
+ 5. Concatenate all → state[73]
16
+ 6. Noise = base × ACC.noise_mult × BioCor.noise_scale
17
+ """
18
+ from __future__ import annotations
19
+ import numpy as np
20
+ import logging
21
+ from typing import Optional
22
+
23
+ from embodied.phantom_memory import PhantomMemory, MEM_OUT_DIM
24
+ from embodied.phantom_biocor import PhantomBioCor, BIO_DIM
25
+ from embodied.phantom_acc import PhantomACC
26
+ from embodied.goal_encoder import GOAL_DIM, GoalSpec, encode_goal, goal_for_task
27
+
28
+ log = logging.getLogger("phantom.cortex")
29
+
30
+ BODY_DIM = 28
31
+ CORTEX_DIM = BODY_DIM + MEM_OUT_DIM + GOAL_DIM + BIO_DIM # 28+8+32+5 = 73
32
+
33
+
34
+ class PhantomCortex:
35
+ """
36
+ Integration hub. One instance per agent; call step() every RL step.
37
+ """
38
+
39
+ def __init__(self):
40
+ self.memory = PhantomMemory()
41
+ self.biocor = PhantomBioCor()
42
+ self.acc = PhantomACC()
43
+
44
+ self._mem_scalars = np.zeros(MEM_OUT_DIM, np.float32)
45
+ self._bio_vec = np.zeros(BIO_DIM, np.float32)
46
+ self._goal_vec = np.zeros(GOAL_DIM, np.float32)
47
+ self._conflict = 0.0
48
+
49
+ # ── episode boundary ──────────────────────────────────────────────────
50
+
51
+ def reset(self, task_name: str = "stand"):
52
+ self.memory.reset()
53
+ self.biocor.reset()
54
+ self._mem_scalars = np.zeros(MEM_OUT_DIM, np.float32)
55
+ self._bio_vec = np.zeros(BIO_DIM, np.float32)
56
+ self._goal_vec = encode_goal(goal_for_task(task_name))
57
+ self._conflict = 0.0
58
+
59
+ def set_goal(self, goal_vec: np.ndarray):
60
+ """Set goal_vec from Gemini (or fallback); BioCor will moderate it."""
61
+ self._goal_vec = goal_vec.astype(np.float32)
62
+
63
+ # ── per-step ──────────────────────────────────────────────────────────
64
+
65
+ def step(self, body_state: np.ndarray, last_action: np.ndarray,
66
+ reward: float, task_name: str) -> np.ndarray:
67
+ """
68
+ Full cortex tick. Returns augmented state float32[69].
69
+ Called every RL environment step.
70
+ """
71
+ # 1. Memory inputs from body state + last action
72
+ mem_in = self._build_mem_inputs(body_state, last_action)
73
+ self._mem_scalars = self.memory.step(mem_in)
74
+
75
+ # 2. ACC conflict detection
76
+ self._conflict = self.acc.detect(task_name, body_state, self._bio_vec)
77
+
78
+ # 3. Bio update (uses reward + memory pressure + conflict)
79
+ mem_pressure = float(self._mem_scalars[0]) # tier1 pressure
80
+ self._bio_vec = self.biocor.step(reward, mem_pressure, self._conflict, task_name)
81
+
82
+ # 4. Goal moderation (BioCor safety filter on Gemini goal)
83
+ safe_goal = self.biocor.moderate_goal(self._goal_vec)
84
+
85
+ # 5. Concatenate
86
+ return np.concatenate([
87
+ body_state.astype(np.float32),
88
+ self._mem_scalars,
89
+ safe_goal,
90
+ self._bio_vec,
91
+ ]).astype(np.float32)
92
+
93
+ def noise_multiplier(self, base_noise: float) -> float:
94
+ """
95
+ ACC conflict + biocor DA/NE modulate exploration noise.
96
+ Returns actual noise std to use.
97
+ """
98
+ acc_mult = self.acc.noise_multiplier() # 1.0 – 2.0
99
+ bio_scale = self.biocor.noise_scale() # 0.5 – 1.5
100
+ return float(np.clip(base_noise * acc_mult * bio_scale, 0.01, 1.0))
101
+
102
+ def safety_bypass(self) -> bool:
103
+ """True when ACC signals safety_bypass — action should be overridden."""
104
+ return self.acc.safety_bypass
105
+
106
+ # ── helpers ───────────────────────────────────────────────────────────
107
+
108
+ @staticmethod
109
+ def _build_mem_inputs(body_state: np.ndarray,
110
+ last_action: np.ndarray) -> list[float]:
111
+ """5-channel memory input: hand_L_y / hand_R_y / torso_tilt / grip_L / grip_R."""
112
+ bs = body_state
113
+ grip_L = float(last_action[8]) if len(last_action) > 8 else 0.0
114
+ grip_R = float(last_action[9]) if len(last_action) > 9 else 0.0
115
+ return [
116
+ float(np.tanh(bs[18])) if len(bs) > 18 else 0.0, # hand_L_rel_y
117
+ float(np.tanh(bs[19])) if len(bs) > 19 else 0.0, # hand_R_rel_y
118
+ float(np.tanh(bs[12])) if len(bs) > 12 else 0.0, # torso_tilt_x
119
+ float(np.tanh(grip_L)),
120
+ float(np.tanh(grip_R)),
121
+ ]
embodied/phantom_memory.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PhantomMemory — 4-tier HexaPenta memory hierarchy.
3
+
4
+ Tier 1 working HexaPentaMemoryLattice depth=600 (fast, shallow)
5
+ Tier 2 episodic FractalHexaPentaMemory depth=15000 segments=3
6
+ Tier 3 long-term FractalHexaPentaMemory depth=100000 segments=7 scale_levels=2
7
+ Tier 4 archive DataLedgerMemory shards=7 retention_blocks=4096
8
+
9
+ PAC-style coupling: tier-N energy gates how often tier-(N+1) is updated.
10
+ Output: np.ndarray[8] — [p1,e1, p2,e2, p3,e3, p4c,p4n]
11
+ p = pressure, e = energy, p4c = DataLedger consensus mean, p4n = centrality
12
+ """
13
+ from __future__ import annotations
14
+ import numpy as np
15
+ import logging
16
+ from typing import Sequence
17
+
18
+ log = logging.getLogger("phantom.memory")
19
+
20
+ try:
21
+ from cogcore.vendor.genomeforge.vertical_memory import (
22
+ HexaPentaMemoryLattice, HexaPentaMemoryConfig,
23
+ )
24
+ from cogcore.vendor.genomeforge.fractal_memory import (
25
+ FractalHexaPentaMemory, FractalHexaPentaMemoryConfig,
26
+ )
27
+ from cogcore.vendor.genomeforge.data_ledger_memory import (
28
+ DataLedgerMemory, DataLedgerMemoryConfig,
29
+ )
30
+ _HAS_MEMORY = True
31
+ except ImportError:
32
+ _HAS_MEMORY = False
33
+ log.warning("genomeforge not available — PhantomMemory running in zero-output mode")
34
+
35
+ MEM_OUT_DIM = 8 # 2 scalars × 4 tiers
36
+
37
+
38
+ class PhantomMemory:
39
+ """4-tier episodic/long-term memory with PAC-style inter-tier gating."""
40
+
41
+ # PAC gate: tier N+1 updates only when tier N energy exceeds this
42
+ _T1_GATE = 0.05
43
+ _T2_GATE = 0.08
44
+ _T3_GATE = 0.10
45
+
46
+ # Fall-back update intervals when gate is never triggered
47
+ _T2_INTERVAL = 4
48
+ _T3_INTERVAL = 16
49
+ _T4_INTERVAL = 64
50
+
51
+ def __init__(self):
52
+ self._step = 0
53
+ self._t1_energy = 0.0
54
+
55
+ if not _HAS_MEMORY:
56
+ self._t1 = self._t2 = self._t3 = self._t4 = None
57
+ return
58
+
59
+ self._t1 = HexaPentaMemoryLattice(HexaPentaMemoryConfig(
60
+ depth=600, active_depth=32,
61
+ tau=0.88, coupling=0.12, ring_coupling=0.10,
62
+ rotation_rate=0.05, rotation_mode="spiral_hex",
63
+ ))
64
+ self._t2 = FractalHexaPentaMemory(FractalHexaPentaMemoryConfig(
65
+ depth=15000, scale_levels=1, segments=3, active_segments=1,
66
+ tau=0.88, coupling=0.12, ring_coupling=0.10,
67
+ scale_coupling=0.18, segment_coupling=0.15,
68
+ consolidation_interval=8, consolidation_strength=0.25,
69
+ rotation_rate=0.05, rotation_mode="spiral_hex",
70
+ ))
71
+ self._t3 = FractalHexaPentaMemory(FractalHexaPentaMemoryConfig(
72
+ depth=100000, scale_levels=2, segments=7, active_segments=1,
73
+ tau=0.90, coupling=0.10, ring_coupling=0.08,
74
+ scale_coupling=0.20, segment_coupling=0.18,
75
+ consolidation_interval=16, consolidation_strength=0.30,
76
+ rotation_rate=0.03, rotation_mode="spiral_hex",
77
+ ))
78
+ self._t4 = DataLedgerMemory(DataLedgerMemoryConfig(
79
+ channels=5, shards=7,
80
+ consensus_window=64, checkpoint_interval=16,
81
+ retention_blocks=4096,
82
+ tau=0.82, recovery_strength=0.35, evolution_rate=0.10,
83
+ ))
84
+
85
+ # Last snapshots (for PAC coupling)
86
+ self._s1 = self._s2 = self._s3 = self._s4 = None
87
+
88
+ # ── public ────────────────────────────────────────────────────────────
89
+
90
+ def step(self, features: Sequence[float]) -> np.ndarray:
91
+ """
92
+ Feed 5 sensory scalars (P/U/Z/B/η) through all tiers.
93
+ Returns float32[8]: [p1,e1, p2,e2, p3,e3, consensus,centrality]
94
+ """
95
+ self._step += 1
96
+ if not _HAS_MEMORY or self._t1 is None:
97
+ return np.zeros(MEM_OUT_DIM, np.float32)
98
+
99
+ feats = list(features[:5]) if len(features) >= 5 else list(features) + [0.0] * (5 - len(features))
100
+
101
+ # Tier 1 — always updated
102
+ s1 = self._t1.step(feats)
103
+ self._s1 = s1
104
+ self._t1_energy = s1.energy
105
+
106
+ # Tier 2 — PAC gate from tier 1 energy OR periodic fallback
107
+ t2_trigger = (s1.energy >= self._T1_GATE) or (self._step % self._T2_INTERVAL == 0)
108
+ if t2_trigger:
109
+ self._s2 = self._t2.step(feats)
110
+
111
+ # Tier 3 — PAC gate from tier 2 energy
112
+ s2_energy = self._s2.energy if self._s2 is not None else 0.0
113
+ t3_trigger = (s2_energy >= self._T2_GATE) or (self._step % self._T3_INTERVAL == 0)
114
+ if t3_trigger:
115
+ self._s3 = self._t3.step(feats)
116
+
117
+ # Tier 4 — PAC gate from tier 3 energy
118
+ s3_energy = self._s3.energy if self._s3 is not None else 0.0
119
+ t4_trigger = (s3_energy >= self._T3_GATE) or (self._step % self._T4_INTERVAL == 0)
120
+ if t4_trigger:
121
+ self._s4 = self._t4.step(feats)
122
+
123
+ return self._pack()
124
+
125
+ def reset(self):
126
+ """Reset all tiers at episode boundary."""
127
+ self._step = 0
128
+ self._t1_energy = 0.0
129
+ self._s1 = self._s2 = self._s3 = self._s4 = None
130
+ if _HAS_MEMORY and self._t1 is not None:
131
+ for tier in (self._t1, self._t2, self._t3, self._t4):
132
+ if hasattr(tier, 'reset'):
133
+ tier.reset()
134
+
135
+ # ── internal ──────────────────────────────────────────────────────────
136
+
137
+ def _pack(self) -> np.ndarray:
138
+ out = np.zeros(MEM_OUT_DIM, np.float32)
139
+ if self._s1 is not None:
140
+ out[0] = float(self._s1.pressure)
141
+ out[1] = float(self._s1.energy)
142
+ if self._s2 is not None:
143
+ out[2] = float(self._s2.pressure)
144
+ out[3] = float(self._s2.energy)
145
+ if self._s3 is not None:
146
+ out[4] = float(self._s3.pressure)
147
+ out[5] = float(self._s3.energy)
148
+ if self._s4 is not None:
149
+ cons = self._s4.consensus
150
+ out[6] = float(np.mean(cons)) if hasattr(cons, '__len__') else float(cons)
151
+ out[7] = float(np.clip(self._s4.centrality, 0.0, 1.0))
152
+ return out
embodied/physics_world.py ADDED
@@ -0,0 +1,651 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PhysicsWorldEnv: voxelový fyzikální svět s gravitací, terénem, interaktivními
3
+ objekty a reálnou fyzikou rozbití (FractureBridge, MaterialDegradation).
4
+
5
+ Materiálová fyzika rozbití:
6
+ - Při každé kolizi se počítá rychlost nárazu (relative_velocity z manifoldu)
7
+ - Kinetická energie → tepelný spike + entropie (inelasticita) → stress pole
8
+ - FractureBridge.evaluate() rozhodne zda objekt praská
9
+ - Při rozpadnutí se spawnují fragmenty s náhodnou rychlostí od nárazu
10
+ """
11
+ from __future__ import annotations
12
+ import sys, os
13
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
14
+
15
+ import numpy as np
16
+ from dataclasses import dataclass, field
17
+ from typing import Dict, List, Optional, Tuple
18
+
19
+ from engine.physics.rigid_body import PhysicsWorld, BodyType, vec3, Vec3, length
20
+ from engine.sim.fracture import FractureBridge, FractureBridgeConfig, compute_stress
21
+
22
+
23
+ # ── Materiálové vlastnosti ────────────────────────────────────────────────────
24
+
25
+ @dataclass(frozen=True)
26
+ class MaterialProps:
27
+ name: str
28
+ yield_velocity: float # m/s — minimální rychlost nárazu pro rozbití
29
+ thermal_sensitivity: float # nárůst T na jednotku kinetické energie
30
+ entropy_base: float # základní entropie materiálu (neuspořádanost)
31
+ fracture_threshold: float # práh FractureBridge (0..1)
32
+ n_fragments: int # počet úlomků
33
+ fragment_size_frac: float # úlomek = frac × original half-size
34
+
35
+
36
+ MATERIAL_PROPS: Dict[str, MaterialProps] = {
37
+ "cup": MaterialProps(
38
+ name="ceramic",
39
+ # 4.5 m/s ≈ drop from ~1 m (table-height drop breaks, gentle placement survives)
40
+ yield_velocity=4.5,
41
+ thermal_sensitivity=0.08, # scaled down so risk is gradual above threshold
42
+ entropy_base=0.40,
43
+ fracture_threshold=0.48,
44
+ n_fragments=6,
45
+ fragment_size_frac=0.45,
46
+ ),
47
+ "block": MaterialProps(
48
+ name="wood",
49
+ yield_velocity=8.0, # block survives falls up to ~3 m
50
+ thermal_sensitivity=0.04,
51
+ entropy_base=0.18,
52
+ fracture_threshold=0.62,
53
+ n_fragments=3,
54
+ fragment_size_frac=0.55,
55
+ ),
56
+ "handle": MaterialProps(
57
+ name="metal",
58
+ yield_velocity=14.0, # kovová klika se téměř nerozbije
59
+ thermal_sensitivity=0.015,
60
+ entropy_base=0.06,
61
+ fracture_threshold=0.80,
62
+ n_fragments=2,
63
+ fragment_size_frac=0.6,
64
+ ),
65
+ "ball": MaterialProps(
66
+ name="rubber",
67
+ yield_velocity=50.0, # gumový míč se nikdy nerozbije
68
+ thermal_sensitivity=0.01,
69
+ entropy_base=0.05,
70
+ fracture_threshold=0.99,
71
+ n_fragments=1,
72
+ fragment_size_frac=0.9,
73
+ ),
74
+ "cube": MaterialProps(
75
+ name="plastic",
76
+ yield_velocity=10.0, # plastová kostka je odolná
77
+ thermal_sensitivity=0.03,
78
+ entropy_base=0.15,
79
+ fracture_threshold=0.70,
80
+ n_fragments=3,
81
+ fragment_size_frac=0.50,
82
+ ),
83
+ "shape_piece": MaterialProps(
84
+ name="wood",
85
+ yield_velocity=8.0,
86
+ thermal_sensitivity=0.04,
87
+ entropy_base=0.18,
88
+ fracture_threshold=0.65,
89
+ n_fragments=2,
90
+ fragment_size_frac=0.55,
91
+ ),
92
+ }
93
+
94
+ _FRACTURE_BRIDGE = FractureBridge(FractureBridgeConfig(
95
+ fracture_risk_threshold=0.50,
96
+ propagation_enabled=True,
97
+ propagation_radius=1,
98
+ propagation_gain=0.38,
99
+ propagation_seed_top_k=8,
100
+ ))
101
+
102
+
103
+ def _impact_stress(mat: MaterialProps, impact_speed: float,
104
+ obj_mass: float, restitution: float) -> float:
105
+ """
106
+ Převede rychlost nárazu → skalární stress (0..1).
107
+ Využívá FractureBridge přes 2×2×2 voxelové pole.
108
+ """
109
+ ke_per_kg = 0.5 * impact_speed ** 2
110
+ T_spike = 293.15 + ke_per_kg * mat.thermal_sensitivity * 400.0
111
+ S_spike = mat.entropy_base + (1.0 - restitution) * ke_per_kg * 0.25
112
+
113
+ shape = (2, 2, 2)
114
+ vox = np.ones(shape, dtype=np.int32)
115
+ thermal = np.full(shape, T_spike, dtype=np.float32)
116
+ entropy = np.full(shape, S_spike, dtype=np.float32)
117
+ stress = compute_stress(
118
+ vox, thermal, entropy,
119
+ thermal_expansion_coeff=5e-4,
120
+ entropy_damage_scale=0.45,
121
+ )
122
+ result = _FRACTURE_BRIDGE.evaluate(
123
+ external_failure_risk_field=stress.astype(np.float64),
124
+ base_stress_field=stress.astype(np.float64),
125
+ )
126
+ return float(result["fracture_risk_max"])
127
+
128
+
129
+ # ── Voxel grid ────────────────────────────────────────────────────────────────
130
+
131
+ class VoxelGrid:
132
+ def __init__(self, size: Tuple[int, int, int]):
133
+ self.data = np.zeros(size, dtype=np.uint8)
134
+ self.size = size
135
+
136
+ def is_solid(self, x: int, y: int, z: int) -> bool:
137
+ sx, sy, sz = self.size
138
+ if 0 <= x < sx and 0 <= y < sy and 0 <= z < sz:
139
+ return bool(self.data[x, y, z] > 0)
140
+ return True
141
+
142
+ def set_solid(self, x: int, y: int, z: int, v: int = 1):
143
+ sx, sy, sz = self.size
144
+ if 0 <= x < sx and 0 <= y < sy and 0 <= z < sz:
145
+ self.data[x, y, z] = v
146
+
147
+
148
+ # ── Interactive object ────────────────────────────────────────────────────────
149
+
150
+ @dataclass
151
+ class InteractiveObject:
152
+ id: str
153
+ obj_type: str
154
+ initial_pos: Vec3
155
+ body_id: str
156
+ is_grasped: bool = False
157
+ held_by: str = ""
158
+ is_broken: bool = False
159
+ fragment_ids: List[str] = field(default_factory=list)
160
+ # Accumulated impact energy between physics steps (reset after fracture check)
161
+ _pending_impact_speed: float = field(default=0.0, repr=False)
162
+ _pending_impact_vel: object = field(default=None, repr=False)
163
+
164
+
165
+ # ── World ─────────────────────────────────────────────────────────────────────
166
+
167
+ class WorldEnv:
168
+ WORLD_SIZE = (64, 32, 64)
169
+
170
+ def __init__(self, seed: int = 42, terrain_type: str = "flat"):
171
+ self.seed = seed
172
+ self.terrain_type = terrain_type
173
+ self.voxels = VoxelGrid(self.WORLD_SIZE)
174
+ self.phys = PhysicsWorld(gravity=vec3(0, -9.81, 0), substeps=3)
175
+ self._tick = 0
176
+ self.interactive_objects: Dict[str, InteractiveObject] = {}
177
+ self._grasped: Dict[str, str] = {}
178
+ self.maze_goal: Optional[Vec3] = None
179
+ self._fracture_log: List[dict] = [] # for observer reporting
180
+ self._build_terrain()
181
+ self._register_collision_callback()
182
+
183
+ # ── Collision callback — live fracture detection ──────────────────────────
184
+
185
+ def _register_collision_callback(self):
186
+ def _on_collision(manifold):
187
+ for body in (manifold.body_a, manifold.body_b):
188
+ bid = body.body_id
189
+ if bid in self.interactive_objects:
190
+ obj = self.interactive_objects[bid]
191
+ if obj.is_broken or obj.is_grasped:
192
+ continue
193
+ speed = float(length(manifold.relative_velocity))
194
+ # Keep worst hit per step
195
+ if speed > obj._pending_impact_speed:
196
+ obj._pending_impact_speed = speed
197
+ obj._pending_impact_vel = manifold.relative_velocity
198
+
199
+ self.phys.on_collision(_on_collision)
200
+
201
+ # ── Terrain builders ──────────────────────────────────────────────────────
202
+
203
+ def _build_terrain(self):
204
+ dispatch = {
205
+ "flat": self._build_flat,
206
+ "hills": self._build_hills,
207
+ "maze": self._build_maze,
208
+ "obstacle": self._build_obstacle_course,
209
+ "stairs": self._build_stairs,
210
+ "household": self._build_household,
211
+ "military": self._build_military_course,
212
+ }
213
+ dispatch.get(self.terrain_type, self._build_flat)()
214
+ self._add_boundary_walls()
215
+ self._add_ground_body()
216
+
217
+ def _add_ground_body(self):
218
+ SX, _, SZ = self.WORLD_SIZE
219
+ self.phys.create_body(
220
+ "ground_plane", BodyType.STATIC,
221
+ position=vec3(SX / 2, -10.0, SZ / 2),
222
+ size=vec3(float(SX * 2), 20.0, float(SZ * 2)),
223
+ mass=0.0, restitution=0.15, friction=0.85,
224
+ )
225
+
226
+ def _build_flat(self):
227
+ SX, _, SZ = self.WORLD_SIZE
228
+ for x in range(SX):
229
+ for z in range(SZ):
230
+ self.voxels.set_solid(x, 0, z, 1)
231
+
232
+ def _build_hills(self):
233
+ SX, SY, SZ = self.WORLD_SIZE
234
+ rng = np.random.default_rng(self.seed)
235
+ for x in range(SX):
236
+ for z in range(SZ):
237
+ h = int(2 + 2.5 * np.sin(x * 0.3) * np.cos(z * 0.25) +
238
+ rng.uniform(0, 0.8))
239
+ for y in range(min(h + 1, SY)):
240
+ self.voxels.set_solid(x, y, z, 1)
241
+
242
+ def _build_maze(self):
243
+ SX, _, SZ = self.WORLD_SIZE
244
+ for x in range(SX):
245
+ for z in range(SZ):
246
+ self.voxels.set_solid(x, 0, z, 1)
247
+ CELL = 4
248
+ rows = SZ // CELL
249
+ cols = SX // CELL
250
+ visited = np.zeros((cols, rows), dtype=bool)
251
+ walls_h = np.ones((cols, rows + 1), dtype=bool)
252
+ walls_v = np.ones((cols + 1, rows), dtype=bool)
253
+
254
+ def _carve(cx, cy):
255
+ visited[cx, cy] = True
256
+ dirs = [(1,0),(-1,0),(0,1),(0,-1)]
257
+ np.random.default_rng(self.seed + cx * 97 + cy).shuffle(dirs)
258
+ for dx, dy in dirs:
259
+ nx, ny = cx + dx, cy + dy
260
+ if 0 <= nx < cols and 0 <= ny < rows and not visited[nx, ny]:
261
+ if dy == 0:
262
+ walls_v[max(cx, nx), cy] = False
263
+ else:
264
+ walls_h[cx, max(cy, ny)] = False
265
+ _carve(nx, ny)
266
+
267
+ _carve(0, 0)
268
+
269
+ def _wall(x0, z0, x1, z1):
270
+ for x in range(min(x0,x1), max(x0,x1)+1):
271
+ for z in range(min(z0,z1), max(z0,z1)+1):
272
+ for y in range(1, 5):
273
+ self.voxels.set_solid(x, y, z, 2)
274
+
275
+ for cx in range(cols):
276
+ for cy in range(rows):
277
+ wx0 = cx * CELL
278
+ wz0 = cy * CELL
279
+ if walls_v[cx, cy]:
280
+ _wall(wx0, wz0, wx0, wz0 + CELL - 1)
281
+ if walls_h[cx, cy]:
282
+ _wall(wx0, wz0, wx0 + CELL - 1, wz0)
283
+ _wall(0, 0, 0, SZ - 1)
284
+ _wall(SX-1, 0, SX-1, SZ-1)
285
+ _wall(0, 0, SX-1, 0)
286
+ _wall(0, SZ-1, SX-1, SZ-1)
287
+ gx = (cols - 1) * CELL + 2
288
+ gz = (rows - 1) * CELL + 2
289
+ for y in range(1, 4):
290
+ self.voxels.set_solid(min(gx, SX-1), y, min(gz, SZ-1), 3)
291
+ self.maze_goal = vec3(float(min(gx, SX-2)), 1.0, float(min(gz, SZ-2)))
292
+
293
+ def _build_obstacle_course(self):
294
+ SX, _, SZ = self.WORLD_SIZE
295
+ for x in range(SX):
296
+ for z in range(SZ):
297
+ self.voxels.set_solid(x, 0, z, 1)
298
+ rng = np.random.default_rng(self.seed + 1)
299
+ for i in range(1, SX // 8):
300
+ h = int(rng.integers(1, 4))
301
+ for x in range(i*8, i*8+4):
302
+ for z in range(10, SZ-10):
303
+ for y in range(1, h+1):
304
+ self.voxels.set_solid(x, y, z, 1)
305
+ for j in range(4):
306
+ bx = 6 + j * 12
307
+ for z in range(20, 40):
308
+ for y in range(1, 3):
309
+ self.voxels.set_solid(bx, y, z, 1)
310
+
311
+ def _build_military_course(self):
312
+ """
313
+ Vojenská překážková dráha (opičí dráha).
314
+ Agent běží ve směru +X od x=4 do x=60, course centrována na z=32.
315
+
316
+ Překážky:
317
+ x=16-17 Hurdle 1 – nízká zeď h=2, přeskočit
318
+ x=26-27 Hurdle 2 – nízká zeď h=2, přeskočit
319
+ x=35-38 Wall climb – vysoká zeď h=4 s výběžkem, vylézt
320
+ x=39-43 Descent – schody dolů z h=4
321
+ x=46-53 Low crawl – strop na y=2, plazit se
322
+ x=55-57 Jump pit – jáma bez země, přeskočit
323
+ """
324
+ SX, _, SZ = self.WORLD_SIZE
325
+ ZC = SZ // 2 # z=32, středová osa kurzu
326
+ ZH = 6 # půl-šířka kurzu (±6), celkem 12m
327
+ Z0, Z1 = ZC - ZH, ZC + ZH
328
+
329
+ def solid(x0, x1, y0, y1, z0, z1, val=1):
330
+ for x in range(x0, x1 + 1):
331
+ for y in range(y0, y1 + 1):
332
+ for z in range(z0, z1 + 1):
333
+ self.voxels.set_solid(x, y, z, val)
334
+
335
+ # Ground floor everywhere
336
+ solid(0, SX - 1, 0, 0, 0, SZ - 1)
337
+
338
+ # Side channel walls (keep agent on track)
339
+ for x in range(SX):
340
+ for y in range(1, 7):
341
+ self.voxels.set_solid(x, y, Z0 - 1, 1)
342
+ self.voxels.set_solid(x, y, Z1 + 1, 1)
343
+
344
+ # ── HURDLE 1 (x=16-17, h=2) ──────────────────────────────────────────
345
+ solid(16, 17, 1, 2, Z0, Z1)
346
+
347
+ # ── HURDLE 2 (x=26-27, h=2) ──────────────────────────────────────────
348
+ solid(26, 27, 1, 2, Z0, Z1)
349
+
350
+ # ── WALL CLIMB (x=35-38, h=4) ─────────────────────────────────────────
351
+ # Toeholds: ledge on approach side at h=1-2 (x=34)
352
+ solid(34, 34, 1, 2, Z0, Z1)
353
+ # Main wall
354
+ solid(35, 38, 1, 4, Z0, Z1)
355
+ # Top platform to land on (x=38, y=4 — flat top)
356
+ solid(38, 38, 4, 4, Z0, Z1)
357
+
358
+ # ── DESCENT STEPS (x=39-43): from h=4 down to ground ────────────────
359
+ # Each step is 1m wide and 1m lower
360
+ for i, x in enumerate(range(39, 44)):
361
+ h = 4 - i
362
+ if h >= 1:
363
+ solid(x, x, 1, h, Z0, Z1)
364
+
365
+ # ── LOW CRAWL TUNNEL (x=46-53): ceiling at y=2) ──────────────────────
366
+ # Ceiling blocks: agent torso height ~1.8m, must crouch to pass y<2
367
+ for x in range(46, 54):
368
+ for z in range(Z0, Z1 + 1):
369
+ self.voxels.set_solid(x, 2, z, 1)
370
+ self.voxels.set_solid(x, 3, z, 1)
371
+
372
+ # ── JUMP PIT (x=55-57): remove ground ───────────────────────────────
373
+ for x in range(55, 58):
374
+ for z in range(Z0, Z1 + 1):
375
+ self.voxels.set_solid(x, 0, z, 0)
376
+
377
+ # Ensure landing pad after pit
378
+ solid(58, SX - 1, 0, 0, Z0, Z1)
379
+
380
+ def _build_stairs(self):
381
+ SX, _, SZ = self.WORLD_SIZE
382
+ for x in range(SX):
383
+ for z in range(SZ):
384
+ self.voxels.set_solid(x, 0, z, 1)
385
+ for step in range(1, 14):
386
+ for x in range(step * 4, step * 4 + 4):
387
+ for z in range(8, SZ - 8):
388
+ for y in range(1, step + 1):
389
+ self.voxels.set_solid(x, y, z, 1)
390
+
391
+ def _build_household(self):
392
+ SX, SY, SZ = self.WORLD_SIZE
393
+ for x in range(SX):
394
+ for z in range(SZ):
395
+ self.voxels.set_solid(x, 0, z, 1)
396
+
397
+ def _fill(x0, x1, y0, y1, z0, z1, mat=2):
398
+ for x in range(x0, x1+1):
399
+ for y in range(y0, y1+1):
400
+ for z in range(z0, z1+1):
401
+ self.voxels.set_solid(x, y, z, mat)
402
+
403
+ _fill(38, 38, 1, 8, 1, 42)
404
+ _fill(1, 38, 1, 8, 42, 42)
405
+ _fill(10, 14, 1, 2, 14, 18)
406
+ _fill(8, 10, 1, 2, 20, 22)
407
+ _fill(8, 10, 3, 5, 22, 23)
408
+ _fill(2, 4, 1, 7, 8, 22, mat=2)
409
+ for shelf_y in (2, 4, 6):
410
+ _fill(2, 4, shelf_y, shelf_y, 8, 22, mat=2)
411
+ for hollow_y in (1, 3, 5):
412
+ for x in range(3, 4):
413
+ for z in range(9, 22):
414
+ self.voxels.set_solid(x, hollow_y, z, 0)
415
+ _fill(20, 26, 1, 3, 3, 7)
416
+ _fill(22, 24, 4, 6, 3, 4)
417
+ self.voxels.set_solid(23, 4, 5, 3)
418
+
419
+ def _add_boundary_walls(self):
420
+ SX, SY, SZ = self.WORLD_SIZE
421
+ for x in range(SX):
422
+ for y in range(SY):
423
+ self.voxels.set_solid(x, y, 0, 1)
424
+ self.voxels.set_solid(x, y, SZ-1, 1)
425
+ for z in range(SZ):
426
+ for y in range(SY):
427
+ self.voxels.set_solid(0, y, z, 1)
428
+ self.voxels.set_solid(SX-1, y, z, 1)
429
+ if self.terrain_type == "household":
430
+ for x in range(13, 18):
431
+ for y in range(1, 7):
432
+ self.voxels.set_solid(x, y, 0, 0)
433
+
434
+ # ── Interactive objects ───────────────────────────────────────────────────
435
+
436
+ def add_interactive_object(self, obj_id: str, obj_type: str,
437
+ pos: Vec3, size: Vec3 = None,
438
+ mass: float = 0.3) -> InteractiveObject:
439
+ if size is None:
440
+ size = vec3(0.08, 0.12, 0.08)
441
+ self.phys.create_body(
442
+ obj_id, BodyType.DYNAMIC,
443
+ position=pos, size=size,
444
+ mass=mass, restitution=0.3, friction=0.6,
445
+ )
446
+ obj = InteractiveObject(
447
+ id=obj_id, obj_type=obj_type,
448
+ initial_pos=vec3(float(pos[0]), float(pos[1]), float(pos[2])),
449
+ body_id=obj_id,
450
+ )
451
+ self.interactive_objects[obj_id] = obj
452
+ return obj
453
+
454
+ # ── Fracture system ───────────────────────────────────────────────────────
455
+
456
+ def _process_fractures(self):
457
+ """Check accumulated impact speeds; fracture objects that exceed yield."""
458
+ for obj_id, obj in list(self.interactive_objects.items()):
459
+ speed = obj._pending_impact_speed
460
+ obj._pending_impact_speed = 0.0
461
+
462
+ if obj.is_broken or speed == 0.0:
463
+ continue
464
+
465
+ mat = MATERIAL_PROPS.get(obj.obj_type)
466
+ if mat is None or speed < mat.yield_velocity:
467
+ continue
468
+
469
+ # Compute stress via FractureBridge
470
+ body = self.phys.bodies.get(obj.body_id)
471
+ if body is None:
472
+ continue
473
+ restitution = body.restitution
474
+ risk = _impact_stress(mat, speed, body.mass, restitution)
475
+
476
+ if risk >= mat.fracture_threshold:
477
+ self._fracture_object(obj, body, obj._pending_impact_vel, mat)
478
+
479
+ def _fracture_object(self, obj: InteractiveObject, body,
480
+ impact_vel, mat: MaterialProps):
481
+ """Replace object with N fragments flying in random directions."""
482
+ pos = body.position.copy() if hasattr(body.position, 'copy') else vec3(*body.position)
483
+ half = body.aabb_local.max # half-size
484
+ rng = np.random.default_rng(int(abs(hash(obj.id))) % (2**31))
485
+
486
+ frag_ids = []
487
+ for i in range(mat.n_fragments):
488
+ fid = f"{obj.id}_frag_{i}"
489
+ foff = vec3(
490
+ float(rng.uniform(-float(half[0]), float(half[0]))),
491
+ float(rng.uniform(0.0, float(half[1]))),
492
+ float(rng.uniform(-float(half[2]), float(half[2]))),
493
+ )
494
+ fsz = vec3(
495
+ float(half[0]) * mat.fragment_size_frac * float(rng.uniform(0.6, 1.0)),
496
+ float(half[1]) * mat.fragment_size_frac * float(rng.uniform(0.6, 1.0)),
497
+ float(half[2]) * mat.fragment_size_frac * float(rng.uniform(0.6, 1.0)),
498
+ )
499
+ fmass = body.mass * mat.fragment_size_frac ** 3 * 8.0
500
+
501
+ frag = self.phys.create_body(
502
+ fid, BodyType.DYNAMIC,
503
+ position=pos + foff,
504
+ size=fsz,
505
+ mass=max(0.01, fmass),
506
+ restitution=0.12,
507
+ friction=0.85,
508
+ )
509
+ iv = impact_vel if impact_vel is not None else vec3()
510
+ frag.velocity = vec3(
511
+ float(iv[0]) * 0.4 + float(rng.uniform(-3.0, 3.0)),
512
+ float(abs(iv[1])) * 0.3 + float(rng.uniform(0.5, 2.5)),
513
+ float(iv[2]) * 0.4 + float(rng.uniform(-3.0, 3.0)),
514
+ )
515
+ frag_ids.append(fid)
516
+
517
+ # Deactivate original body — mark as static so physics ignores it
518
+ body.body_type = BodyType.STATIC
519
+ body.velocity = vec3()
520
+
521
+ obj.is_broken = True
522
+ obj.fragment_ids = frag_ids
523
+
524
+ self._fracture_log.append({
525
+ "tick": self._tick,
526
+ "object": obj.id,
527
+ "material": mat.name,
528
+ "impact_speed": float(body.restitution),
529
+ "n_fragments": mat.n_fragments,
530
+ })
531
+
532
+ # ── Grasp ─────────────────────────────────────────────────────────────────
533
+
534
+ def check_grasp(self, hand_id: str, hand_pos: Vec3, grip: float,
535
+ grasp_radius: float = 0.35):
536
+ if grip > 0.25:
537
+ self.release(hand_id)
538
+ return
539
+ if grip < -0.25 and hand_id not in self._grasped:
540
+ for obj in self.interactive_objects.values():
541
+ if not obj.is_grasped and not obj.is_broken:
542
+ body = self.phys.bodies.get(obj.body_id)
543
+ if body is None:
544
+ continue
545
+ d = float(length(hand_pos - body.position))
546
+ if d < grasp_radius:
547
+ obj.is_grasped = True
548
+ obj.held_by = hand_id
549
+ self._grasped[hand_id] = obj.id
550
+ return
551
+
552
+ def release(self, hand_id: str):
553
+ if hand_id in self._grasped:
554
+ obj = self.interactive_objects[self._grasped[hand_id]]
555
+ obj.is_grasped = False
556
+ obj.held_by = ""
557
+ del self._grasped[hand_id]
558
+
559
+ def update_grasped(self, body_positions: Dict[str, Vec3],
560
+ body_velocities: Dict[str, Vec3]):
561
+ for hand_id, obj_id in self._grasped.items():
562
+ obj = self.interactive_objects[obj_id]
563
+ pbody = self.phys.bodies.get(obj.body_id)
564
+ if pbody is None or obj.is_broken:
565
+ continue
566
+ h_pos = body_positions[hand_id]
567
+ h_vel = body_velocities.get(hand_id, vec3())
568
+ pbody.position = h_pos + vec3(0, -0.08, 0.05)
569
+ pbody.velocity = vec3(float(h_vel[0]), float(h_vel[1]), float(h_vel[2]))
570
+
571
+ def get_object_pos(self, obj_id: str) -> Vec3:
572
+ body = self.phys.bodies.get(self.interactive_objects[obj_id].body_id)
573
+ return body.position if body else vec3()
574
+
575
+ def is_grasped(self, obj_id: str) -> bool:
576
+ obj = self.interactive_objects.get(obj_id)
577
+ return bool(obj and obj.is_grasped)
578
+
579
+ def is_broken(self, obj_id: str) -> bool:
580
+ obj = self.interactive_objects.get(obj_id)
581
+ return bool(obj and obj.is_broken)
582
+
583
+ # ── Physics step ──────────────────────────────────────────────────────────
584
+
585
+ def step(self, dt: float = 0.016) -> List:
586
+ self._tick += 1
587
+ manifolds = self.phys.step(dt)
588
+
589
+ # Anti-tunneling ground clamp + impact detection for interactive objects
590
+ from engine.physics.rigid_body import BodyType as BT
591
+ for body in self.phys.bodies.values():
592
+ if body.body_type == BT.STATIC or body.mass <= 0:
593
+ continue
594
+ h = body.aabb_local.max[1]
595
+ floor_y = self.get_ground_height(
596
+ float(body.position[0]), float(body.position[2]))
597
+ if float(body.position[1]) - h < floor_y:
598
+ impact_speed = abs(float(body.velocity[1]))
599
+ # Record impact for interactive objects (voxel surface contact)
600
+ bid = body.body_id
601
+ if bid in self.interactive_objects:
602
+ obj = self.interactive_objects[bid]
603
+ if impact_speed > obj._pending_impact_speed:
604
+ obj._pending_impact_speed = impact_speed
605
+ obj._pending_impact_vel = body.velocity
606
+ body.position[1] = floor_y + h
607
+ if float(body.velocity[1]) < 0:
608
+ body.velocity[1] = abs(body.velocity[1]) * body.restitution
609
+ body.is_grounded = True
610
+
611
+ # Also record impacts from AABB body-body manifolds
612
+ for m in manifolds:
613
+ speed = float(length(m.relative_velocity))
614
+ for body in (m.body_a, m.body_b):
615
+ bid = body.body_id
616
+ if bid in self.interactive_objects:
617
+ obj = self.interactive_objects[bid]
618
+ if speed > obj._pending_impact_speed:
619
+ obj._pending_impact_speed = speed
620
+ obj._pending_impact_vel = m.relative_velocity
621
+
622
+ # Fracture check — runs after every physics step
623
+ self._process_fractures()
624
+ return manifolds
625
+
626
+ def get_ground_height(self, x: float, z: float) -> float:
627
+ ix, iz = int(x), int(z)
628
+ SX, SY, SZ = self.WORLD_SIZE
629
+ ix = max(1, min(SX-2, ix))
630
+ iz = max(1, min(SZ-2, iz))
631
+ for y in range(SY - 1, -1, -1):
632
+ if self.voxels.is_solid(ix, y, iz):
633
+ return float(y + 1)
634
+ return 0.0
635
+
636
+ def is_solid_at(self, x: float, y: float, z: float) -> bool:
637
+ return self.voxels.is_solid(int(x), int(y), int(z))
638
+
639
+ def reset(self):
640
+ self._tick = 0
641
+ self._grasped.clear()
642
+ self._fracture_log.clear()
643
+ for obj in self.interactive_objects.values():
644
+ obj.is_grasped = False
645
+ obj.held_by = ""
646
+ obj.is_broken = False
647
+ obj.fragment_ids = []
648
+ obj._pending_impact_speed = 0.0
649
+ obj._pending_impact_vel = None
650
+ self.phys = PhysicsWorld(gravity=vec3(0, -9.81, 0), substeps=3)
651
+ self._register_collision_callback()
embodied/pretrain/__init__.py ADDED
File without changes
embodied/pretrain/bc_driving.py ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BC předtrénování pro DriveCity — z reálných řidičských dat.
3
+
4
+ Datasety (dle průzkumu, best-to-use):
5
+ 1. CARLA Autopilot — huggingface: immanuelpeter/carla-autopilot-multimodal-dataset
6
+ Plně normalizované akce: throttle, steer, brake
7
+ 2. L2D (Learning to Drive) — yaak-ai/L2D
8
+ Největší: 19M+ snímků, 5000+ hodin, 10 Hz
9
+ steering(norm), gas(0-1), brake(0-1)
10
+
11
+ Mapování na agent 10D:
12
+ action[0] = throttle - brake (dopředu/dozadu)
13
+ action[2] = steer (zatáčení)
14
+ action[1,3..9] = 0 (nevyužité pro řízení)
15
+
16
+ Spuštění:
17
+ python -m embodied.pretrain.bc_driving --dataset carla --steps 12000
18
+ python -m embodied.pretrain.bc_driving --dataset l2d --steps 20000
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ import os
24
+ import sys
25
+ import time
26
+ import glob
27
+ from pathlib import Path
28
+ from typing import Tuple
29
+
30
+ import numpy as np
31
+
32
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
33
+
34
+ from cogcore.cognition.motor import MotorActionHead, MotorActionConfig
35
+ from embodied.rl_loop import RLConfig, PhantomAgent
36
+ from embodied.pretrain.bc_pretrain import _save_agent_weights
37
+ from embodied.goal_encoder import encode_goal, goal_for_task
38
+ from embodied.phantom_cortex import CORTEX_DIM, BODY_DIM
39
+ from embodied.tasks.drive_city import DriveCity
40
+
41
+ log = logging.getLogger("phantom.bc_driving")
42
+ logging.basicConfig(level=logging.INFO,
43
+ format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
44
+ datefmt="%H:%M:%S")
45
+
46
+
47
+ # ── Stav vozidla pro DriveCity (inicializovat s nulovou rychlostí) ────────────
48
+
49
+ def _make_drive_state(speed_norm: float = 0.0,
50
+ steer: float = 0.0,
51
+ throttle: float = 0.0) -> np.ndarray:
52
+ """
53
+ Vytvoří 73D state vektor odpovídající DriveCity.get_task_state():
54
+ state[0] = speed (normalizovaná 0–1)
55
+ state[2] = steer (−1..+1)
56
+ state[3] = throttle (0..1)
57
+ state[18:26] = city_obs (nuly — bez mapy)
58
+ state[36:68] = goal_vec (drive_city goal)
59
+ """
60
+ state = np.zeros(CORTEX_DIM, dtype=np.float32)
61
+ state[0] = speed_norm
62
+ state[2] = steer
63
+ state[3] = throttle
64
+ goal_vec = encode_goal(goal_for_task("drive_city"))
65
+ state[BODY_DIM + 8: BODY_DIM + 8 + 32] = goal_vec
66
+ return state
67
+
68
+
69
+ def _make_drive_action(throttle: float, brake: float, steer: float) -> np.ndarray:
70
+ """Normalizovaná 10D akce pro řízení."""
71
+ a = np.zeros(10, dtype=np.float32)
72
+ a[0] = float(np.clip(throttle - brake, -1.0, 1.0)) # gas/brake na ose 0
73
+ a[2] = float(np.clip(steer, -1.0, 1.0)) # steer
74
+ return a
75
+
76
+
77
+ # ── Syntetická data z vlastní simulace (fallback) ────────────────────────────
78
+
79
+ def _generate_synthetic_driving_data(n_samples: int = 15000,
80
+ seed: int = 42) -> Tuple[np.ndarray, np.ndarray]:
81
+ """
82
+ Generuje syntetická řidičská data z DriveCity simulace.
83
+ Expert policy: sleduj waypoint, brzdí u semaforů, zpomaluj u retardérů.
84
+
85
+ Používá se jako fallback pokud není dostupný HuggingFace dataset.
86
+ """
87
+ rng = np.random.default_rng(seed)
88
+ states, actions = [], []
89
+
90
+ # Simulujeme různé scénáře
91
+ scenarios = [
92
+ # (speed_kmh, target_speed, steer_variance, throttle_base)
93
+ (80, 80, 0.05, 0.65), # dálnice — rovně
94
+ (50, 50, 0.12, 0.55), # centrum — mírné zatáčení
95
+ (30, 30, 0.20, 0.40), # residential — pomalé, více zatáček
96
+ (130, 130, 0.02, 0.85), # D1 — max rychlost, minimum zatáček
97
+ (0, 50, 0.0, 0.80), # rozjezd
98
+ (80, 0, 0.0, 0.0), # brzdění
99
+ (60, 70, 0.08, 0.60), # boulevard
100
+ (70, 30, 0.10, 0.0), # zpomalení před křižovatkou
101
+ ]
102
+
103
+ per_scenario = n_samples // len(scenarios)
104
+ for speed, target, steer_var, thr_base in scenarios:
105
+ for _ in range(per_scenario):
106
+ speed_norm = speed / 140.0
107
+ steer = float(rng.normal(0.0, steer_var))
108
+ steer = np.clip(steer, -1.0, 1.0)
109
+
110
+ # Throttle: přiblíž se k target rychlosti
111
+ speed_err = (target - speed) / 140.0
112
+ throttle = float(np.clip(thr_base + speed_err * 0.5
113
+ + rng.normal(0.0, 0.05), 0.0, 1.0))
114
+ brake = float(np.clip(-speed_err * 0.6
115
+ + rng.normal(0.0, 0.02), 0.0, 1.0))
116
+ if throttle > 0.1:
117
+ brake = 0.0
118
+
119
+ s = _make_drive_state(speed_norm, steer, throttle)
120
+ # Drobné perturbace v city obs části
121
+ s[18:26] = rng.uniform(0, 1, 8).astype(np.float32) * 0.3
122
+ a = _make_drive_action(throttle, brake, steer)
123
+ states.append(s)
124
+ actions.append(a)
125
+
126
+ # Shuffle
127
+ idx = rng.permutation(len(states))
128
+ return (np.array(states, dtype=np.float32)[idx],
129
+ np.array(actions, dtype=np.float32)[idx])
130
+
131
+
132
+ # ── Loader pro L2D dataset (yaak-ai/L2D) ─────────────────────────────────────
133
+
134
+ # Mapování road_type → kategorické číslo (pro state obohacení)
135
+ _L2D_ROAD_MAP = {
136
+ "motorway": 1.0, "trunk": 0.9, "primary": 0.7,
137
+ "secondary": 0.55, "tertiary": 0.4, "residential": 0.25,
138
+ "service": 0.15, "unclassified": 0.3,
139
+ }
140
+
141
+
142
+ def _download_l2d_files(n_files: int = 2, cache_dir: str = "/tmp/l2d_cache") -> list:
143
+ """
144
+ Stáhne prvních n_files parquet souborů z yaak-ai/L2D (chunk-000).
145
+ Každý soubor = ~57 MB, ~1.1M řádků skutečné jízdy z Německa.
146
+ """
147
+ from huggingface_hub import hf_hub_download
148
+ os.makedirs(cache_dir, exist_ok=True)
149
+ paths = []
150
+ for i in range(n_files):
151
+ fname = f"data/chunk-000/file-{i:03d}.parquet"
152
+ local = os.path.join(cache_dir, f"l2d_chunk000_file{i:03d}.parquet")
153
+ if os.path.exists(local):
154
+ log.info("L2D soubor %d už existuje v cache (%s)", i, local)
155
+ paths.append(local)
156
+ continue
157
+ log.info("Stahuji L2D %s ...", fname)
158
+ path = hf_hub_download("yaak-ai/L2D", fname, repo_type="dataset",
159
+ local_dir=cache_dir)
160
+ paths.append(path)
161
+ return paths
162
+
163
+
164
+ def _load_l2d_dataset(
165
+ data_dir: str = "/tmp/l2d_cache",
166
+ n_samples: int = 80000,
167
+ road_filter: Optional[str] = None,
168
+ download: bool = True,
169
+ n_files: int = 1,
170
+ ) -> Tuple[np.ndarray, np.ndarray]:
171
+ """
172
+ Načte reálná řidičská data z L2D (yaak-ai/L2D, Apache 2.0).
173
+
174
+ Schema parquet:
175
+ action.continuous [3]: [gas_pedal_norm, brake_pedal_norm, steering_angle_norm]
176
+ observation.state.vehicle [8]: [speed_kmh, heading, heading_error, lat, lon, alt, acc_x, acc_y]
177
+ observation.state.road: "motorway" | "primary" | "residential" | ...
178
+ observation.state.max_speed: rychlostní limit (string, např. "50.0")
179
+
180
+ Args:
181
+ road_filter : 'highway' → motorway+trunk, 'city' → residential+tertiary, None → vše
182
+ download : stáhnout z HuggingFace pokud nejsou lokální soubory
183
+ n_files : počet parquet souborů ke stažení (1 soubor = ~1.1M řádků)
184
+ """
185
+ try:
186
+ import pandas as pd
187
+ except ImportError:
188
+ raise ImportError("pandas vyžadován pro L2D loader: pip install pandas pyarrow")
189
+
190
+ # Najdi nebo stáhni parquet soubory
191
+ parquet_files = sorted(glob.glob(os.path.join(data_dir, "l2d_*.parquet")))
192
+ if not parquet_files:
193
+ parquet_files = sorted(glob.glob(os.path.join(data_dir, "**/*.parquet"), recursive=True))
194
+ if not parquet_files and download:
195
+ parquet_files = _download_l2d_files(n_files=n_files, cache_dir=data_dir)
196
+ if not parquet_files:
197
+ raise FileNotFoundError(f"Žádné L2D parquet soubory v {data_dir}. Použij download=True.")
198
+
199
+ parquet_files = parquet_files[:n_files]
200
+
201
+ all_states, all_actions = [], []
202
+ for fpath in parquet_files:
203
+ log.info("Načítám %s ...", fpath)
204
+ df = pd.read_parquet(fpath, columns=[
205
+ "action.continuous",
206
+ "observation.state.vehicle",
207
+ "observation.state.road",
208
+ "observation.state.max_speed",
209
+ ])
210
+
211
+ # Filtr podle typu silnice
212
+ if road_filter == "highway":
213
+ mask = df["observation.state.road"].isin(["motorway", "trunk"])
214
+ df = df[mask]
215
+ elif road_filter == "city":
216
+ mask = df["observation.state.road"].isin(["residential", "tertiary", "service"])
217
+ df = df[mask]
218
+
219
+ if len(df) == 0:
220
+ log.warning("Po filtraci road_filter=%r: 0 řádků v %s", road_filter, fpath)
221
+ continue
222
+
223
+ # Subsample na n_samples // n_files
224
+ per_file = n_samples // len(parquet_files)
225
+ if len(df) > per_file:
226
+ df = df.sample(n=per_file, random_state=42)
227
+
228
+ # Vektorizované zpracování — rychlejší než iterrows
229
+ acts = np.stack(df["action.continuous"].values).astype(np.float32) # (N, 3)
230
+ vehs = np.stack(df["observation.state.vehicle"].values).astype(np.float32) # (N, 8)
231
+
232
+ gas = np.clip(acts[:, 0], 0.0, 1.0)
233
+ brake = np.clip(acts[:, 1], 0.0, 1.0)
234
+ steer = np.clip(acts[:, 2], -1.0, 1.0)
235
+ speed_norm = np.clip(vehs[:, 0] / 140.0, 0.0, 1.5)
236
+
237
+ # Rychlostní limit → normalizuj
238
+ def _parse_limit(s):
239
+ try:
240
+ v = float(s)
241
+ return v if v > 0 else 50.0
242
+ except (ValueError, TypeError):
243
+ return 50.0
244
+ limits = np.array([_parse_limit(x) for x in df["observation.state.max_speed"].values],
245
+ dtype=np.float32)
246
+ limit_norm = np.clip(limits / 130.0, 0.0, 1.0)
247
+
248
+ # Road type encoding
249
+ road_norms = np.array([_L2D_ROAD_MAP.get(r, 0.3)
250
+ for r in df["observation.state.road"].values], dtype=np.float32)
251
+
252
+ N = len(df)
253
+ batch_states = np.zeros((N, CORTEX_DIM), dtype=np.float32)
254
+ batch_actions = np.zeros((N, 10), dtype=np.float32)
255
+
256
+ goal_vec = encode_goal(goal_for_task("drive_city"))
257
+ batch_states[:, BODY_DIM + 8: BODY_DIM + 8 + 32] = goal_vec
258
+
259
+ batch_states[:, 0] = speed_norm
260
+ batch_states[:, 2] = steer
261
+ batch_states[:, 3] = gas
262
+ batch_states[:, 22] = road_norms
263
+ batch_states[:, 23] = limit_norm
264
+
265
+ batch_actions[:, 0] = np.clip(gas - brake, -1.0, 1.0)
266
+ batch_actions[:, 2] = steer
267
+
268
+ all_states.append(batch_states)
269
+ all_actions.append(batch_actions)
270
+
271
+ if not all_states:
272
+ raise ValueError("L2D loader: nepodařilo se načíst žádná data.")
273
+
274
+ states = np.concatenate(all_states, axis=0)
275
+ actions = np.concatenate(all_actions, axis=0)
276
+
277
+ # Shuffle
278
+ rng = np.random.default_rng(42)
279
+ idx = rng.permutation(len(states))
280
+ log.info("L2D dataset: %d vzorků (road_filter=%r)", len(states), road_filter)
281
+ return states[idx], actions[idx]
282
+
283
+
284
+ # ── Loader pro CARLA Autopilot dataset ───────────────────────────────────────
285
+
286
+ def _load_carla_dataset(data_dir: str,
287
+ max_episodes: int = 200) -> Tuple[np.ndarray, np.ndarray]:
288
+ """
289
+ Načte CARLA Autopilot epizody (immanuelpeter/carla-autopilot-multimodal-dataset).
290
+
291
+ Formát: každý epizoda = NPZ soubor s klíči:
292
+ 'throttle' (T,), 'brake' (T,), 'steer' (T,), 'speed' (T,)
293
+ nebo CSV/JSON s control array.
294
+ """
295
+ states, actions = [], []
296
+
297
+ # Zkus NPZ formát
298
+ npz_files = sorted(glob.glob(os.path.join(data_dir, "**/*.npz"), recursive=True))
299
+ npz_files += sorted(glob.glob(os.path.join(data_dir, "*.npz")))
300
+ npz_files = list(dict.fromkeys(npz_files))[:max_episodes]
301
+
302
+ for fpath in npz_files:
303
+ try:
304
+ d = np.load(fpath, allow_pickle=True)
305
+ keys = list(d.keys())
306
+ throttle = d.get("throttle", d.get("gas", np.zeros(1)))
307
+ brake = d.get("brake", np.zeros(len(throttle)))
308
+ steer = d.get("steer", d.get("steering", np.zeros(len(throttle))))
309
+ speed = d.get("speed", np.zeros(len(throttle)))
310
+ T = min(len(throttle), len(brake), len(steer), len(speed))
311
+ for t in range(T):
312
+ s = _make_drive_state(
313
+ speed_norm=float(speed[t]) / 140.0,
314
+ steer=float(steer[t]),
315
+ throttle=float(throttle[t]),
316
+ )
317
+ a = _make_drive_action(float(throttle[t]), float(brake[t]), float(steer[t]))
318
+ states.append(s)
319
+ actions.append(a)
320
+ except Exception as e:
321
+ log.debug("Nelze načíst %s: %s", fpath, e)
322
+
323
+ if not states:
324
+ raise FileNotFoundError(f"Žádná CARLA data v {data_dir}")
325
+ return np.array(states, dtype=np.float32), np.array(actions, dtype=np.float32)
326
+
327
+
328
+ # ── Hlavní BC funkce ──────────────────────────────────────────────────────────
329
+
330
+ def run_driving_bc_pretraining(
331
+ dataset: str = "synthetic",
332
+ data_dir: str = "/tmp/carla_driving",
333
+ steps: int = 12000,
334
+ batch_size: int = 64,
335
+ lr: float = 0.002,
336
+ save_path: str = "checkpoints/bc_driving.npz",
337
+ log_every: int = 1000,
338
+ road_filter: Optional[str] = None,
339
+ n_files: int = 1,
340
+ ) -> PhantomAgent:
341
+ """
342
+ BC předtrénování pro DriveCity.
343
+
344
+ Args:
345
+ dataset : 'synthetic' | 'carla' | 'l2d'
346
+ data_dir : cesta k datasetu (pro carla/l2d)
347
+ steps : počet BC kroků
348
+ road_filter : (L2D) 'highway' | 'city' | None → filtruj typ silnice
349
+ n_files : (L2D) počet parquet souborů ke stažení (1 = ~1.1M řádků, 57 MB)
350
+ """
351
+ rng = np.random.default_rng(42)
352
+
353
+ if dataset == "synthetic":
354
+ log.info("Generuji syntetická řidičská data (15 000 vzorků)...")
355
+ states, actions = _generate_synthetic_driving_data(n_samples=15000)
356
+ elif dataset == "carla":
357
+ log.info("Načítám CARLA Autopilot data z %s ...", data_dir)
358
+ states, actions = _load_carla_dataset(data_dir)
359
+ elif dataset == "l2d":
360
+ log.info("Načítám L2D dataset (yaak-ai/L2D) road_filter=%r n_files=%d ...",
361
+ road_filter, n_files)
362
+ states, actions = _load_l2d_dataset(
363
+ data_dir=data_dir, n_samples=80000,
364
+ road_filter=road_filter, download=True, n_files=n_files,
365
+ )
366
+ else:
367
+ raise ValueError(f"Neznámý dataset: {dataset}. Použij 'synthetic', 'carla' nebo 'l2d'.")
368
+
369
+ N = len(states)
370
+ log.info("Dataset: %d vzorků state=%s action=%s", N, states.shape, actions.shape)
371
+ log.info("Action range: throttle~[%.2f,%.2f] steer~[%.2f,%.2f]",
372
+ actions[:, 0].min(), actions[:, 0].max(),
373
+ actions[:, 2].min(), actions[:, 2].max())
374
+
375
+ cfg = RLConfig(lr=lr)
376
+ agent = PhantomAgent(cfg)
377
+
378
+ losses = []
379
+ t0 = time.time()
380
+ for step in range(steps):
381
+ idx = rng.integers(0, N, size=batch_size)
382
+ loss = agent.head.train_supervised_step(states[idx], actions[idx])
383
+ losses.append(loss)
384
+ if (step + 1) % log_every == 0:
385
+ log.info("BC-driving step %5d/%d loss=%.4f (%.1f s)",
386
+ step + 1, steps, float(np.mean(losses[-log_every:])), time.time() - t0)
387
+
388
+ _save_agent_weights(agent, save_path)
389
+ log.info("BC-driving hotovo → %s final_loss=%.4f",
390
+ save_path, float(np.mean(losses[-500:])))
391
+ return agent
392
+
393
+
394
+ # ── CLI ───────────────────────────────────────────────────────────────────────
395
+
396
+ if __name__ == "__main__":
397
+ import argparse
398
+ p = argparse.ArgumentParser()
399
+ p.add_argument("--dataset", default="synthetic",
400
+ choices=["synthetic", "carla", "l2d"])
401
+ p.add_argument("--data-dir", default="/tmp/l2d_cache")
402
+ p.add_argument("--steps", type=int, default=12000)
403
+ p.add_argument("--lr", type=float, default=0.002)
404
+ p.add_argument("--save", default="checkpoints/bc_driving.npz")
405
+ p.add_argument("--log-every", type=int, default=1000)
406
+ p.add_argument("--road-filter", default=None, choices=["highway", "city", None],
407
+ help="L2D: filtruj typ silnice (highway=motorway/trunk, city=residential)")
408
+ p.add_argument("--n-files", type=int, default=1,
409
+ help="L2D: počet parquet souborů ke stažení (1 soubor = ~57 MB, ~1.1M řádků)")
410
+ args = p.parse_args()
411
+ run_driving_bc_pretraining(
412
+ dataset=args.dataset, data_dir=args.data_dir,
413
+ steps=args.steps, lr=args.lr,
414
+ save_path=args.save, log_every=args.log_every,
415
+ road_filter=args.road_filter, n_files=args.n_files,
416
+ )
embodied/pretrain/bc_ice_driving.py ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BC předtrénování pro IceDriving — syntetický expert policy.
3
+
4
+ Protože žádný WRC výrobce (Subaru/Mitsubishi/Suzuki) ani veřejná komunita
5
+ nezveřejnili telemetrická data ze sněhu/ledu, generujeme expertní demonstrace
6
+ přímo z naší IceDrivingTask fyziky.
7
+
8
+ Expert policy principy (Scandinavian/rally styl):
9
+ 1. Anticipační brzdění — začíná dřív než na suchém asfaltu (mu×7× delší dráha)
10
+ 2. Plynulé vstupy — na ledu skoky způsobí blokování kol
11
+ 3. Řízený drift (drift_index 0.15–0.55) — Scandinavian flick před zatáčkou
12
+ 4. Regen brzdění — EM motory jako generátory, bez lockup
13
+ 5. Throttle na výjezdu ze zatáčky — AWD trakce ven ze zatáčky
14
+
15
+ State layout (IceDrivingTask, relevantní části):
16
+ state[18] = dist_norm (vzdálenost k waypointu / 800)
17
+ state[19] = ang_err / pi (−1..+1, negativní = vlevo)
18
+ state[20] = mu_factor (0.07 black_ice .. 1.0 dry)
19
+ state[21] = drift_index (0..1)
20
+ state[22] = speed_norm (speed_kmh / 140)
21
+ state[23] = regen_kw_norm (0..1)
22
+ state[24] = prev_steer (−1..+1)
23
+ state[25] = prev_throttle (0..1)
24
+ state[26] = slip_ratio_front
25
+ state[27] = black_ice_nearby (0/1)
26
+
27
+ Výstup akce (10D):
28
+ action[0] = throttle - brake (+1 = plný plyn, −1 = plná regen-brzda)
29
+ action[2] = steer (−1 = vlevo, +1 = vpravo)
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import logging
34
+ import math
35
+ import os
36
+ import sys
37
+ import time
38
+ from pathlib import Path
39
+ from typing import Tuple
40
+
41
+ import numpy as np
42
+
43
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
44
+
45
+ from embodied.rl_loop import RLConfig, PhantomAgent
46
+ from embodied.pretrain.bc_pretrain import _save_agent_weights
47
+ from embodied.goal_encoder import encode_goal, goal_for_task
48
+ from embodied.phantom_cortex import CORTEX_DIM, BODY_DIM
49
+
50
+ log = logging.getLogger("phantom.bc_ice")
51
+ logging.basicConfig(level=logging.INFO,
52
+ format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
53
+ datefmt="%H:%M:%S")
54
+
55
+
56
+ # ── Expert policy ─────────────────────────────────────────────────────────────
57
+
58
+ def _expert_action(state: np.ndarray,
59
+ rng: np.random.Generator,
60
+ noise: float = 0.04) -> np.ndarray:
61
+ """
62
+ Heuristický expert pro jízdu na ledu/sněhu.
63
+
64
+ Vstup: 73D state z IceDrivingTask.get_task_state()
65
+ Výstup: 10D action
66
+ """
67
+ dist_norm = float(state[18]) # 0-1
68
+ ang_err = float(state[19]) # -1..+1 (/ pi)
69
+ mu = float(state[20]) # 0.07-1.0
70
+ drift_idx = float(state[21]) # 0-1
71
+ speed = float(state[22]) # 0-1 (/ 140 km/h)
72
+ prev_steer = float(state[24])
73
+ prev_thr = float(state[25])
74
+ black_ice = float(state[27])
75
+
76
+ # ── Cílová rychlost ──────────────────────────────────────────────────────
77
+ # Na ledu (mu=0.14) → max cca 45 km/h v zatáčce, 80 na rovince
78
+ # Na sněhu (mu=0.28) → max cca 70 km/h v zatáčce, 100 na rovince
79
+ corner_sharpness = min(1.0, abs(ang_err) * 1.8) # 0 = rovinka, 1 = vlásenka
80
+ # Základní target normalizovaná rychlost
81
+ target_straight = min(0.6, mu * 3.2) # ice: 0.45, snow: 0.90→0.6
82
+ target_corner = min(0.35, mu * 2.0) # ice: 0.28, snow: 0.56→0.35
83
+ target_speed_norm = target_straight * (1.0 - 0.6 * corner_sharpness) + \
84
+ target_corner * 0.6 * corner_sharpness
85
+
86
+ if black_ice > 0.5:
87
+ target_speed_norm *= 0.55 # ještě opatrněji na černém ledu
88
+
89
+ # Anticipační brzdění: začni brzdit výrazně před zatáčkou
90
+ # dist_norm ~ 0.0 znamená jsme blízko waypointu → zatáčka přichází
91
+ anticipate = max(0.0, 0.4 - dist_norm) * corner_sharpness
92
+ target_speed_norm *= max(0.5, 1.0 - anticipate * 2.5)
93
+
94
+ # ── Throttle / Regen-brzda ────────────────────────────────────────────────
95
+ speed_err = target_speed_norm - speed
96
+
97
+ if drift_idx > 0.65:
98
+ # Spin-out riziko → seber plyn, jemně kontr-zatočení
99
+ thr_brake = min(0.05, speed_err * 0.5)
100
+ elif speed_err > 0.04:
101
+ # Potřebujeme zrychlit
102
+ thr_brake = min(0.85, speed_err * 2.5 + 0.05)
103
+ elif speed_err < -0.04:
104
+ # Regen-brzdění (záporná hodnota = EM generátory)
105
+ # Omez brzdění pokud zároveň zatáčíme (late_brake penalty)
106
+ brake_limit = 0.7 if abs(ang_err) < 0.2 else 0.4
107
+ thr_brake = max(-brake_limit, speed_err * 2.8)
108
+ else:
109
+ thr_brake = speed_err * 0.8
110
+
111
+ # Jemné uvolnění plynu při přiblížení k zatáčce (engine braking)
112
+ if corner_sharpness > 0.3 and thr_brake > 0.0:
113
+ thr_brake *= max(0.3, 1.0 - corner_sharpness)
114
+
115
+ # ── Řízení ───────────────────────────────────────────────────────────────
116
+ # Na ledu: KONZERVATIVNÍ gain — malé vstupy, ledová plocha zesiluje efekt
117
+ steer_gain = 0.45 if mu < 0.20 else 0.60 # ice=0.45, snow=0.60
118
+ raw_steer = ang_err * steer_gain
119
+
120
+ # Spin-out recovery: pokud drift > 0.65, kontr-zatáčení a uvolnění
121
+ if drift_idx > 0.65:
122
+ raw_steer = -math.copysign(min(0.15, drift_idx * 0.2), prev_steer)
123
+
124
+ # Scandinavian flick: jen při plynulé jízdě (drift nízký, těsná zatáčka)
125
+ elif (dist_norm > 0.20 and corner_sharpness > 0.55 and drift_idx < 0.08
126
+ and abs(prev_steer) < 0.15):
127
+ flick = -math.copysign(0.10, ang_err) # jemný protisměrný impuls
128
+ raw_steer += flick
129
+
130
+ raw_steer = float(np.clip(raw_steer, -1.0, 1.0))
131
+
132
+ # LP filtr na řízení → plynulost vstupů (kritická na ledu)
133
+ alpha_steer = 0.25 # 25% nový signál, 75% stará hodnota = velmi hladké
134
+ steer = alpha_steer * raw_steer + (1.0 - alpha_steer) * prev_steer
135
+
136
+ # LP filtr na throttle
137
+ alpha_thr = 0.25
138
+ thr_brake_smooth = alpha_thr * thr_brake + (1.0 - alpha_thr) * (
139
+ prev_thr if thr_brake >= 0 else -prev_thr)
140
+
141
+ # Výstupní akce
142
+ action = np.zeros(10, dtype=np.float32)
143
+ action[0] = float(np.clip(thr_brake_smooth, -1.0, 1.0))
144
+ action[2] = float(np.clip(steer, -1.0, 1.0))
145
+
146
+ # Malý šum pro rozmanitost datasetu
147
+ if noise > 0:
148
+ action[0] += float(rng.normal(0, noise * 0.6))
149
+ action[2] += float(rng.normal(0, noise))
150
+ action = np.clip(action, -1.0, 1.0)
151
+
152
+ return action.astype(np.float32)
153
+
154
+
155
+ # ── Generátor syntetických demonstrací ────────────────────────────────────────
156
+
157
+ # Trasa _ICE_TRACK − normalizovaná pro přímé generování stavů
158
+ _TRACK_WAYPOINTS = [
159
+ (400, 0), # T1 brzdění
160
+ (600, 200), # T2 vlásenka
161
+ (600, 500), # T3 rychlá
162
+ (400, 700), # T4 chicane a
163
+ (200, 700), # T4 chicane b
164
+ ( 0, 500), # T5 dlouhá
165
+ (-200, 200), # T6 kink
166
+ ( 0, 0), # cíl
167
+ ]
168
+
169
+ # Úhly mezi waypoints − typ zatáčky pro scénáře
170
+ def _corner_angle(i: int) -> float:
171
+ if i == 0 or i >= len(_TRACK_WAYPOINTS) - 1:
172
+ return 0.0
173
+ ax, az = _TRACK_WAYPOINTS[i - 1]
174
+ bx, bz = _TRACK_WAYPOINTS[i]
175
+ cx, cz = _TRACK_WAYPOINTS[i + 1]
176
+ v1 = (bx - ax, bz - az)
177
+ v2 = (cx - bx, cz - bz)
178
+ n1 = math.sqrt(v1[0]**2 + v1[1]**2)
179
+ n2 = math.sqrt(v2[0]**2 + v2[1]**2)
180
+ if n1 == 0 or n2 == 0:
181
+ return 0.0
182
+ cos_a = (v1[0]*v2[0] + v1[1]*v2[1]) / (n1 * n2)
183
+ return math.acos(max(-1.0, min(1.0, cos_a))) # 0 = rovinka, pi = vlásenka
184
+
185
+
186
+ def generate_ice_expert_data(
187
+ n_samples: int = 40000,
188
+ surface: str = "all", # "ice" | "snow" | "black_ice" | "all"
189
+ seed: int = 42,
190
+ noise: float = 0.04,
191
+ ) -> Tuple[np.ndarray, np.ndarray]:
192
+ """
193
+ Generuje syntetické BC demonstrace pro IceDrivingTask.
194
+
195
+ Místo spouštění plné fyziky simulujeme různé stavy jízdní situace
196
+ (rychlost, úhel zatáčky, drift_index, mu_factor) a aplikujeme expert policy.
197
+ Výsledek: dataset (state, expert_action) pro BC trénink.
198
+ """
199
+ rng = np.random.default_rng(seed)
200
+ log.info("Generuji expert demonstrace: n=%d surface=%s noise=%.3f",
201
+ n_samples, surface, noise)
202
+
203
+ # Mu faktory pro různé povrchy
204
+ surface_mus = {
205
+ "ice": (0.10, 0.18), # ledová dráha (WRC Szwecja)
206
+ "snow": (0.22, 0.34), # utlačený sníh (Rallye Monte Carlo)
207
+ "black_ice": (0.05, 0.09), # černý led (zákeřný)
208
+ "mixed": (0.10, 0.32), # střídání povrchů
209
+ }
210
+
211
+ if surface == "all":
212
+ surf_choices = ["ice", "snow", "black_ice", "mixed"]
213
+ surf_weights = [0.30, 0.35, 0.15, 0.20]
214
+ else:
215
+ surf_choices = [surface]
216
+ surf_weights = [1.0]
217
+
218
+ # Pre-compute goal vector for ice task
219
+ goal_vec = encode_goal(goal_for_task("drive_city")) # fallback goal
220
+
221
+ states = np.zeros((n_samples, CORTEX_DIM), dtype=np.float32)
222
+ actions = np.zeros((n_samples, 10), dtype=np.float32)
223
+
224
+ # Koutové úhly pro každý waypoint
225
+ corner_angles = [_corner_angle(i) for i in range(len(_TRACK_WAYPOINTS))]
226
+ max_corner = max(corner_angles) if corner_angles else 1.0
227
+
228
+ for i in range(n_samples):
229
+ # ── Výběr povrchu ─────────────────────────────────────────────────────
230
+ surf = rng.choice(surf_choices, p=surf_weights)
231
+ mu_lo, mu_hi = surface_mus[surf]
232
+ mu = float(rng.uniform(mu_lo, mu_hi))
233
+
234
+ # ── Náhodná pozice na trase ───────────────────���────────────────────────
235
+ wp_idx = int(rng.integers(0, len(_TRACK_WAYPOINTS)))
236
+ corner_ang = corner_angles[wp_idx] # 0=rovinka, ~pi/2=těsná
237
+
238
+ # Vzdálenost k waypointu (blíže = zatáčka se blíží)
239
+ dist_to_wp = float(rng.uniform(0, 800))
240
+ dist_norm = float(np.clip(dist_to_wp / 800.0, 0.0, 1.0))
241
+
242
+ # Úhel k waypointu (ang_err / pi)
243
+ # Na rovince: malý ang_err; v zatáčce: 0.2–0.8
244
+ ang_amplitude = corner_ang / math.pi * 0.9
245
+ ang_err_raw = float(rng.uniform(-ang_amplitude, ang_amplitude))
246
+ # Blíže k waypointu → menší ang_err (agent se srovnává)
247
+ ang_err = ang_err_raw * max(0.2, dist_norm)
248
+
249
+ # ── Rychlost ──────────────────────────────────────────────────────────
250
+ # Vzorkujeme realistický rozsah pro daný mu a zatáčku
251
+ max_speed_kmh = mu * 280.0 * (1.0 - 0.6 * corner_ang / math.pi)
252
+ max_speed_kmh = float(np.clip(max_speed_kmh, 5.0, 120.0))
253
+ speed_kmh = float(rng.uniform(5.0, max_speed_kmh))
254
+ speed_norm = float(np.clip(speed_kmh / 140.0, 0.0, 1.0))
255
+
256
+ # ── Drift index ───────────────────────────────────────────────────────
257
+ # 20% scénářů: spin-out recovery (drift > 0.65) — agent musí vědět jak z toho ven
258
+ if rng.random() < 0.20:
259
+ drift_idx = float(rng.uniform(0.65, 0.95))
260
+ else:
261
+ drift_base = speed_norm * corner_ang / math.pi * (1.0 - mu)
262
+ drift_idx = float(np.clip(
263
+ rng.normal(drift_base * 0.8, 0.08), 0.0, 0.60))
264
+
265
+ # ── Předchozí vstupy (LP filter paměť) ───────────────────────────────
266
+ prev_steer = float(rng.uniform(-ang_amplitude * 0.5, ang_amplitude * 0.5))
267
+ prev_thr = float(rng.uniform(0.1, 0.7) * (1.0 - corner_ang / math.pi * 0.6))
268
+
269
+ # ── Regen kW (přibližně) ──────────────────────────────────────────────
270
+ regen_norm = float(np.clip(rng.uniform(0, 0.4), 0.0, 1.0))
271
+
272
+ # ── Slip ratio ────────────────────────────────────────────────────────
273
+ slip = float(np.clip(drift_idx * 2.0, 0.0, 1.0))
274
+
275
+ # ── Černý led v okolí ─────────────────────────────────────────────────
276
+ black_ice = 1.0 if (surf == "black_ice" and rng.random() < 0.6) else 0.0
277
+
278
+ # ── Sestavení state vektoru ───────────────────────────────────────────
279
+ s = np.zeros(CORTEX_DIM, dtype=np.float32)
280
+
281
+ # state[0:18] = telemetrie (zjednodušená simulace)
282
+ s[0] = speed_norm
283
+ s[1] = float(rng.uniform(0.0, 0.5)) # lateral_accel (mock)
284
+ s[2] = prev_steer # steer
285
+ s[3] = prev_thr # throttle
286
+ s[4] = float(rng.uniform(0.5, 1.0)) # battery_soc
287
+ s[5] = float(rng.uniform(0.3, 0.8)) # supercap_soc
288
+ s[6] = regen_norm * 50.0 / 100.0 # regen_kw norm
289
+ s[7] = float(rng.uniform(-5, 5)) # yaw mock
290
+ s[8] = drift_idx # drift_index
291
+ s[9] = float(rng.uniform(0.0, 0.3)) # nose lift (mock)
292
+ s[10] = mu # tire_temp_fl proxy
293
+ s[11] = mu # tire_temp_fr proxy
294
+ s[12] = mu * 0.95 # tire_temp_rl
295
+ s[13] = mu * 0.95 # tire_temp_rr
296
+ s[14] = float(rng.uniform(-0.2, 0.2)) # gyro_x
297
+ s[15] = float(rng.uniform(-0.1, 0.1)) # gyro_z
298
+ s[16] = float(rng.uniform(0.3, 0.7)) # height
299
+
300
+ # state[18..27] = IceDrivingTask specifické
301
+ s[18] = dist_norm
302
+ s[19] = ang_err
303
+ s[20] = mu
304
+ s[21] = drift_idx
305
+ s[22] = speed_norm
306
+ s[23] = regen_norm
307
+ s[24] = prev_steer
308
+ s[25] = prev_thr
309
+ s[26] = slip
310
+ s[27] = black_ice
311
+
312
+ # Goal vector (mem + goal pásmo)
313
+ s[BODY_DIM + 8: BODY_DIM + 8 + 32] = goal_vec
314
+
315
+ # ── Expert akce ───────────────────────────────────────────────────────
316
+ a = _expert_action(s, rng, noise=noise)
317
+
318
+ states[i] = s
319
+ actions[i] = a
320
+
321
+ # Shuffle
322
+ idx = rng.permutation(n_samples)
323
+ states = states[idx]
324
+ actions = actions[idx]
325
+
326
+ log.info("Vygenerováno %d expert demonstrací "
327
+ "thr_range=[%.2f, %.2f] steer_range=[%.2f, %.2f]",
328
+ n_samples,
329
+ actions[:, 0].min(), actions[:, 0].max(),
330
+ actions[:, 2].min(), actions[:, 2].max())
331
+ return states, actions
332
+
333
+
334
+ # ── BC trénink ────────────────────────────────────────────────────────────────
335
+
336
+ def run_ice_bc_pretraining(
337
+ surface: str = "all",
338
+ n_samples: int = 40000,
339
+ steps: int = 15000,
340
+ batch_size: int = 64,
341
+ lr: float = 0.002,
342
+ save_path: str = "checkpoints/bc_ice.npz",
343
+ log_every: int = 1000,
344
+ noise: float = 0.04,
345
+ seed: int = 42,
346
+ ) -> PhantomAgent:
347
+ """
348
+ BC předtrénování pro IceDriving — syntetický expert.
349
+
350
+ Agent se naučí:
351
+ - Anticipační brzdění (dřív než na suchu)
352
+ - Plynulé vstupy (LP filter)
353
+ - Scandinavian flick (protisteer před vlásenkou)
354
+ - Regen-brzdění místo kotouče
355
+
356
+ Args:
357
+ surface : 'ice' | 'snow' | 'black_ice' | 'all' (mix)
358
+ n_samples : počet syntetických demonstrací
359
+ steps : počet BC tréninkových kroků
360
+ noise : šum na expert akcích pro rozmanitost
361
+ save_path : cesta pro uložení vah
362
+
363
+ Returns:
364
+ PhantomAgent s předtrénovanými vahami pro IceDriving
365
+ """
366
+ t0 = time.time()
367
+
368
+ states, actions = generate_ice_expert_data(
369
+ n_samples=n_samples, surface=surface, seed=seed, noise=noise)
370
+
371
+ N = len(states)
372
+ log.info("Dataset: %d state=%s action=%s", N, states.shape, actions.shape)
373
+
374
+ rng = np.random.default_rng(seed)
375
+ cfg = RLConfig(lr=lr)
376
+ agent = PhantomAgent(cfg)
377
+
378
+ losses = []
379
+ for step in range(steps):
380
+ idx = rng.integers(0, N, size=batch_size)
381
+ loss = agent.head.train_supervised_step(states[idx], actions[idx])
382
+ losses.append(float(loss))
383
+
384
+ if (step + 1) % log_every == 0:
385
+ avg = float(np.mean(losses[-log_every:]))
386
+ log.info("BC-ice step %5d/%d loss=%.4f (%.1fs)",
387
+ step + 1, steps, avg, time.time() - t0)
388
+
389
+ _save_agent_weights(agent, save_path)
390
+ final_loss = float(np.mean(losses[-500:]))
391
+ log.info("BC-ice hotovo → %s final_loss=%.4f (%.1fs)",
392
+ save_path, final_loss, time.time() - t0)
393
+ return agent
394
+
395
+
396
+ # ── CLI ───────────────────────────────────────────────────────────────────────
397
+
398
+ if __name__ == "__main__":
399
+ import argparse
400
+ p = argparse.ArgumentParser(description="BC předtrénování pro IceDriving — syntetický expert")
401
+ p.add_argument("--surface", default="all",
402
+ choices=["ice", "snow", "black_ice", "all"],
403
+ help="Povrch: 'ice' | 'snow' | 'black_ice' | 'all' (mix vše)")
404
+ p.add_argument("--n-samples", type=int, default=40000,
405
+ help="Počet syntetických demonstrací (default: 40000)")
406
+ p.add_argument("--steps", type=int, default=15000,
407
+ help="Počet BC tréninkových kroků (default: 15000)")
408
+ p.add_argument("--lr", type=float, default=0.002)
409
+ p.add_argument("--noise", type=float, default=0.04,
410
+ help="Šum na expert akcích (default: 0.04 = 4%)")
411
+ p.add_argument("--save", default="checkpoints/bc_ice.npz")
412
+ p.add_argument("--log-every", type=int, default=1000)
413
+ p.add_argument("--seed", type=int, default=42)
414
+ args = p.parse_args()
415
+ run_ice_bc_pretraining(
416
+ surface=args.surface, n_samples=args.n_samples,
417
+ steps=args.steps, lr=args.lr, noise=args.noise,
418
+ save_path=args.save, log_every=args.log_every, seed=args.seed,
419
+ )
embodied/pretrain/bc_pretrain.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BCPretrainer — Behaviour Cloning předtrénování agenta z MoCap + manipulačních dat.
3
+
4
+ Pipeline:
5
+ 1. Načtení LAFAN-G1 (lokomoce: chůze, běh) → stavy + akce pro tělo
6
+ 2. Načtení LIBERO-3D (manipulace: pick, place) → stavy + akce pro ruce
7
+ 3. Sloučení a zamíchání datasetu
8
+ 4. BC trénink: MotorActionHead.train_supervised_step()
9
+ - MSE loss: predikovaná_akce vs. demonstrovaná_akce
10
+ - čistě numpy, žádný PyTorch
11
+ 5. Uložení předtrénovaných vah do checkpoints/bc_pretrained.npz
12
+
13
+ Výsledek:
14
+ Agent začíná RL trénink s:
15
+ - Pochopením základní kinematiky chůze (LAFAN)
16
+ - Základním vzorcem manipulace — přiblížení ruky, uchopení, přenos (LIBERO)
17
+ → Výrazně rychlejší konvergence household_pick, household_stack, sweep.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ import sys
24
+ import time
25
+ from pathlib import Path
26
+ from typing import Tuple, Optional
27
+
28
+ import numpy as np
29
+
30
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
31
+
32
+ from cogcore.cognition.motor import MotorActionHead, MotorActionConfig
33
+ from embodied.rl_loop import RLConfig, PhantomAgent
34
+ from embodied.pretrain.motion_dataset import (
35
+ load_lafan, load_libero, load_basketball_bvh, merge_datasets, dataset_stats
36
+ )
37
+
38
+ log = logging.getLogger("phantom.pretrain")
39
+ logging.basicConfig(
40
+ level=logging.INFO,
41
+ format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
42
+ datefmt="%H:%M:%S",
43
+ )
44
+
45
+ CKPT_DIR = Path("checkpoints")
46
+ CKPT_DIR.mkdir(exist_ok=True)
47
+
48
+
49
+ # ── BC trénink ────────────────────────────────────────────────────────────────
50
+
51
+ def run_bc_pretraining(
52
+ lafan_dir: str = "/tmp/lafan_walk",
53
+ libero_dir: str = "/tmp/libero_pick",
54
+ steps: int = 8000,
55
+ batch_size: int = 64,
56
+ lr: float = 0.002,
57
+ save_path: str = "checkpoints/bc_pretrained.npz",
58
+ log_every: int = 500,
59
+ ) -> PhantomAgent:
60
+ """
61
+ Spustí BC předtrénování a vrátí natrénovaného agenta.
62
+
63
+ Args:
64
+ lafan_dir : cesta ke složce s LAFAN .npz soubory
65
+ libero_dir: cesta ke složce s LIBERO epizodami (rekurzivně)
66
+ steps : počet BC tréninkových kroků
67
+ batch_size: velikost dávky
68
+ lr : learning rate (vyšší než RL — BC konverguje rychle)
69
+ save_path : kam uložit váhy
70
+ log_every : interval logování
71
+ Returns:
72
+ PhantomAgent s předtrénovanými vahami
73
+ """
74
+ rng = np.random.default_rng(42)
75
+
76
+ # ── Načtení dat ──────────────────────────────────────────────────────────
77
+ datasets = []
78
+
79
+ log.info("Načítám LAFAN-G1 lokomoční data z %s ...", lafan_dir)
80
+ try:
81
+ s_laf, a_laf = load_lafan(lafan_dir, task_name="walk", max_files=10, subsample=3)
82
+ dataset_stats(s_laf, a_laf, "LAFAN-walk")
83
+ datasets.append((s_laf, a_laf))
84
+ log.info("LAFAN: %d vzorků lokomoce", len(s_laf))
85
+ except Exception as e:
86
+ log.warning("LAFAN nenačten: %s", e)
87
+
88
+ log.info("Načítám LIBERO-3D manipulační demonstrace z %s ...", libero_dir)
89
+ try:
90
+ s_lib, a_lib = load_libero(libero_dir, task_name="household_pick", max_episodes=30)
91
+ dataset_stats(s_lib, a_lib, "LIBERO-pick")
92
+ datasets.append((s_lib, a_lib))
93
+ log.info("LIBERO: %d vzorků manipulace", len(s_lib))
94
+ except Exception as e:
95
+ log.warning("LIBERO nenačten: %s", e)
96
+
97
+ if not datasets:
98
+ raise RuntimeError("Nepodařilo se načíst žádná data pro BC předtrénování.")
99
+
100
+ states, actions = merge_datasets(*datasets, shuffle=True, seed=42)
101
+ N = len(states)
102
+ log.info("Celkem %d vzorků (lokomoce + manipulace) → BC trénink %d kroků", N, steps)
103
+
104
+ # ── Inicializace agenta ──────────────────────────────────────────────────
105
+ cfg = RLConfig(lr=lr)
106
+ agent = PhantomAgent(cfg)
107
+
108
+ # ── BC smyčka ────────────────────────────────────────────────────────────
109
+ losses = []
110
+ t0 = time.time()
111
+
112
+ for step in range(steps):
113
+ idx = rng.integers(0, N, size=batch_size)
114
+ batch_s = states[idx]
115
+ batch_a = actions[idx]
116
+
117
+ loss = agent.head.train_supervised_step(batch_s, batch_a)
118
+ losses.append(loss)
119
+
120
+ if (step + 1) % log_every == 0:
121
+ recent_loss = float(np.mean(losses[-log_every:]))
122
+ elapsed = time.time() - t0
123
+ log.info(
124
+ "BC step %5d/%d loss=%.4f (%.1f s)",
125
+ step + 1, steps, recent_loss, elapsed,
126
+ )
127
+
128
+ # ── Uložení vah ──────────────────────────────────────────────────────────
129
+ _save_agent_weights(agent, save_path)
130
+ log.info("BC předtrénování hotovo. Váhy uloženy: %s", save_path)
131
+ log.info("Průměrná finální loss (posledních 500): %.4f", float(np.mean(losses[-500:])))
132
+
133
+ return agent
134
+
135
+
136
+ def _save_agent_weights(agent: PhantomAgent, path: str) -> None:
137
+ """Uloží váhy MotorActionHead do .npz — kompatibilní s rl_loop checkpointem."""
138
+ np.savez(
139
+ path,
140
+ W1=agent.head.W1,
141
+ b1=agent.head.b1,
142
+ W_action=agent.head.W_action,
143
+ b_action=agent.head.b_action,
144
+ W_disc=agent.head.W_disc,
145
+ b_disc=agent.head.b_disc,
146
+ )
147
+
148
+
149
+ def load_bc_weights_into_agent(agent: PhantomAgent, path: str) -> None:
150
+ """
151
+ Načte BC předtrénované váhy do existujícího agenta.
152
+ Volat před spuštěním RL tréninku.
153
+ """
154
+ if not os.path.exists(path):
155
+ log.warning("BC váhy nenalezeny: %s — přeskakuji", path)
156
+ return
157
+ d = np.load(path)
158
+ agent.head.W1 = d["W1"].astype(np.float32)
159
+ agent.head.b1 = d["b1"].astype(np.float32)
160
+ agent.head.W_action = d["W_action"].astype(np.float32)
161
+ agent.head.b_action = d["b_action"].astype(np.float32)
162
+ agent.head.W_disc = d["W_disc"].astype(np.float32)
163
+ agent.head.b_disc = d["b_disc"].astype(np.float32)
164
+ log.info("BC předtrénované váhy načteny z %s", path)
165
+
166
+
167
+ def run_basketball_bc_pretraining(
168
+ basketball_dir: str = "/tmp/basketball_mocap",
169
+ steps: int = 10000,
170
+ batch_size: int = 64,
171
+ lr: float = 0.002,
172
+ save_path: str = "checkpoints/bc_basketball.npz",
173
+ log_every: int = 500,
174
+ subsample: int = 6,
175
+ ) -> PhantomAgent:
176
+ """
177
+ BC předtrénování z CMU MoCap basketbalových BVH sekvencí.
178
+
179
+ Naučí agenta základní basketbalové pohyby:
180
+ - dribbling (pravá ruka dolů/nahoru, rytmický pohyb)
181
+ - shooting (výmah paže, zápěstí snap)
182
+ - layup (kombinace pohybu + střelby)
183
+
184
+ Args:
185
+ basketball_dir : složka s CMU MoCap .bvh soubory (subject 49 + 140)
186
+ steps : počet BC kroků
187
+ save_path : kde uložit váhy
188
+ subsample : podvzorkování 120fps → 20fps (subsample=6)
189
+ """
190
+ rng = np.random.default_rng(42)
191
+
192
+ log.info("Načítám CMU MoCap basketbalová data z %s ...", basketball_dir)
193
+ s_bball, a_bball = load_basketball_bvh(
194
+ basketball_dir, task_name="dribble", subsample=subsample
195
+ )
196
+ dataset_stats(s_bball, a_bball, "CMU-basketball")
197
+ log.info("Basketball BVH: %d vzorků z %s", len(s_bball), basketball_dir)
198
+
199
+ # Přidáme i lokomoční data (basketbal = pohyb + ruce)
200
+ datasets: list = [(s_bball, a_bball)]
201
+ try:
202
+ s_laf, a_laf = load_lafan("/tmp/lafan_walk", task_name="walk", max_files=5, subsample=3)
203
+ datasets.append((s_laf, a_laf))
204
+ log.info("LAFAN lokomoce přidána: %d vzorků", len(s_laf))
205
+ except Exception:
206
+ pass
207
+
208
+ states, actions = merge_datasets(*datasets, shuffle=True, seed=42)
209
+ N = len(states)
210
+ log.info("Celkem %d vzorků → BC trénink %d kroků", N, steps)
211
+
212
+ cfg = RLConfig(lr=lr)
213
+ agent = PhantomAgent(cfg)
214
+
215
+ losses = []
216
+ t0 = time.time()
217
+ for step in range(steps):
218
+ idx = rng.integers(0, N, size=batch_size)
219
+ loss = agent.head.train_supervised_step(states[idx], actions[idx])
220
+ losses.append(loss)
221
+ if (step + 1) % log_every == 0:
222
+ log.info("BC-basketball step %5d/%d loss=%.4f (%.1f s)",
223
+ step + 1, steps, float(np.mean(losses[-log_every:])), time.time() - t0)
224
+
225
+ _save_agent_weights(agent, save_path)
226
+ log.info("BC-basketball hotovo. Váhy: %s final_loss=%.4f", save_path,
227
+ float(np.mean(losses[-500:])))
228
+ return agent
229
+
230
+
231
+ # ── Standalone spuštění ───────────────────────────────────────────────────────
232
+
233
+ if __name__ == "__main__":
234
+ import argparse
235
+ parser = argparse.ArgumentParser(description="BC předtrénování z LAFAN + LIBERO")
236
+ parser.add_argument("--lafan", default="/tmp/lafan_walk", help="Složka s LAFAN .npz")
237
+ parser.add_argument("--libero", default="/tmp/libero_pick", help="Složka s LIBERO epizodami")
238
+ parser.add_argument("--steps", type=int, default=8000, help="Počet BC kroků")
239
+ parser.add_argument("--batch", type=int, default=64, help="Velikost dávky")
240
+ parser.add_argument("--lr", type=float, default=0.002, help="Learning rate")
241
+ parser.add_argument("--save", default="checkpoints/bc_pretrained.npz")
242
+ parser.add_argument("--log-every", type=int, default=500)
243
+ args = parser.parse_args()
244
+
245
+ run_bc_pretraining(
246
+ lafan_dir=args.lafan,
247
+ libero_dir=args.libero,
248
+ steps=args.steps,
249
+ batch_size=args.batch,
250
+ lr=args.lr,
251
+ save_path=args.save,
252
+ log_every=args.log_every,
253
+ )
embodied/pretrain/motion_dataset.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MotionDataset — načítač MoCap a manipulačních demonstrací pro BC pre-training.
3
+
4
+ Zdroje:
5
+ LAFAN-G1 (ember-lab-berkeley/LAFAN-G1) — lokomoce: chůze, běh, pád+vstávání
6
+ LIBERO-3D (Ayana666888/LIBERO-3D) — manipulace: pick, place, open drawer
7
+
8
+ Mapování do agentova prostoru (73D stav, 10D akce):
9
+ ──────────────────────────────────────────────────
10
+ LAFAN → stav:
11
+ dof_positions[0:28] → body_state[0:28] (28 ze 29 kloubů G1 humanoid)
12
+ mem[0:8] = 0 (paměť se inicializuje za běhu)
13
+ goal[0:32] = goal_for_task("walk")
14
+ bio[0:5] = 0
15
+
16
+ LAFAN → akce (Δ kloubní pozice → 10D):
17
+ akce[0] = mean(Δhip_pitch_L, Δhip_pitch_R) ← dopředný pohyb
18
+ akce[1] = mean(Δhip_roll_L, Δhip_roll_R) ← laterální vychýlení
19
+ akce[2] = Δwaist_yaw ← rotace trupu
20
+ akce[3] = Δleft_shoulder_pitch ← levé rameno
21
+ akce[4] = Δleft_elbow ← levý loket
22
+ akce[5] = Δleft_wrist_pitch ← levé zápěstí
23
+ akce[6] = Δright_shoulder_pitch ← pravé rameno
24
+ akce[7] = Δright_elbow ← pravý loket
25
+ akce[8] = Δright_wrist_pitch ← pravé zápěstí
26
+ akce[9] = 0 ← gripper (LAFAN nemá)
27
+
28
+ LIBERO → stav:
29
+ state[0:8] → body_state[0:8] (end-effector + gripper jako proxy kloubů)
30
+ goal[0:32] = goal_for_task("household_pick")
31
+
32
+ LIBERO → akce (7D → 10D):
33
+ akce[0:3] = 0 ← základ stojí
34
+ akce[3:6] = libero_action[0:3] ← translace → pohyb levé paže
35
+ akce[6:9] = libero_action[3:6] ← rotace → orientace pravé paže
36
+ akce[9] = libero_action[6] ← gripper (1D)
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import os
41
+ import glob
42
+ import numpy as np
43
+ from pathlib import Path
44
+ from typing import List, Tuple, Optional
45
+
46
+ from embodied.goal_encoder import encode_goal, goal_for_task, GOAL_DIM
47
+ from embodied.phantom_cortex import CORTEX_DIM, BODY_DIM
48
+
49
+ # ── LAFAN kloubní indexy (z dof_names G1 humanoid) ──────────────────────────
50
+ _L = {
51
+ "hip_pitch_L": 0, "hip_roll_L": 1, "hip_yaw_L": 2,
52
+ "knee_L": 3, "ankle_pitch_L": 4, "ankle_roll_L": 5,
53
+ "hip_pitch_R": 6, "hip_roll_R": 7, "hip_yaw_R": 8,
54
+ "knee_R": 9, "ankle_pitch_R": 10, "ankle_roll_R": 11,
55
+ "waist_yaw": 12, "waist_roll": 13, "waist_pitch": 14,
56
+ "shoulder_pitch_L": 15, "shoulder_roll_L": 16, "shoulder_yaw_L": 17,
57
+ "elbow_L": 18, "wrist_roll_L": 19, "wrist_pitch_L": 20, "wrist_yaw_L": 21,
58
+ "shoulder_pitch_R": 22, "shoulder_roll_R": 23, "shoulder_yaw_R": 24,
59
+ "elbow_R": 25, "wrist_roll_R": 26, "wrist_pitch_R": 27, "wrist_yaw_R": 28,
60
+ }
61
+
62
+ _ACTION_SCALE = 5.0 # Δklouby jsou malé → škáluj na [-1, 1] range
63
+
64
+
65
+ def _lafan_to_state(dof_pos: np.ndarray, task_name: str = "walk") -> np.ndarray:
66
+ """29D dof_positions → 73D agent stav."""
67
+ state = np.zeros(CORTEX_DIM, dtype=np.float32)
68
+ state[0:28] = dof_pos[0:28]
69
+ goal_vec = encode_goal(goal_for_task(task_name))
70
+ state[BODY_DIM + 8: BODY_DIM + 8 + GOAL_DIM] = goal_vec
71
+ return state
72
+
73
+
74
+ def _lafan_delta_to_action(delta: np.ndarray) -> np.ndarray:
75
+ """Δ29D dof → 10D agent akce."""
76
+ a = np.zeros(10, dtype=np.float32)
77
+ a[0] = (delta[_L["hip_pitch_L"]] + delta[_L["hip_pitch_R"]]) * 0.5 * _ACTION_SCALE
78
+ a[1] = (delta[_L["hip_roll_L"]] + delta[_L["hip_roll_R"]]) * 0.5 * _ACTION_SCALE
79
+ a[2] = delta[_L["waist_yaw"]] * _ACTION_SCALE
80
+ a[3] = delta[_L["shoulder_pitch_L"]] * _ACTION_SCALE
81
+ a[4] = delta[_L["elbow_L"]] * _ACTION_SCALE
82
+ a[5] = delta[_L["wrist_pitch_L"]] * _ACTION_SCALE
83
+ a[6] = delta[_L["shoulder_pitch_R"]] * _ACTION_SCALE
84
+ a[7] = delta[_L["elbow_R"]] * _ACTION_SCALE
85
+ a[8] = delta[_L["wrist_pitch_R"]] * _ACTION_SCALE
86
+ a[9] = 0.0
87
+ return np.clip(a, -1.0, 1.0)
88
+
89
+
90
+ def _libero_to_state(robot_state: np.ndarray, task_name: str = "household_pick") -> np.ndarray:
91
+ """8D LIBERO end-effector stav → 73D agent stav."""
92
+ state = np.zeros(CORTEX_DIM, dtype=np.float32)
93
+ n = min(len(robot_state), 8)
94
+ state[0:n] = robot_state[:n]
95
+ goal_vec = encode_goal(goal_for_task(task_name))
96
+ state[BODY_DIM + 8: BODY_DIM + 8 + GOAL_DIM] = goal_vec
97
+ return state
98
+
99
+
100
+ def _libero_action_to_agent(libero_action: np.ndarray) -> np.ndarray:
101
+ """7D LIBERO akce → 10D agent akce."""
102
+ a = np.zeros(10, dtype=np.float32)
103
+ a[0:3] = 0.0
104
+ a[3:6] = libero_action[0:3]
105
+ a[6:9] = libero_action[3:6]
106
+ a[9] = libero_action[6]
107
+ return np.clip(a, -1.0, 1.0)
108
+
109
+
110
+ # ── Loadery ──────────────────────────────────────────────────────���───────────
111
+
112
+ def load_lafan(data_dir: str, task_name: str = "walk",
113
+ max_files: int = 20, subsample: int = 3) -> Tuple[np.ndarray, np.ndarray]:
114
+ """
115
+ Načte LAFAN-G1 .npz soubory → (states, actions).
116
+ subsample: bere každý N-tý snímek (30fps → 10fps při subsample=3).
117
+ """
118
+ files = glob.glob(os.path.join(data_dir, "*.npz"))
119
+ if not files:
120
+ raise FileNotFoundError(f"Žádné .npz soubory v {data_dir}")
121
+ files = sorted(files)[:max_files]
122
+
123
+ all_states, all_actions = [], []
124
+ for fpath in files:
125
+ d = np.load(fpath)
126
+ pos = d["dof_positions"][::subsample] # shape (T, 29)
127
+ if len(pos) < 2:
128
+ continue
129
+ deltas = np.diff(pos, axis=0) # shape (T-1, 29)
130
+ for i in range(len(deltas)):
131
+ s = _lafan_to_state(pos[i], task_name)
132
+ a = _lafan_delta_to_action(deltas[i])
133
+ all_states.append(s)
134
+ all_actions.append(a)
135
+
136
+ return np.array(all_states, dtype=np.float32), np.array(all_actions, dtype=np.float32)
137
+
138
+
139
+ def load_libero(data_dir: str, task_name: str = "household_pick",
140
+ max_episodes: int = 50) -> Tuple[np.ndarray, np.ndarray]:
141
+ """
142
+ Načte LIBERO-3D .npz epizody → (states, actions).
143
+ Rekurzivně prohledá data_dir pro všechna data.npz.
144
+ """
145
+ files = sorted(glob.glob(os.path.join(data_dir, "**", "data.npz"), recursive=True))
146
+ if not files:
147
+ raise FileNotFoundError(f"Žádné data.npz v {data_dir}")
148
+ files = files[:max_episodes]
149
+
150
+ all_states, all_actions = [], []
151
+ for fpath in files:
152
+ d = np.load(fpath, allow_pickle=True)
153
+ states = d["state"] # (T, 8)
154
+ actions = d["actions"] # (T, 7)
155
+ T = min(len(states), len(actions))
156
+ for t in range(T):
157
+ s = _libero_to_state(states[t], task_name)
158
+ a = _libero_action_to_agent(actions[t])
159
+ all_states.append(s)
160
+ all_actions.append(a)
161
+
162
+ return np.array(all_states, dtype=np.float32), np.array(all_actions, dtype=np.float32)
163
+
164
+
165
+ def merge_datasets(*datasets: Tuple[np.ndarray, np.ndarray],
166
+ shuffle: bool = True, seed: int = 42) -> Tuple[np.ndarray, np.ndarray]:
167
+ """Sloučí více (states, actions) dvojic dohromady."""
168
+ all_s = np.concatenate([d[0] for d in datasets], axis=0)
169
+ all_a = np.concatenate([d[1] for d in datasets], axis=0)
170
+ if shuffle:
171
+ rng = np.random.default_rng(seed)
172
+ idx = rng.permutation(len(all_s))
173
+ all_s, all_a = all_s[idx], all_a[idx]
174
+ return all_s, all_a
175
+
176
+
177
+ def dataset_stats(states: np.ndarray, actions: np.ndarray, name: str = "") -> None:
178
+ print(f"[Dataset{' ' + name if name else ''}]")
179
+ print(f" Vzorků : {len(states):,}")
180
+ print(f" State : {states.shape} range=[{states.min():.3f}, {states.max():.3f}]")
181
+ print(f" Action : {actions.shape} range=[{actions.min():.3f}, {actions.max():.3f}]")
182
+ print(f" Act mean : {actions.mean(0).round(3)}")
183
+ print(f" Act std : {actions.std(0).round(3)}")
184
+
185
+
186
+ # ── CMU MoCap BVH parser (basketbalové sekvence) ─────────────────────────────
187
+ #
188
+ # CMU skeleton (subject 49, 140) — channel layout (96 ch / frame):
189
+ # Root/Hips ch 0-5 : Xpos, Ypos, Zpos, Zrot, Yrot, Xrot
190
+ # LHipJoint ch 6-8 : Zrot, Yrot, Xrot
191
+ # LeftUpLeg ch 9-11
192
+ # LeftLeg ch 12-14
193
+ # LeftFoot ch 15-17
194
+ # LeftToeBase ch 18-20
195
+ # RHipJoint ch 21-23
196
+ # RightUpLeg ch 24-26
197
+ # RightLeg ch 27-29
198
+ # RightFoot ch 30-32
199
+ # RightToeBase ch 33-35
200
+ # LowerBack ch 36-38
201
+ # Spine ch 39-41
202
+ # Spine1 ch 42-44
203
+ # Neck/Neck1/Head ch 45-53
204
+ # LeftShoulder ch 54-56
205
+ # LeftArm ch 57-59 ← shoulder pitch (Xrot = ch 59)
206
+ # LeftForeArm ch 60-62 ← elbow (Zrot = ch 60)
207
+ # LeftHand ch 63-65 ← wrist (Xrot = ch 65)
208
+ # LeftFinger/LThumb ch 66-74
209
+ # RightShoulder ch 75-77
210
+ # RightArm ch 78-80 ← shoulder pitch (Xrot = ch 80)
211
+ # RightForeArm ch 81-83 ← elbow (Zrot = ch 81)
212
+ # RightHand ch 84-86 ← wrist (Xrot = ch 86)
213
+ # RightFinger/RThumb ch 87-95
214
+
215
+ _CMU_TOTAL_CH = 96
216
+ _CMU_DEG2RAD = np.pi / 180.0
217
+
218
+ # Mapování CMU channels → agent 10D actions
219
+ # [0] hip forward = mean(LeftUpLeg Xrot, RightUpLeg Xrot)
220
+ # [1] hip lateral = mean(LeftUpLeg Zrot, RightUpLeg Zrot)
221
+ # [2] waist yaw = LowerBack Yrot
222
+ # [3] L shoulder = LeftArm Xrot
223
+ # [4] L elbow = LeftForeArm Zrot
224
+ # [5] L wrist = LeftHand Xrot
225
+ # [6] R shoulder = RightArm Xrot
226
+ # [7] R elbow = RightForeArm Zrot
227
+ # [8] R wrist = RightHand Xrot
228
+ # [9] gripper = derived: RightHand downward velocity (dribble push)
229
+ _CMU_CH = {
230
+ "lup_xrot": 11, "lup_zrot": 9,
231
+ "rup_xrot": 26, "rup_zrot": 24,
232
+ "lback_yrot": 37,
233
+ "larm_xrot": 59,
234
+ "lfore_zrot": 60,
235
+ "lhand_xrot": 65,
236
+ "rarm_xrot": 80,
237
+ "rfore_zrot": 81,
238
+ "rhand_xrot": 86,
239
+ "root_ypos": 1, # výška boků (pro dribble detekci)
240
+ "rhand_yrot": 85, # wrist Y = dribble záp
241
+ }
242
+
243
+ _CMU_ACTION_SCALE = 0.012 # degrees → [-1,1]: 80° full range → ~1.0
244
+
245
+
246
+ def _parse_bvh_frames(filepath: str) -> Optional[np.ndarray]:
247
+ """Parsuje BVH soubor → pole snímků (T, 96)."""
248
+ frames_data = []
249
+ in_motion = False
250
+ n_frames = 0
251
+ with open(filepath, "r") as f:
252
+ for line in f:
253
+ line = line.strip()
254
+ if line == "MOTION":
255
+ in_motion = True
256
+ continue
257
+ if not in_motion:
258
+ continue
259
+ if line.startswith("Frames:"):
260
+ n_frames = int(line.split(":")[1].strip())
261
+ continue
262
+ if line.startswith("Frame Time:"):
263
+ continue
264
+ vals = [float(x) for x in line.split()]
265
+ if len(vals) == _CMU_TOTAL_CH:
266
+ frames_data.append(vals)
267
+ if not frames_data:
268
+ return None
269
+ arr = np.array(frames_data, dtype=np.float32)
270
+ return arr[:n_frames] if n_frames > 0 else arr
271
+
272
+
273
+ def _cmu_frame_to_state(frame: np.ndarray, task_name: str = "dribble") -> np.ndarray:
274
+ """96D CMU frame → 73D agent stav."""
275
+ state = np.zeros(CORTEX_DIM, dtype=np.float32)
276
+ # Klouby horní končetiny → body_state[15:28] (shoulder, elbow, wrist obě strany)
277
+ state[15] = frame[_CMU_CH["larm_xrot"]] * _CMU_DEG2RAD
278
+ state[16] = frame[_CMU_CH["lfore_zrot"]] * _CMU_DEG2RAD
279
+ state[17] = frame[_CMU_CH["lhand_xrot"]] * _CMU_DEG2RAD
280
+ state[18] = frame[_CMU_CH["rarm_xrot"]] * _CMU_DEG2RAD
281
+ state[19] = frame[_CMU_CH["rfore_zrot"]] * _CMU_DEG2RAD
282
+ state[20] = frame[_CMU_CH["rhand_xrot"]] * _CMU_DEG2RAD
283
+ # Nohy → body_state[0:6]
284
+ state[0] = frame[_CMU_CH["lup_xrot"]] * _CMU_DEG2RAD
285
+ state[1] = frame[_CMU_CH["lup_zrot"]] * _CMU_DEG2RAD
286
+ state[6] = frame[_CMU_CH["rup_xrot"]] * _CMU_DEG2RAD
287
+ state[7] = frame[_CMU_CH["rup_zrot"]] * _CMU_DEG2RAD
288
+ # Trup
289
+ state[12] = frame[_CMU_CH["lback_yrot"]] * _CMU_DEG2RAD
290
+ # Goal vector
291
+ goal_vec = encode_goal(goal_for_task(task_name))
292
+ state[BODY_DIM + 8: BODY_DIM + 8 + GOAL_DIM] = goal_vec
293
+ return state
294
+
295
+
296
+ def _cmu_delta_to_action(frame: np.ndarray, prev_frame: np.ndarray) -> np.ndarray:
297
+ """Δ mezi dvěma CMU snímky → 10D agent akce."""
298
+ delta = frame - prev_frame
299
+ a = np.zeros(10, dtype=np.float32)
300
+ a[0] = (delta[_CMU_CH["lup_xrot"]] + delta[_CMU_CH["rup_xrot"]]) * 0.5 * _CMU_ACTION_SCALE
301
+ a[1] = (delta[_CMU_CH["lup_zrot"]] + delta[_CMU_CH["rup_zrot"]]) * 0.5 * _CMU_ACTION_SCALE
302
+ a[2] = delta[_CMU_CH["lback_yrot"]] * _CMU_ACTION_SCALE
303
+ a[3] = delta[_CMU_CH["larm_xrot"]] * _CMU_ACTION_SCALE
304
+ a[4] = delta[_CMU_CH["lfore_zrot"]] * _CMU_ACTION_SCALE
305
+ a[5] = delta[_CMU_CH["lhand_xrot"]] * _CMU_ACTION_SCALE
306
+ a[6] = delta[_CMU_CH["rarm_xrot"]] * _CMU_ACTION_SCALE
307
+ a[7] = delta[_CMU_CH["rfore_zrot"]] * _CMU_ACTION_SCALE
308
+ a[8] = delta[_CMU_CH["rhand_xrot"]] * _CMU_ACTION_SCALE
309
+ # Gripper: pravá ruka jde dolů (pozitivní Xrot delta = flexe → dribble tlak)
310
+ rhand_delta_y = delta[_CMU_CH["rhand_yrot"]]
311
+ a[9] = float(np.clip(rhand_delta_y * 0.03, -1.0, 1.0))
312
+ return np.clip(a, -1.0, 1.0)
313
+
314
+
315
+ def load_basketball_bvh(
316
+ data_dir: str,
317
+ task_name: str = "dribble",
318
+ subsample: int = 6,
319
+ max_files: int = 30,
320
+ ) -> Tuple[np.ndarray, np.ndarray]:
321
+ """
322
+ Načte CMU MoCap basketball BVH soubory → (states, actions).
323
+
324
+ subsample=6: 120fps → 20fps (basketbal je rychlý pohyb)
325
+ Vrací prázdné pole pokud žádné BVH soubory nenalezeny.
326
+ """
327
+ files = sorted(glob.glob(os.path.join(data_dir, "*.bvh")))
328
+ if not files:
329
+ raise FileNotFoundError(f"Žádné .bvh soubory v {data_dir}")
330
+ files = files[:max_files]
331
+
332
+ all_states, all_actions = [], []
333
+ for fpath in files:
334
+ frames = _parse_bvh_frames(fpath)
335
+ if frames is None or len(frames) < 2:
336
+ continue
337
+ frames = frames[::subsample]
338
+ for i in range(1, len(frames)):
339
+ s = _cmu_frame_to_state(frames[i], task_name)
340
+ a = _cmu_delta_to_action(frames[i], frames[i - 1])
341
+ all_states.append(s)
342
+ all_actions.append(a)
343
+
344
+ if not all_states:
345
+ raise ValueError("Žádná platná data z BVH souborů")
346
+ return np.array(all_states, dtype=np.float32), np.array(all_actions, dtype=np.float32)
embodied/rl_loop.py ADDED
@@ -0,0 +1,503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Autonomous RL training loop with:
3
+ - 10-DOF action (hips/knees/shoulders/elbows/grip)
4
+ - 69-dim state (24 body + 8 memory + 32 goal + 5 biocor)
5
+ - PhantomCortex: 4-tier HexaPenta memory, BioCor safety, ACC conflict detection
6
+ - Per-task noise reset + ACC/BioCor noise modulation
7
+ """
8
+ from __future__ import annotations
9
+ import sys, os, json, time, logging
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
11
+
12
+ import numpy as np
13
+ from pathlib import Path
14
+ from typing import List, Dict, Optional, Tuple, Any
15
+ from dataclasses import dataclass, field, asdict
16
+
17
+ from cogcore.cognition.motor import MotorActionHead, MotorActionConfig, MotorReplayBuffer
18
+ from cogcore.emergent.loop import EmergentCognitiveLoop, EmergentLoopConfig
19
+ from engine.cognitive_observation import EngineCognitiveObservationAdapter
20
+ from embodied.language_goal_bridge import goal_to_text
21
+ from embodied.anti_stasis import (
22
+ AntiDoNothingController, AntiStasisConfig, AgentNeeds,
23
+ EmbodiedObservation, MotorExplorationScheduler, EventRecord,
24
+ )
25
+ from embodied.virtual_body import VirtualBody
26
+ from embodied.physics_world import WorldEnv
27
+ from embodied.tasks.base import Task, TaskResult
28
+ from embodied.goal_encoder import GOAL_DIM, goal_for_task, encode_goal
29
+ from embodied.phantom_cortex import PhantomCortex, CORTEX_DIM
30
+
31
+ log = logging.getLogger("phantom.rl")
32
+
33
+
34
+ # ── Config ────────────────────────────────────────────────────────────────────
35
+
36
+ @dataclass
37
+ class RLConfig:
38
+ state_dim: int = CORTEX_DIM # 24 body + 8 mem + 32 goal + 5 bio = 69
39
+ action_dim: int = 10
40
+ hidden_dim: int = 64
41
+ lr: float = 0.003
42
+ gamma: float = 0.99
43
+ buffer_cap: int = 8192
44
+ train_every: int = 16
45
+ batch_size: int = 32
46
+ noise_std: float = 0.20
47
+ noise_floor: float = 0.10 # min 10% explorace — nikdy nepřestat zkoumat
48
+ noise_decay: float = 0.9998 # pomaleji: min dosažen za ~8000 train-steps (~65 ep)
49
+ reset_noise: float = 0.20 # při každém novém tasku resetuj na 20%
50
+ dt: float = 0.016
51
+ seed: int = 42
52
+ use_gemini: bool = False # multimodální goal generation přes Gemini API
53
+
54
+
55
+ # ── Experience & Replay ───────────────────────────────────────────────────────
56
+
57
+ @dataclass
58
+ class Transition:
59
+ state: np.ndarray
60
+ action: np.ndarray
61
+ reward: float
62
+ next_state: np.ndarray
63
+ done: bool
64
+
65
+
66
+ class ExperienceBuffer:
67
+ def __init__(self, capacity: int, state_dim: int, action_dim: int):
68
+ self.cap = capacity
69
+ self.idx = 0
70
+ self.full = False
71
+ self.states = np.zeros((capacity, state_dim), np.float32)
72
+ self.actions = np.zeros((capacity, action_dim), np.float32)
73
+ self.rewards = np.zeros(capacity, np.float32)
74
+ self.next_states = np.zeros((capacity, state_dim), np.float32)
75
+ self.dones = np.zeros(capacity, bool)
76
+
77
+ def push(self, t: Transition):
78
+ i = self.idx % self.cap
79
+ self.states[i] = t.state
80
+ self.actions[i] = t.action
81
+ self.rewards[i] = t.reward
82
+ self.next_states[i] = t.next_state
83
+ self.dones[i] = t.done
84
+ self.idx += 1
85
+ if self.idx >= self.cap:
86
+ self.full = True
87
+
88
+ def sample(self, n: int, rng: np.random.Generator) -> Tuple:
89
+ size = self.cap if self.full else self.idx
90
+ idx = rng.integers(0, size, size=min(n, size))
91
+ return (self.states[idx], self.actions[idx],
92
+ self.rewards[idx], self.next_states[idx], self.dones[idx])
93
+
94
+ def __len__(self):
95
+ return self.cap if self.full else self.idx
96
+
97
+
98
+ # ── RL Agent ──────────────────────────────────────────────────────────────────
99
+
100
+ class PhantomAgent:
101
+ """
102
+ MotorActionHead + PhantomCortex (4-tier memory, BioCor safety, ACC conflict).
103
+ State: 24 body + 8 mem + 32 goal + 5 bio = 69 dims.
104
+ """
105
+
106
+ def __init__(self, cfg: RLConfig):
107
+ self.cfg = cfg
108
+ self.rng = np.random.default_rng(cfg.seed)
109
+ motor_cfg = MotorActionConfig(
110
+ state_dim=cfg.state_dim,
111
+ action_dim=cfg.action_dim,
112
+ hidden_dim=cfg.hidden_dim,
113
+ lr=cfg.lr,
114
+ )
115
+ self.head = MotorActionHead(config=motor_cfg, seed=cfg.seed)
116
+ self.buffer = ExperienceBuffer(cfg.buffer_cap, cfg.state_dim, cfg.action_dim)
117
+ self.noise = cfg.noise_std
118
+ self.cortex = PhantomCortex()
119
+ self.motor_scheduler = MotorExplorationScheduler(
120
+ epsilon_start=0.25, epsilon_end=0.05, decay_ticks=300_000, seed=cfg.seed
121
+ )
122
+ self._global_tick = 0
123
+ self._step = 0
124
+ self._task_name = "stand"
125
+
126
+ def reset_memory(self, task_name: str = "stand"):
127
+ self._task_name = task_name
128
+ self.cortex.reset(task_name)
129
+
130
+ def set_goal(self, goal_vec: np.ndarray):
131
+ self.cortex.set_goal(goal_vec)
132
+
133
+ def act(self, body_state: np.ndarray, last_action: np.ndarray,
134
+ reward: float = 0.0,
135
+ goal_vec: np.ndarray | None = None) -> tuple[np.ndarray, np.ndarray]:
136
+ if goal_vec is not None:
137
+ self.cortex.set_goal(goal_vec)
138
+ state = self.cortex.step(body_state, last_action, reward, self._task_name)
139
+ out = self.head.forward(state)
140
+ action = np.array(out["action"], dtype=np.float32)
141
+ # ACC + BioCor modulate exploration noise
142
+ eff_noise = self.cortex.noise_multiplier(self.noise)
143
+ noise = self.rng.normal(0, eff_noise, action.shape).astype(np.float32)
144
+ # Safety bypass: zero out action if ACC signals danger
145
+ if self.cortex.safety_bypass():
146
+ action = np.zeros_like(action)
147
+ action = np.clip(action + noise, -1.0, 1.0)
148
+ # CogCore ε-greedy motor exploration (AntiStasis MotorExplorationScheduler)
149
+ _, explored, _ = self.motor_scheduler.choose(None, [], tick=self._global_tick)
150
+ if explored:
151
+ action = self.rng.uniform(-1.0, 1.0, action.shape).astype(np.float32)
152
+ self._global_tick += 1
153
+ return action, state
154
+
155
+ def store(self, t: Transition):
156
+ self.buffer.push(t)
157
+
158
+ def update(self) -> Optional[float]:
159
+ if len(self.buffer) < self.cfg.batch_size:
160
+ return None
161
+ states, actions, rewards, next_states, dones = \
162
+ self.buffer.sample(self.cfg.batch_size, self.rng)
163
+ next_feats = np.stack([
164
+ np.array(self.head.forward(ns)["features"][:self.cfg.action_dim], np.float32)
165
+ for ns in next_states
166
+ ])
167
+ v_next = np.mean(np.abs(next_feats), axis=1)
168
+ targets = rewards + self.cfg.gamma * v_next * (~dones)
169
+ advs = targets - np.mean(targets)
170
+ tgt_acts = np.clip(
171
+ actions + np.outer(advs, np.ones(self.cfg.action_dim)) * 0.1,
172
+ -1.0, 1.0,
173
+ )
174
+ loss = self.head.train_supervised_step(states, tgt_acts)
175
+ self.noise = max(self.cfg.noise_floor, self.noise * self.cfg.noise_decay)
176
+ return float(loss)
177
+
178
+ def reset_noise_for_task(self, task=None):
179
+ level = getattr(task, "reset_noise", self.cfg.reset_noise)
180
+ self.noise = max(level, self.cfg.noise_floor)
181
+
182
+ def save(self, path: Path):
183
+ np.savez(str(path),
184
+ W1=self.head.W1, b1=self.head.b1,
185
+ W_action=self.head.W_action, b_action=self.head.b_action,
186
+ W_disc=self.head.W_disc, b_disc=self.head.b_disc,
187
+ noise=np.array([self.noise]))
188
+
189
+ def load(self, path: Path):
190
+ w = np.load(str(path))
191
+ self.head.W1 = w["W1"]
192
+ self.head.b1 = w["b1"]
193
+ self.head.W_action = w["W_action"]
194
+ self.head.b_action = w["b_action"]
195
+ self.head.W_disc = w["W_disc"]
196
+ self.head.b_disc = w["b_disc"]
197
+ if "noise" in w:
198
+ self.noise = float(w["noise"][0])
199
+
200
+
201
+ # ── Episode runner ────────────────────────────────────────────────────────────
202
+
203
+ @dataclass
204
+ class EpisodeStats:
205
+ episode: int
206
+ task: str
207
+ steps: int
208
+ total_reward: float
209
+ success: bool
210
+ fallen: bool
211
+ noise: float
212
+ loss: Optional[float]
213
+ duration_s: float
214
+ interesting: List[str] = field(default_factory=list)
215
+
216
+
217
+ def run_episode(agent: PhantomAgent, task: Task, cfg: RLConfig,
218
+ ckpt_dir: Path, episode: int,
219
+ gemini=None,
220
+ command_goal_vec: Optional[np.ndarray] = None) -> EpisodeStats:
221
+ t0 = time.time()
222
+
223
+ world = WorldEnv(seed=episode, terrain_type=task.terrain)
224
+ body_cls = getattr(task, "body_class", VirtualBody)
225
+ body = body_cls(world.phys, origin=None)
226
+ task.reset(body, world)
227
+ agent.reset_memory(task.name)
228
+
229
+ # ── Goal vector (Gemini multimodální → PFC) ────────────────────────────
230
+ # Gemini generuje goal_vec jednou na začátku epizody (cíl se nemění).
231
+ # Bez Gemini API se použije předdefinovaný fallback vektor.
232
+ goal_vec = None
233
+ goal_desc = ""
234
+ if gemini is not None and cfg.use_gemini:
235
+ try:
236
+ from embodied.world_renderer import render_scene
237
+ target_pos = getattr(task, "TARGET_POS", None) or getattr(task, "CHAIR_POS", None)
238
+ # FPV = co agent skutečně vidí ze svých očí; minimap = prostorový kontext
239
+ fpv_img, minimap_img = render_scene(world, body, target_pos=target_pos)
240
+ torso_pos = body.bodies["torso"].position if "torso" in body.bodies else None
241
+ spec, goal_vec = gemini.get_goal(
242
+ f"Proveď úlohu: {task.name}",
243
+ fpv_img=fpv_img,
244
+ minimap_img=minimap_img,
245
+ agent_pos=torso_pos,
246
+ target_pos=target_pos,
247
+ )
248
+ goal_desc = spec.description
249
+ log.debug("[%s] Gemini goal: %s → vec norm=%.2f",
250
+ task.name, goal_desc, float(np.linalg.norm(goal_vec)))
251
+ except Exception as e:
252
+ log.warning("Gemini goal generation selhal: %s", e)
253
+
254
+ if goal_vec is None:
255
+ if command_goal_vec is not None:
256
+ goal_vec = command_goal_vec # language command přepisuje fallback
257
+ else:
258
+ spec = goal_for_task(task.name)
259
+ goal_vec = encode_goal(spec)
260
+ agent.set_goal(goal_vec)
261
+
262
+ total_reward = 0.0
263
+ steps = 0
264
+ last_loss = None
265
+ interesting = []
266
+ last_action = np.zeros(cfg.action_dim, np.float32)
267
+ prev_bstate = body.get_state()
268
+ prev_reward = 0.0
269
+ # CogCore AntiDoNothingController — officially tuned anti-stasis coefficients
270
+ anti_stasis_cfg = AntiStasisConfig(
271
+ hunger_rate=0.0018,
272
+ idle_energy_decay=0.0011,
273
+ action_energy_cost_scale=0.004,
274
+ hunger_penalty_weight=0.20,
275
+ low_energy_penalty_weight=0.10,
276
+ death_penalty=-3.0,
277
+ survival_bonus=0.004,
278
+ curiosity_weight=0.40,
279
+ movement_reward=0.015,
280
+ food_reward=0.50,
281
+ # Boredom snížen 5× — musí být dost na přelomení alive-only (+74),
282
+ # ale nesmí zničit gradient signál z proximity reward
283
+ repeated_position_penalty=-0.030,
284
+ repeated_signature_penalty=-0.008,
285
+ boredom_window=60,
286
+ )
287
+ anti_stasis = AntiDoNothingController(config=anti_stasis_cfg)
288
+ emergent_loop = EmergentCognitiveLoop()
289
+ emergent_loop.reset()
290
+ cog_obs_adapter = EngineCognitiveObservationAdapter()
291
+ prev_visual = None
292
+
293
+ for step in range(task.max_steps):
294
+ action, aug_state = agent.act(prev_bstate, last_action,
295
+ reward=prev_reward, goal_vec=goal_vec)
296
+
297
+ # POLARIS server blend: task může mít polaris_server atribut
298
+ # → část akce pochází z analogového POLARIS čipu (inference v HfO2)
299
+ if getattr(task, "polaris_server", None) is not None:
300
+ _ps = task.polaris_server
301
+ _pid = getattr(task, "polaris_agent_id", "agent")
302
+ _blend = getattr(task, "polaris_blend", 0.25)
303
+ _state_v = aug_state if aug_state is not None else prev_bstate
304
+ pol_action = _ps.infer(_pid, _state_v.astype(np.float32))
305
+ action = np.clip(
306
+ action * (1.0 - _blend) + pol_action * _blend, -1.0, 1.0
307
+ ).astype(np.float32)
308
+ # Online Hebbian update: POLARIS se učí z RL akce
309
+ if step % 4 == 0:
310
+ _ps.learn(_pid, _state_v.astype(np.float32), action)
311
+
312
+ body.apply_action(action)
313
+ world.step(cfg.dt)
314
+ body.update_contacts(ground_y=0.15)
315
+
316
+ bstate = body.get_state()
317
+ # Tasky s vlastní fyzikou (DriveCity, apod.) mohou přepsat state vektor
318
+ if hasattr(task, "get_task_state"):
319
+ bstate = task.get_task_state(bstate)
320
+ task_r = task.step_reward(body, world, action)
321
+
322
+ # CogCore anti-stasis bonus (officially tuned, fights local optimum of doing nothing)
323
+ torso = body.torso_pos()
324
+ body_vel = float(np.linalg.norm([bstate[0], bstate[1]]))
325
+ grasped = bool(action[8] < -0.5 or action[9] < -0.5)
326
+
327
+ # KFP food motivation: task může vrátit food_event (póza = dumpling = metabolismus)
328
+ found_food, nutrition, energy_gain = False, 0.0, 0.0
329
+ if hasattr(task, 'food_event'):
330
+ found_food, nutrition, energy_gain = task.food_event(body, world)
331
+
332
+ # Krmení metabolismu: buď KFP food event, nebo obecná task_r
333
+ if found_food:
334
+ anti_stasis.needs.feed(nutrition=nutrition, energy_gain=energy_gain)
335
+ elif task_r > 0.0:
336
+ anti_stasis.needs.feed(
337
+ nutrition=min(0.50, float(task_r) * 1.5),
338
+ energy_gain=min(0.15, float(task_r) * 0.5),
339
+ )
340
+
341
+ events_list = []
342
+ if grasped:
343
+ events_list.append(EventRecord("INTERACT_SUCCESS", source_id="agent"))
344
+ if body_vel > 0.1:
345
+ events_list.append(EventRecord("MOVE", source_id="agent"))
346
+ obs = EmbodiedObservation(
347
+ agent_id="phantom",
348
+ tick=step,
349
+ position=(int(torso[0]), int(torso[1]), int(torso[2])),
350
+ visual_state=prev_visual,
351
+ predicted_next_visual=prev_visual,
352
+ actual_next_visual=bstate,
353
+ action_id="grasp" if grasped else ("move" if body_vel > 0.1 else "idle"),
354
+ action_cost=float(np.mean(np.abs(action))) * 0.3,
355
+ events=tuple(events_list),
356
+ external_reward=0.0, # task_r zvlášť — žádné dvojité počítání
357
+ found_food=found_food, # KFP: dobrá póza = jídlo = metabolická odměna
358
+ nutrition=nutrition,
359
+ energy_gain=energy_gain,
360
+ )
361
+ report = anti_stasis.step(obs)
362
+ im_bonus = float(report.reward)
363
+
364
+ # EmergentCognitiveLoop — officiální engine.cognitive_observation bridge
365
+ e_cog = cog_obs_adapter.from_mapping({
366
+ "position": (float(torso[0]), float(torso[1]), float(torso[2])),
367
+ "velocity": (float(bstate[0]), 0.0, float(bstate[1])),
368
+ "energy": float(anti_stasis.needs.energy),
369
+ "damage": 1.0 if body.is_fallen() else 0.0,
370
+ "contact": 1.0 if grasped else 0.0,
371
+ "blocked": 1.0 if body_vel < 0.005 else 0.0,
372
+ })
373
+ e_obs = e_cog.to_cogcore_dict()
374
+ e_action = {"control_match": min(1.0, float(task_r + 1.0) / 2.0) if task_r > -1.0 else 0.0}
375
+ e_report = emergent_loop.step(e_obs, action=e_action)
376
+ # planner_bias.caution: jen log pro monitoring — noise_decay řídí hlavní plán
377
+ # Emergent loop nesmí přebít noise_decay schedule, proto nemodifikuje agent.noise
378
+
379
+ prev_visual = bstate
380
+
381
+ # Každých 100 kroků: agent "řekne" co si myslí že dělá (language goal bridge)
382
+ if step % 100 == 0 and step > 0:
383
+ ws_tokens = list(e_report.workspace.get("broadcast", []))[:3] if e_report else []
384
+ thought = goal_to_text(
385
+ goal_vec,
386
+ object_dist=-1.0,
387
+ energy=float(anti_stasis.needs.energy),
388
+ noise=agent.noise,
389
+ workspace_tokens=ws_tokens,
390
+ )
391
+ log.debug("[%s] step %d | %s", task.name, step, thought)
392
+
393
+ reward = task_r + im_bonus
394
+ done = task.is_done(body, world)
395
+ total_reward += reward
396
+ steps += 1
397
+
398
+ _, next_aug = agent.act(bstate, action, reward=reward, goal_vec=goal_vec)
399
+ agent.store(Transition(aug_state, action, reward, next_aug, done))
400
+
401
+ if steps % cfg.train_every == 0:
402
+ loss = agent.update()
403
+ if loss is not None:
404
+ last_loss = loss
405
+
406
+ if reward < -1.5:
407
+ interesting.append(f"fall_penalty@step{step}")
408
+ if task.success_condition(body, world):
409
+ interesting.append(f"SUCCESS@step{step}")
410
+ done = True
411
+
412
+ last_action = action
413
+ prev_bstate = bstate
414
+ prev_reward = reward
415
+ if done:
416
+ break
417
+
418
+ success = task.success_condition(body, world)
419
+ fallen = body.is_fallen()
420
+
421
+ return EpisodeStats(
422
+ episode=episode, task=task.name, steps=steps,
423
+ total_reward=round(total_reward, 4),
424
+ success=success, fallen=fallen,
425
+ noise=round(agent.noise, 4),
426
+ loss=round(last_loss, 6) if last_loss is not None else None,
427
+ duration_s=round(time.time() - t0, 2),
428
+ interesting=interesting,
429
+ )
430
+
431
+
432
+ # ── Curriculum ────────────────────────────────────────────────────────────────
433
+
434
+ def train_curriculum(
435
+ tasks: List[Task],
436
+ episodes_per_task: int,
437
+ cfg: RLConfig,
438
+ ckpt_dir: Path,
439
+ log_path: Path,
440
+ resume: bool = False,
441
+ gemini=None,
442
+ command_goal_vec: Optional[np.ndarray] = None,
443
+ bc_weights_path: str = "",
444
+ ) -> List[EpisodeStats]:
445
+ agent = PhantomAgent(cfg)
446
+
447
+ ckpt = ckpt_dir / "agent_latest.npz"
448
+ if resume and ckpt.exists():
449
+ agent.load(ckpt)
450
+ log.info("Resumed from %s", ckpt)
451
+ elif bc_weights_path:
452
+ # BC předtrénování: načti váhy před RL (jen pokud nezačínáme z RL checkpointu)
453
+ try:
454
+ from embodied.pretrain.bc_pretrain import load_bc_weights_into_agent
455
+ load_bc_weights_into_agent(agent, bc_weights_path)
456
+ log.info("BC předtrénované váhy načteny: %s", bc_weights_path)
457
+ except Exception as e:
458
+ log.warning("BC váhy nelze načíst (%s) — start z náhodné inicializace", e)
459
+
460
+ all_stats: List[EpisodeStats] = []
461
+ ep_global = 0
462
+
463
+ for task in tasks:
464
+ log.info("══ Task: %s (%d episodes, reset_noise=%.2f) ══",
465
+ task.name, episodes_per_task, getattr(task, "reset_noise", cfg.reset_noise))
466
+ # Restore exploration noise for each new task (manipulation tasks use higher noise)
467
+ agent.reset_noise_for_task(task)
468
+ task_rewards = []
469
+
470
+ for ep in range(episodes_per_task):
471
+ stats = run_episode(agent, task, cfg, ckpt_dir, ep_global,
472
+ gemini=gemini, command_goal_vec=command_goal_vec)
473
+ all_stats.append(stats)
474
+ task_rewards.append(stats.total_reward)
475
+ ep_global += 1
476
+
477
+ if True: # loguj každou epizodu pro rychlejší feedback
478
+ avg = np.mean(task_rewards[-5:]) if len(task_rewards) >= 5 else stats.total_reward
479
+ log.info("[%s] ep %d/%d reward=%.2f avg5=%.2f noise=%.3f %s",
480
+ task.name, ep+1, episodes_per_task,
481
+ stats.total_reward, avg, agent.noise,
482
+ "SUCCESS" if stats.success else ("FALLEN" if stats.fallen else ""))
483
+
484
+ with log_path.open("a") as f:
485
+ d = asdict(stats)
486
+ # numpy float32 → Python float pro JSON serializaci
487
+ def _to_python(v):
488
+ if isinstance(v, np.floating): return float(v)
489
+ if isinstance(v, np.integer): return int(v)
490
+ return v
491
+ d = {k: _to_python(v) for k, v in d.items()}
492
+ f.write(json.dumps(d) + "\n")
493
+
494
+ # Průběžný checkpoint každých 10 epizod (ochrana před výpadkem kontejneru)
495
+ if (ep + 1) % 10 == 0:
496
+ agent.save(ckpt)
497
+ agent.save(ckpt_dir / f"agent_{task.name}_ep{ep+1}.npz")
498
+
499
+ agent.save(ckpt)
500
+ agent.save(ckpt_dir / f"agent_{task.name}.npz")
501
+ log.info("Checkpoint saved after task %s", task.name)
502
+
503
+ return all_stats
embodied/run_embodied.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Embodied training: virtual body + Noctilith physics world.
4
+
5
+ Curriculum (default):
6
+ 1. stand – stay upright
7
+ 2. reach – reach a target with the right hand
8
+ 3. walk – walk forward over hills
9
+ 4. stairs – climb a staircase
10
+ 5. maze – navigate a procedural labyrinth
11
+ 6. household_pick – pick a cup off the table, carry to counter
12
+ 7. household_door – push a door handle, walk through
13
+ 8. household_stack – pick up 3 blocks, stack on table
14
+ 9. household_sweep – knock cups off the table
15
+ 10. household_sit – navigate to chair and sit down
16
+
17
+ Usage:
18
+ python -m embodied.run_embodied [--episodes N] [--tasks stand,walk,maze]
19
+ python -m embodied.run_embodied --episodes 40 --tasks household_pick,household_door
20
+ """
21
+ import argparse, logging, json, os, sys, time
22
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
23
+
24
+ from pathlib import Path
25
+ import numpy as np
26
+
27
+ logging.basicConfig(
28
+ level=logging.INFO,
29
+ format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
30
+ datefmt="%H:%M:%S",
31
+ )
32
+ log = logging.getLogger("phantom.embodied")
33
+
34
+ CKPT_DIR = Path(os.getenv("PHANTOM_CKPT", "checkpoints"))
35
+ LOG_DIR = Path(os.getenv("PHANTOM_LOGS", "logs"))
36
+ RES_DIR = Path(os.getenv("PHANTOM_RESULTS", "results"))
37
+ for p in [CKPT_DIR, LOG_DIR, RES_DIR]:
38
+ p.mkdir(parents=True, exist_ok=True)
39
+
40
+ LOG_PATH = LOG_DIR / "embodied_training.jsonl"
41
+
42
+
43
+ TASK_REGISTRY = {
44
+ "stand": ("embodied.tasks.stand", "StandTask"),
45
+ "walk": ("embodied.tasks.walk", "WalkTask"),
46
+ "reach": ("embodied.tasks.reach", "ReachTask"),
47
+ "stairs": ("embodied.tasks.stairs", "StairsTask"),
48
+ "maze": ("embodied.tasks.maze", "MazeTask"),
49
+ "household_pick": ("embodied.tasks.household_pick", "HouseholdPickTask"),
50
+ "household_door": ("embodied.tasks.household_door", "HouseholdDoorTask"),
51
+ "household_stack": ("embodied.tasks.household_stack", "HouseholdStackTask"),
52
+ "household_sweep": ("embodied.tasks.household_sweep", "HouseholdSweepTask"),
53
+ "household_sit": ("embodied.tasks.household_sit", "HouseholdSitTask"),
54
+ # ── Nové interaktivní + mediální úlohy ────────────────────────────────────
55
+ "reach_arm": ("embodied.tasks.reach_arm", "ReachArmTask"),
56
+ # ── Jóga ─────────────────────────────────────────────────────────────────
57
+ "yoga_mountain": ("embodied.tasks.yoga", "YogaMountainTask"),
58
+ "yoga_tree": ("embodied.tasks.yoga", "YogaTreeTask"),
59
+ "yoga_warrior1": ("embodied.tasks.yoga", "YogaWarrior1Task"),
60
+ "yoga_warrior2": ("embodied.tasks.yoga", "YogaWarrior2Task"),
61
+ "yoga_triangle": ("embodied.tasks.yoga", "YogaTriangleTask"),
62
+ "yoga_child": ("embodied.tasks.yoga", "YogaChildTask"),
63
+ "yoga_downdog": ("embodied.tasks.yoga", "YogaDowndogTask"),
64
+ "yoga_cobra": ("embodied.tasks.yoga", "YogaCobraTask"),
65
+ "yoga_all": ("embodied.tasks.yoga", "YogaAllTask"),
66
+ "yoga_random": ("embodied.tasks.yoga", "YogaRandomTask"),
67
+ "household_ball": ("embodied.tasks.household_ball", "BallTask"),
68
+ "shape_sorter": ("embodied.tasks.shape_sorter", "ShapeSorterTask"),
69
+ "media_watch": ("embodied.tasks.media_watch", "MediaWatchTask"),
70
+ # ── Vojenská překážková dráha ─────────────────────────────────────────────
71
+ "obstacle_course": ("embodied.tasks.obstacle_course", "ObstacleCourseTask"),
72
+ # ── Řízení hypercar v městském prostředí ──────────────────────────────────
73
+ "drive_city": ("embodied.tasks.drive_city", "DriveCity"),
74
+ "drive_city_echo": ("embodied.tasks.drive_city_echo", "DriveCity_Echo"),
75
+ # ── Terminator (POLARIS v3.1 + EHA-C Rev C.1) ────────────────────────────
76
+ "terminator_stand": ("embodied.tasks.terminator_stand", "TerminatorStandTask"),
77
+ "terminator_factory": ("embodied.tasks.terminator_factory", "TerminatorFactoryTask"),
78
+ "terminator_obstacle":("embodied.tasks.terminator_factory", "TerminatorObstacleTask"),
79
+ # ── Terminator jemná motorika (nářadí) ───────────────────────────────────
80
+ "terminator_weld": ("embodied.tasks.terminator_tools", "TerminatorWeldTask"),
81
+ "terminator_screw": ("embodied.tasks.terminator_tools", "TerminatorScrewTask"),
82
+ "terminator_assembly":("embodied.tasks.terminator_tools", "TerminatorAssemblyTask"),
83
+ # ── PolarisServer — virtuální neuromorphní server ve VoxForge světě ─────
84
+ "polaris_server": ("embodied.tasks.polaris_server_task","PolarisServerTask"),
85
+ # ── Drive + POLARIS koprocesor ────────────────────────────────────────────
86
+ "drive_city_polaris": ("embodied.tasks.drive_city_polaris", "DriveCity_Polaris"),
87
+ # ── Basketbal ─────────────────────────────────────────────────────────────
88
+ "dribble": ("embodied.tasks.basketball", "DribbleTask"),
89
+ "shoot": ("embodied.tasks.basketball", "ShootTask"),
90
+ "layup": ("embodied.tasks.basketball", "LayupTask"),
91
+ "basketball": ("embodied.tasks.basketball", "BasketballTask"),
92
+ "basketball_1v1": ("embodied.tasks.basketball_1v1", "Basketball1v1Task"),
93
+ "basketball_coop": ("embodied.tasks.basketball_1v1", "BasketballCoopTask"),
94
+ # ── Časovaná navigace (3 módy emergentního chování) ──────────────────────
95
+ "timed_nav_legal": ("embodied.tasks.timed_nav", "TimedNavLegal"),
96
+ "timed_nav_realistic":("embodied.tasks.timed_nav", "TimedNavRealistic"),
97
+ "timed_nav_race": ("embodied.tasks.timed_nav", "TimedNavRace"),
98
+ # ── Jízda na ledu / sněhu (emergentní rally techniky) ────────────────────
99
+ "ice_driving_ice": ("embodied.tasks.ice_driving", "IceDrivingIce"),
100
+ "ice_driving_snow": ("embodied.tasks.ice_driving", "IceDrivingSnow"),
101
+ "ice_driving_black_ice": ("embodied.tasks.ice_driving", "IceDrivingBlackIce"),
102
+ "ice_driving_mixed": ("embodied.tasks.ice_driving", "IceDrivingMixed"),
103
+ # ── Led + POLARIS koprocesor ──────────────────────────────────────────────
104
+ "ice_driving_polaris": ("embodied.tasks.ice_driving_polaris", "IceDriving_Polaris"),
105
+ "ice_driving_ice_polaris": ("embodied.tasks.ice_driving_polaris", "IceDrivingIce_Polaris"),
106
+ "ice_driving_black_ice_polaris": ("embodied.tasks.ice_driving_polaris", "IceDrivingBlackIce_Polaris"),
107
+ }
108
+
109
+
110
+ def _load_task(name: str):
111
+ mod_name, cls_name = TASK_REGISTRY[name]
112
+ import importlib
113
+ mod = importlib.import_module(mod_name)
114
+ return getattr(mod, cls_name)()
115
+
116
+
117
+ def main():
118
+ parser = argparse.ArgumentParser()
119
+ parser.add_argument("--episodes", type=int, default=20)
120
+ parser.add_argument("--tasks",
121
+ default="stand,reach,walk,stairs,maze,"
122
+ "household_pick,household_door,household_stack,"
123
+ "household_sweep,household_sit")
124
+ parser.add_argument("--resume", action="store_true")
125
+ parser.add_argument("--seed", type=int, default=42)
126
+ parser.add_argument("--gemini", action="store_true",
127
+ help="Zapni Gemini multimodální goal generation (vyžaduje GEMINI_API_KEY)")
128
+ parser.add_argument("--command", type=str, default="",
129
+ help="Přirozený jazyk příkaz (CZ/EN) → přepíše goal_vec pro všechny tasky. "
130
+ "Např: 'vezmi hrníček a dej ho na pult'")
131
+ parser.add_argument("--bc-init", type=str, default="",
132
+ help="Cesta k BC předtrénovaným vahám (checkpoints/bc_pretrained.npz). "
133
+ "Načte váhy před RL tréninkem — agent začíná s pohybovými prio ry.")
134
+ parser.add_argument("--bc-steps", type=int, default=0,
135
+ help="Spustit BC předtrénování NYNÍ s tímto počtem kroků (přepíše --bc-init). "
136
+ "Data: /tmp/lafan_walk + /tmp/libero_pick")
137
+ parser.add_argument("--bc-ice", action="store_true",
138
+ help="Spustit BC ICE předtrénování (syntetický rally expert) → checkpoints/bc_ice.npz. "
139
+ "Vhodné pro ice_driving_* tasky. Nevyžaduje žádná externí data.")
140
+ parser.add_argument("--bc-ice-surface", default="all",
141
+ choices=["ice", "snow", "black_ice", "all"],
142
+ help="Povrch pro BC ICE (default: all = mix ledu/sněhu/černého ledu)")
143
+ parser.add_argument("--bc-ice-steps", type=int, default=15000,
144
+ help="Počet kroků BC ICE předtrénování (default: 15000)")
145
+ args = parser.parse_args()
146
+
147
+ from embodied.rl_loop import RLConfig, train_curriculum
148
+ from embodied.observer import load_log, analyse, print_report, save_report
149
+ from embodied.phantom_cortex import CORTEX_DIM
150
+
151
+ task_names = [t.strip() for t in args.tasks.split(",")]
152
+ tasks = [_load_task(n) for n in task_names]
153
+
154
+ cfg = RLConfig(
155
+ state_dim=CORTEX_DIM, action_dim=10,
156
+ hidden_dim=64, lr=0.003,
157
+ gamma=0.99, buffer_cap=8192,
158
+ train_every=16, batch_size=32,
159
+ noise_std=0.20, noise_floor=0.10,
160
+ noise_decay=0.9998, reset_noise=0.20,
161
+ dt=0.016, seed=args.seed,
162
+ use_gemini=args.gemini,
163
+ )
164
+
165
+ # Volitelný Gemini commander (jazyková oblast)
166
+ gemini = None
167
+ if args.gemini:
168
+ try:
169
+ from embodied.gemini_commander import GeminiCommander
170
+ gemini = GeminiCommander(log_path=LOG_PATH)
171
+ log.info("Gemini multimodální goal generation: AKTIVNÍ")
172
+ except Exception as e:
173
+ log.warning("Gemini nedostupný (%s) — trénink pokračuje s fallback goal_vec", e)
174
+
175
+ # ── BC předtrénování / načtení BC vah ────────────────────────────────────
176
+ bc_weights_path = ""
177
+ if args.bc_ice:
178
+ log.info("Spouštím BC ICE předtrénování (syntetický rally expert) "
179
+ "surface=%s steps=%d ...", args.bc_ice_surface, args.bc_ice_steps)
180
+ try:
181
+ from embodied.pretrain.bc_ice_driving import run_ice_bc_pretraining
182
+ run_ice_bc_pretraining(
183
+ surface=args.bc_ice_surface,
184
+ steps=args.bc_ice_steps,
185
+ save_path="checkpoints/bc_ice.npz",
186
+ )
187
+ bc_weights_path = "checkpoints/bc_ice.npz"
188
+ except Exception as e:
189
+ log.warning("BC ICE předtrénování selhalo (%s) — pokračuji bez něj", e)
190
+ elif args.bc_steps > 0:
191
+ log.info("Spouštím BC předtrénování (%d kroků) z LAFAN + LIBERO ...", args.bc_steps)
192
+ try:
193
+ from embodied.pretrain.bc_pretrain import run_bc_pretraining
194
+ run_bc_pretraining(
195
+ lafan_dir="/tmp/lafan_walk",
196
+ libero_dir="/tmp/libero_pick",
197
+ steps=args.bc_steps,
198
+ save_path="checkpoints/bc_pretrained.npz",
199
+ )
200
+ bc_weights_path = "checkpoints/bc_pretrained.npz"
201
+ except Exception as e:
202
+ log.warning("BC předtrénování selhalo (%s) — pokračuji bez něj", e)
203
+ elif args.bc_init:
204
+ bc_weights_path = args.bc_init
205
+
206
+ # Volitelný language command → goal_vec override
207
+ command_goal_vec = None
208
+ if args.command:
209
+ from embodied.language_goal_bridge import text_to_goal
210
+ from embodied.goal_encoder import encode_goal
211
+ spec = text_to_goal(args.command)
212
+ command_goal_vec = encode_goal(spec)
213
+ log.info("Language command: %r → action=%s target=%s dest=%s grip=%+.1f",
214
+ args.command, spec.action, spec.target, spec.destination, spec.grip)
215
+
216
+ log.info("Starting embodied training v4 (PhantomCortex): tasks=%s episodes_each=%d",
217
+ task_names, args.episodes)
218
+ log.info("Engine: VoxForge Noctilith action_dim=10 state_dim=%d "
219
+ "(28 body [+gyro+height] + 8 mem + 32 goal + 5 bio) "
220
+ "cortex=PhantomCortex(4-tier HexaPenta + BioCor + ACC)", CORTEX_DIM)
221
+
222
+ t0 = time.time()
223
+ all_stats = train_curriculum(
224
+ tasks=tasks,
225
+ episodes_per_task=args.episodes,
226
+ cfg=cfg,
227
+ ckpt_dir=CKPT_DIR,
228
+ log_path=LOG_PATH,
229
+ resume=args.resume,
230
+ gemini=gemini,
231
+ command_goal_vec=command_goal_vec,
232
+ bc_weights_path=bc_weights_path,
233
+ )
234
+ elapsed = time.time() - t0
235
+
236
+ log.info("Training complete in %.1fs total_episodes=%d", elapsed, len(all_stats))
237
+
238
+ rows = load_log(LOG_PATH)
239
+ report = analyse(rows)
240
+ report["elapsed_s"] = round(elapsed, 1)
241
+ print_report(report)
242
+ save_report(report, RES_DIR / "embodied_report.json")
243
+
244
+
245
+ if __name__ == "__main__":
246
+ main()
embodied/tasks/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .base import Task, TaskResult
2
+ from .stand import StandTask
3
+ from .walk import WalkTask
4
+ from .maze import MazeTask
5
+ from .reach import ReachTask
6
+ from .stairs import StairsTask
7
+ from .household_pick import HouseholdPickTask
8
+ from .household_door import HouseholdDoorTask
9
+ from .household_stack import HouseholdStackTask
10
+ from .household_sweep import HouseholdSweepTask
11
+ from .household_sit import HouseholdSitTask
12
+ from .obstacle_course import ObstacleCourseTask
embodied/tasks/base.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from typing import Dict, Any
4
+ import numpy as np
5
+ import sys, os
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
7
+ from engine.physics.rigid_body import vec3, Vec3
8
+
9
+
10
+ @dataclass
11
+ class TaskResult:
12
+ success: bool
13
+ reward: float
14
+ steps: int
15
+ info: Dict[str, Any] = field(default_factory=dict)
16
+
17
+
18
+ class Task:
19
+ name = "base"
20
+ terrain = "flat"
21
+ max_steps = 600
22
+ reset_noise: float = 0.15 # noise level restored at each new task in curriculum
23
+
24
+ def reset(self, body, world_env) -> None:
25
+ raise NotImplementedError
26
+
27
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
28
+ raise NotImplementedError
29
+
30
+ def is_done(self, body, world_env) -> bool:
31
+ return body.is_fallen() or world_env._tick >= self.max_steps
32
+
33
+ def success_condition(self, body, world_env) -> bool:
34
+ return False
embodied/tasks/basketball.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Basketbalové tasky pro PhantomCortex agent.
3
+
4
+ DribbleTask — dribling: rytmické odrážení míče pravou rukou
5
+ ShootTask — střelba: hod míče na koš (výška 3.05 m, vzdálenost ~4 m)
6
+ LayupTask — layup: přiběh k obruči + střelba
7
+ BasketballTask — celková úloha: dribling → pohyb → střelba
8
+
9
+ Fyzika míče (zjednodušená):
10
+ ball.pos — 3D pozice míče
11
+ Odraz: ball_y(t+1) = abs(ball_y(t) - BOUNCE_LOSS) pokud u podlahy
12
+ Gravitace: ball_vy -= 9.8 * dt
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import numpy as np
17
+ from .base import Task
18
+ import sys, os
19
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
20
+ from engine.physics.rigid_body import vec3, length
21
+
22
+ _GRAVITY = 9.8
23
+ _BOUNCE = 0.72 # koeficient odrazu
24
+ _HOOP_H = 3.05 # výška obruče [m]
25
+ _HOOP_X = 4.0 # vzdálenost obruče [m]
26
+ _HOOP_R = 0.23 # polomer obruče [m]
27
+ _DRIBBLE_H = 0.12 # výška míče při kontaktu s podlahou
28
+ _HAND_R = 0.25 # dosah ruky pro dotek míče [m]
29
+
30
+
31
+ class _Ball:
32
+ """Jednoduchý míč s gravitací a odrazem."""
33
+ def __init__(self, pos: np.ndarray):
34
+ self.pos = pos.copy().astype(np.float32)
35
+ self.vel = np.zeros(3, dtype=np.float32)
36
+
37
+ def step(self, dt: float):
38
+ self.vel[1] -= _GRAVITY * dt
39
+ self.pos += self.vel * dt
40
+ if self.pos[1] <= _DRIBBLE_H and self.vel[1] < 0:
41
+ self.pos[1] = _DRIBBLE_H
42
+ self.vel[1] *= -_BOUNCE
43
+
44
+ def apply_impulse(self, impulse: np.ndarray):
45
+ self.vel += impulse
46
+
47
+
48
+ class DribbleTask(Task):
49
+ """
50
+ Dribling: agent drží míč na dosah pravé ruky a rytmicky ho odrází.
51
+
52
+ Odměna:
53
+ +2.0 za každý odraz míče od podlahy (ball.pos.y ≤ dribble_h)
54
+ +1.0 za každý kontakt ruky s míčem (distance < HAND_R)
55
+ +0.5 za rytmus (pravidelné odrazy ~0.5 s)
56
+ -2.0 pokud míč odletí > 2 m od agenta (ztráta kontroly)
57
+ alive +0.03 / step
58
+ """
59
+ name = "dribble"
60
+ terrain = "flat"
61
+ max_steps = 1500
62
+ reset_noise = 0.20
63
+
64
+ def reset(self, body, world_env):
65
+ body.reset(vec3(0.0, 2.0, 0.0))
66
+ hp = body.hand_pos("R")
67
+ start = np.array([hp[0], hp[1] - 0.5, hp[2]], dtype=np.float32)
68
+ self._ball = _Ball(start)
69
+ self._bounces = 0
70
+ self._last_bounce = 0
71
+ self._dribble_intervals: list = []
72
+ self._contacts = 0
73
+ self._tick = 0
74
+
75
+ def step_reward(self, body, world_env, action):
76
+ dt = 0.016
77
+ self._tick += 1
78
+
79
+ hp = body.hand_pos("R")
80
+ hand = np.array([hp[0], hp[1], hp[2]], dtype=np.float32)
81
+
82
+ # Kontakt ruky s míčem
83
+ dist = float(np.linalg.norm(hand - self._ball.pos))
84
+ hand_contact = dist < _HAND_R
85
+ if hand_contact:
86
+ self._contacts += 1
87
+ grip = float(action[9])
88
+ # Ruka tlačí dolů (grip < 0 = svírá) → dáme míči impulz dolů
89
+ if grip < -0.1:
90
+ push = np.array([0.0, -4.5 * abs(grip), 0.0], dtype=np.float32)
91
+ self._ball.apply_impulse(push)
92
+ # Ruka pohybuje míčem laterálně
93
+ arm_vel = np.array([action[6] * 0.8, action[7] * 0.5, 0.0], dtype=np.float32)
94
+ self._ball.apply_impulse(arm_vel * 0.3)
95
+
96
+ prev_y = self._ball.pos[1]
97
+ self._ball.step(dt)
98
+
99
+ # Detekce odrazu
100
+ bounce_reward = 0.0
101
+ if prev_y > _DRIBBLE_H and self._ball.pos[1] <= _DRIBBLE_H + 0.02:
102
+ self._bounces += 1
103
+ interval = self._tick - self._last_bounce
104
+ if self._last_bounce > 0:
105
+ self._dribble_intervals.append(interval)
106
+ self._last_bounce = self._tick
107
+ bounce_reward = 2.0
108
+
109
+ # Rytmus bonus (intervaly mezi odrazy jsou konzistentní)
110
+ rhythm_reward = 0.0
111
+ if len(self._dribble_intervals) >= 3:
112
+ std = float(np.std(self._dribble_intervals[-6:]))
113
+ if std < 4.0:
114
+ rhythm_reward = 0.5
115
+
116
+ # Kontakt bonus
117
+ contact_reward = 0.5 if hand_contact else 0.0
118
+
119
+ # Ztráta míče penalty
120
+ agent_pos = np.array([0.0, 0.0, 0.0], dtype=np.float32)
121
+ ball_dist = float(np.linalg.norm(self._ball.pos - agent_pos))
122
+ lost_penalty = -2.0 if ball_dist > 2.5 else 0.0
123
+
124
+ alive = 0.03 if not body.is_fallen() else -2.0
125
+ action_pen = float(np.mean(np.square(action))) * 0.002
126
+
127
+ return bounce_reward + rhythm_reward + contact_reward + lost_penalty + alive - action_pen
128
+
129
+ def success_condition(self, body, world_env) -> bool:
130
+ return self._bounces >= 20
131
+
132
+ def food_event(self, body, world_env) -> tuple:
133
+ if self._ball.pos[1] <= _DRIBBLE_H + 0.05:
134
+ return True, 0.15, 0.06 # odraz = krekr
135
+ return False, 0.0, 0.0
136
+
137
+
138
+ class ShootTask(Task):
139
+ """
140
+ Střelba na koš: agent vhodí míč tak, aby prošel obruční (r=0.23 m, h=3.05 m).
141
+
142
+ Odměna:
143
+ +3.0 za výšku trajektorie (míč dosáhne > 2.5 m)
144
+ +10.0 za průchod obruční (úspěch)
145
+ progressivní: čím blíže k obruči, tím více bodů
146
+ alive +0.02 / step
147
+ """
148
+ name = "shoot"
149
+ terrain = "flat"
150
+ max_steps = 800
151
+ reset_noise = 0.20
152
+
153
+ HOOP_POS = np.array([_HOOP_X, _HOOP_H, 0.0], dtype=np.float32)
154
+
155
+ def reset(self, body, world_env):
156
+ body.reset(vec3(0.0, 2.0, 0.0))
157
+ hp = body.hand_pos("R")
158
+ self._ball = _Ball(np.array([hp[0], hp[1], hp[2]], dtype=np.float32))
159
+ self._shot_taken = False
160
+ self._peak_height = 0.0
161
+ self._scored = False
162
+ self._prev_hoop_dist = float(np.linalg.norm(self._ball.pos - self.HOOP_POS))
163
+
164
+ def step_reward(self, body, world_env, action):
165
+ dt = 0.016
166
+
167
+ hp = body.hand_pos("R")
168
+ hand = np.array([hp[0], hp[1], hp[2]], dtype=np.float32)
169
+ dist = float(np.linalg.norm(hand - self._ball.pos))
170
+
171
+ if not self._shot_taken:
172
+ # Fáze 1: přiblížení ruky k míči
173
+ if dist < _HAND_R:
174
+ # Střelecký pohyb: ruka jde nahoru + vpřed → dáme míči impuls
175
+ shoot_vel = np.array([
176
+ action[6] * 3.5 + 2.0, # vpřed ke koši
177
+ abs(action[7]) * 4.0 + 3.5, # nahoru (výška oblouku)
178
+ action[8] * 0.5,
179
+ ], dtype=np.float32)
180
+ self._ball.vel = shoot_vel
181
+ self._shot_taken = True
182
+
183
+ self._ball.step(dt)
184
+ self._peak_height = max(self._peak_height, float(self._ball.pos[1]))
185
+
186
+ # Výška bonus
187
+ height_reward = 0.0
188
+ if self._ball.pos[1] > 2.5:
189
+ height_reward = 0.3
190
+
191
+ # Přiblížení ke koši
192
+ hoop_dist = float(np.linalg.norm(self._ball.pos - self.HOOP_POS))
193
+ approach = (self._prev_hoop_dist - hoop_dist) * 2.0
194
+ self._prev_hoop_dist = hoop_dist
195
+
196
+ # Skóre: míč prošel obruční
197
+ score_reward = 0.0
198
+ if not self._scored and self._shot_taken:
199
+ horiz_dist = float(np.sqrt(
200
+ (self._ball.pos[0] - _HOOP_X) ** 2 + self._ball.pos[2] ** 2
201
+ ))
202
+ if horiz_dist < _HOOP_R and abs(self._ball.pos[1] - _HOOP_H) < 0.3:
203
+ score_reward = 10.0
204
+ self._scored = True
205
+
206
+ alive = 0.02 if not body.is_fallen() else -2.0
207
+ action_pen = float(np.mean(np.square(action))) * 0.002
208
+
209
+ return height_reward + approach + score_reward + alive - action_pen
210
+
211
+ def success_condition(self, body, world_env) -> bool:
212
+ return self._scored
213
+
214
+ def food_event(self, body, world_env) -> tuple:
215
+ if self._scored:
216
+ return True, 1.0, 0.40 # koš = velký dumpling
217
+ if self._ball.pos[1] > 2.5:
218
+ return True, 0.12, 0.05 # míč letí vysoko → krekr
219
+ return False, 0.0, 0.0
220
+
221
+
222
+ class LayupTask(Task):
223
+ """
224
+ Layup: agent přiběhne k obruči a vystřelí z pohybu.
225
+
226
+ Fáze:
227
+ 1. Přiblížení k obruči (z 6 m na < 2 m) → reward: pohyb k obruči
228
+ 2. Výskok + střelba → reward: ShootTask logika
229
+ """
230
+ name = "layup"
231
+ terrain = "flat"
232
+ max_steps = 1200
233
+ reset_noise = 0.25
234
+
235
+ HOOP_POS = np.array([_HOOP_X, _HOOP_H, 0.0], dtype=np.float32)
236
+
237
+ def reset(self, body, world_env):
238
+ body.reset(vec3(-3.0, 2.0, 0.0)) # start 3 m před košem
239
+ hp = body.hand_pos("R")
240
+ self._ball = _Ball(np.array([hp[0], hp[1], hp[2]], dtype=np.float32))
241
+ self._phase = "approach"
242
+ self._scored = False
243
+ self._bounces = 0
244
+ agent_x = -3.0
245
+ self._prev_dist = float(np.sqrt((agent_x - _HOOP_X)**2 + _HOOP_H**2))
246
+
247
+ def step_reward(self, body, world_env, action):
248
+ dt = 0.016
249
+ hp = body.hand_pos("R")
250
+ hand = np.array([hp[0], hp[1], hp[2]], dtype=np.float32)
251
+ dist = float(np.linalg.norm(hand - self._ball.pos))
252
+
253
+ # Míč sleduje ruku v approach fázi (dribble pohyb)
254
+ if self._phase == "approach":
255
+ self._ball.pos = hand + np.array([0.0, -0.4, 0.0], dtype=np.float32)
256
+ # Vzdálenost od koše (X osa)
257
+ hoop_approach = max(0.0, (self._prev_dist - abs(hand[0] - _HOOP_X)) * 1.5)
258
+ self._prev_dist = abs(hand[0] - _HOOP_X)
259
+ # Přechod na střelbu: ruka blízko obruče
260
+ if hand[0] > (_HOOP_X - 1.8):
261
+ self._phase = "shoot"
262
+ alive = 0.03 if not body.is_fallen() else -2.0
263
+ return hoop_approach + alive
264
+
265
+ # Fáze střelby
266
+ if dist < _HAND_R and self._ball.vel[1] < 1.0:
267
+ shoot_vel = np.array([
268
+ action[6] * 2.0 + 1.5,
269
+ abs(action[7]) * 3.5 + 4.0,
270
+ action[8] * 0.3,
271
+ ], dtype=np.float32)
272
+ self._ball.vel = shoot_vel
273
+
274
+ self._ball.step(dt)
275
+
276
+ score_reward = 0.0
277
+ if not self._scored:
278
+ horiz_dist = float(np.sqrt(
279
+ (self._ball.pos[0] - _HOOP_X)**2 + self._ball.pos[2]**2
280
+ ))
281
+ if horiz_dist < _HOOP_R and abs(self._ball.pos[1] - _HOOP_H) < 0.3:
282
+ score_reward = 15.0
283
+ self._scored = True
284
+
285
+ height_reward = 0.3 if self._ball.pos[1] > 2.5 else 0.0
286
+ alive = 0.02 if not body.is_fallen() else -2.0
287
+ action_pen = float(np.mean(np.square(action))) * 0.002
288
+
289
+ return score_reward + height_reward + alive - action_pen
290
+
291
+ def success_condition(self, body, world_env) -> bool:
292
+ return self._scored
293
+
294
+ def food_event(self, body, world_env) -> tuple:
295
+ if self._scored:
296
+ return True, 1.2, 0.50
297
+ if self._phase == "shoot" and self._ball.pos[1] > 2.5:
298
+ return True, 0.10, 0.04
299
+ return False, 0.0, 0.0
300
+
301
+
302
+ class BasketballTask(Task):
303
+ """
304
+ Kompletní basketbalová úloha: dribling → přesun → střelba.
305
+
306
+ Fáze:
307
+ 1. dribble — 20 odrazů míče
308
+ 2. move — přiblíž se k obruči
309
+ 3. shoot — vystřel na koš
310
+ """
311
+ name = "basketball"
312
+ terrain = "flat"
313
+ max_steps = 2000
314
+ reset_noise = 0.20
315
+
316
+ HOOP_POS = np.array([_HOOP_X, _HOOP_H, 0.0], dtype=np.float32)
317
+
318
+ def reset(self, body, world_env):
319
+ body.reset(vec3(-2.0, 2.0, 0.0))
320
+ hp = body.hand_pos("R")
321
+ self._ball = _Ball(np.array([hp[0], hp[1] - 0.4, hp[2]], dtype=np.float32))
322
+ self._phase = "dribble"
323
+ self._bounces = 0
324
+ self._contacts = 0
325
+ self._scored = False
326
+ self._last_bounce = 0
327
+ self._tick = 0
328
+
329
+ def step_reward(self, body, world_env, action):
330
+ dt = 0.016
331
+ self._tick += 1
332
+
333
+ hp = body.hand_pos("R")
334
+ hand = np.array([hp[0], hp[1], hp[2]], dtype=np.float32)
335
+ dist = float(np.linalg.norm(hand - self._ball.pos))
336
+
337
+ reward = 0.0
338
+
339
+ if self._phase == "dribble":
340
+ # Kontakt + dribble logika
341
+ if dist < _HAND_R:
342
+ grip = float(action[9])
343
+ if grip < -0.1:
344
+ self._ball.apply_impulse(np.array([0.0, -4.0 * abs(grip), 0.0], dtype=np.float32))
345
+
346
+ prev_y = self._ball.pos[1]
347
+ self._ball.step(dt)
348
+
349
+ if prev_y > _DRIBBLE_H and self._ball.pos[1] <= _DRIBBLE_H + 0.02:
350
+ self._bounces += 1
351
+ reward += 1.5
352
+
353
+ if self._bounces >= 12:
354
+ self._phase = "move"
355
+ reward += 5.0 # bonus za dokončení dribble fáze
356
+
357
+ elif self._phase == "move":
358
+ # Míč sleduje ruku
359
+ self._ball.pos = hand + np.array([0.0, -0.3, 0.0], dtype=np.float32)
360
+ approach = max(0.0, (_HOOP_X - hand[0]) * 0.0)
361
+ hoop_x_dist = abs(hand[0] - _HOOP_X)
362
+ reward += max(0.0, (2.5 - hoop_x_dist) * 0.5)
363
+
364
+ if hoop_x_dist < 2.0:
365
+ self._phase = "shoot"
366
+ reward += 3.0
367
+
368
+ elif self._phase == "shoot":
369
+ if dist < _HAND_R and self._ball.vel[1] < 0.5:
370
+ self._ball.vel = np.array([
371
+ action[6] * 2.5 + 1.8,
372
+ abs(action[7]) * 4.0 + 3.8,
373
+ action[8] * 0.3,
374
+ ], dtype=np.float32)
375
+
376
+ self._ball.step(dt)
377
+
378
+ if not self._scored:
379
+ horiz = float(np.sqrt(
380
+ (self._ball.pos[0] - _HOOP_X)**2 + self._ball.pos[2]**2
381
+ ))
382
+ if horiz < _HOOP_R and abs(self._ball.pos[1] - _HOOP_H) < 0.35:
383
+ self._scored = True
384
+ reward += 20.0
385
+
386
+ if self._ball.pos[1] > 2.5:
387
+ reward += 0.3
388
+
389
+ alive = 0.03 if not body.is_fallen() else -2.5
390
+ action_pen = float(np.mean(np.square(action))) * 0.002
391
+ return reward + alive - action_pen
392
+
393
+ def success_condition(self, body, world_env) -> bool:
394
+ return self._scored
395
+
396
+ def food_event(self, body, world_env) -> tuple:
397
+ if self._scored:
398
+ return True, 1.5, 0.60 # koš v plné hře = velký dumpling
399
+ if self._phase == "dribble" and self._ball.pos[1] <= _DRIBBLE_H + 0.05:
400
+ return True, 0.12, 0.05
401
+ if self._phase == "shoot" and self._ball.pos[1] > 2.5:
402
+ return True, 0.15, 0.06
403
+ return False, 0.0, 0.0
embodied/tasks/basketball_1v1.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ┌─────────────────────────────────────────────────────────────────────────┐
2
+ # │ SUBSYSTÉM: PhantomCortex Cognitive Neural Engine — Task Layer │
3
+ # │ Modul: Basketball 1v1 (agent vs NPC) + Basketball Coop (s NPC) │
4
+ # │ Závislost: BasketballNPC (CharacterStudio) + Basketball fyzika │
5
+ # └─────────────────────────────────────────────────────────────────────────┘
6
+ """
7
+ Basketball1v1Task — agent vs AI protihráč (NPC opponent z CharacterStudio).
8
+ BasketballCoopTask — agent + AI spoluhráč (NPC teammate) vs AI bránič.
9
+
10
+ Observation rozšíření:
11
+ Standardní 28D body state + 4D NPC obs → agent "vidí" soupeře/spoluhráče.
12
+ PhantomCortex přijme 28D (city_obs override pro rozšíření stavu nepoužijeme —
13
+ NPC info vstupuje jako reward shaping a reward signal).
14
+
15
+ Reward 1v1:
16
+ +15 za koš agenta
17
+ -10 za koš NPC (soupeř dal koš)
18
+ -5 za ukradení míče NPC
19
+ +3 za úspěšný výstřel i bez koše (výška oblouku)
20
+ +alive 0.02/step
21
+
22
+ Reward Coop:
23
+ +15 za koš agenta
24
+ +8 za koš spoluhráče NPC (agent ho vedl do pozice)
25
+ -10 za koš nepřítele
26
+ +0.5 za přiblížení spoluhráče k obruči
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import math
31
+ import numpy as np
32
+ from .base import Task
33
+
34
+ import sys, os
35
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
36
+ from engine.physics.rigid_body import vec3
37
+
38
+ from .basketball import _Ball, _HOOP_X, _HOOP_H, _HOOP_R, _DRIBBLE_H, _HAND_R
39
+ from .basketball_npc import BasketballNPC
40
+
41
+
42
+ class Basketball1v1Task(Task):
43
+ """
44
+ 1v1: Agent (PhantomCortex) vs NPC protihráč (CharacterStudio).
45
+
46
+ NPC se vygeneruje z kategorie 'basketball_player' s behavior_tags: defend, steal.
47
+ Každá epizoda dostane nového NPC (různý seed → různé dovednosti).
48
+ """
49
+ name = "basketball_1v1"
50
+ terrain = "flat"
51
+ max_steps = 1800
52
+ reset_noise = 0.22
53
+
54
+ HOOP_POS = np.array([_HOOP_X, _HOOP_H, 0.0], dtype=np.float32)
55
+
56
+ def reset(self, body, world_env):
57
+ body.reset(vec3(-1.0, 2.0, 0.0))
58
+ hp = body.hand_pos("R")
59
+ self._ball = _Ball(np.array([hp[0], hp[1] - 0.4, hp[2]], dtype=np.float32))
60
+ self._phase = "dribble"
61
+ self._bounces = 0
62
+ self._scored = False
63
+ self._npc_scored = 0
64
+ self._tick = 0
65
+ self._stole = 0
66
+
67
+ # NPC s náhodným seedem → jiné dovednosti každou epizodu
68
+ npc_seed = int(world_env._tick * 7 + 13) if hasattr(world_env, "_tick") else 42
69
+ self._npc = BasketballNPC(role="opponent", seed=npc_seed % 100)
70
+
71
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
72
+ dt = 0.016
73
+ self._tick += 1
74
+
75
+ hp = body.hand_pos("R")
76
+ hand = np.array([hp[0], hp[1], hp[2]], dtype=np.float32)
77
+ hand_2d = np.array([hp[0], 0.0, hp[2]], dtype=np.float32)
78
+ dist_hand_ball = float(np.linalg.norm(hand - self._ball.pos))
79
+ ball_held = dist_hand_ball < _HAND_R
80
+
81
+ reward = 0.0
82
+
83
+ # ── Fáze dribbling ────────────────────────────────────────────────
84
+ if self._phase == "dribble":
85
+ if ball_held:
86
+ grip = float(action[9])
87
+ if grip < -0.1:
88
+ self._ball.apply_impulse(np.array([0.0, -4.0 * abs(grip), 0.0], np.float32))
89
+ prev_y = self._ball.pos[1]
90
+ self._ball.step(dt)
91
+ if prev_y > _DRIBBLE_H and self._ball.pos[1] <= _DRIBBLE_H + 0.02:
92
+ self._bounces += 1
93
+ reward += 0.8
94
+ if self._bounces >= 8:
95
+ self._phase = "move"
96
+ reward += 3.0
97
+
98
+ # ── Fáze pohyb ke koši ────────────────────────────────────────────
99
+ elif self._phase == "move":
100
+ self._ball.pos = hand + np.array([0.0, -0.3, 0.0], np.float32)
101
+ hoop_dist = abs(hand[0] - _HOOP_X)
102
+ reward += max(0.0, (3.0 - hoop_dist) * 0.3)
103
+ if hoop_dist < 2.5:
104
+ self._phase = "shoot"
105
+ reward += 2.0
106
+
107
+ # ── Fáze střelba ─────────────────────────────────────────────────
108
+ elif self._phase == "shoot":
109
+ if dist_hand_ball < _HAND_R and self._ball.vel[1] < 0.5:
110
+ self._ball.vel = np.array([
111
+ action[6] * 2.5 + 1.8,
112
+ abs(action[7]) * 4.0 + 3.8,
113
+ action[8] * 0.3,
114
+ ], np.float32)
115
+ self._ball.step(dt)
116
+ if self._ball.pos[1] > 2.5:
117
+ reward += 0.3
118
+ if not self._scored:
119
+ horiz = float(np.sqrt(
120
+ (self._ball.pos[0] - _HOOP_X)**2 + self._ball.pos[2]**2
121
+ ))
122
+ if horiz < _HOOP_R and abs(self._ball.pos[1] - _HOOP_H) < 0.35:
123
+ self._scored = True
124
+ reward += 15.0
125
+
126
+ # ── NPC update ────────────────────────────────────────────────────
127
+ agent_pos = np.array([hp[0], 1.0, hp[2]], np.float32)
128
+ npc_result = self._npc.step(agent_pos, self._ball.pos,
129
+ ball_held_by_agent=ball_held, dt=dt)
130
+
131
+ # NPC ukradl míč
132
+ if npc_result["stole"] and not self._scored:
133
+ self._stole += 1
134
+ reward -= 5.0
135
+ self._phase = "dribble" # reset fáze
136
+ self._bounces = 0
137
+
138
+ # NPC dal koš
139
+ if npc_result["scored"]:
140
+ self._npc_scored += 1
141
+ reward -= 10.0
142
+
143
+ alive = 0.02 if not body.is_fallen() else -2.0
144
+ action_pen = float(np.mean(np.square(action))) * 0.002
145
+ return float(reward + alive - action_pen)
146
+
147
+ def is_done(self, body, world_env) -> bool:
148
+ if body.is_fallen():
149
+ return True
150
+ if self._npc_scored >= 3: # soupeř dal 3 koše → prohra
151
+ return True
152
+ return world_env._tick >= self.max_steps
153
+
154
+ def success_condition(self, body, world_env) -> bool:
155
+ return self._scored and self._npc_scored == 0
156
+
157
+ def get_npc_obs(self) -> np.ndarray:
158
+ """4D NPC pozorování pro rozšíření stavu (volitelné)."""
159
+ return self._npc.get_obs() if hasattr(self, "_npc") else np.zeros(4, np.float32)
160
+
161
+
162
+ class BasketballCoopTask(Task):
163
+ """
164
+ Kooperativní basketbal: agent + NPC spoluhráč vs NPC bránič.
165
+
166
+ Agent zodpovídá za dribling + střelbu.
167
+ Spoluhráč NPC běží na volnou pozici a sám může dát koš pokud dostane příležitost.
168
+ Bránič NPC se snaží zablokovat agenta.
169
+ """
170
+ name = "basketball_coop"
171
+ terrain = "flat"
172
+ max_steps = 2000
173
+ reset_noise = 0.22
174
+
175
+ HOOP_POS = np.array([_HOOP_X, _HOOP_H, 0.0], dtype=np.float32)
176
+
177
+ def reset(self, body, world_env):
178
+ body.reset(vec3(-2.0, 2.0, 0.0))
179
+ hp = body.hand_pos("R")
180
+ self._ball = _Ball(np.array([hp[0], hp[1] - 0.35, hp[2]], np.float32))
181
+ self._phase = "dribble"
182
+ self._bounces = 0
183
+ self._agent_scored = False
184
+ self._npc_scored = 0
185
+ self._enemy_scored = 0
186
+ self._tick = 0
187
+
188
+ seed = getattr(world_env, "_tick", 0) % 200
189
+ self._teammate = BasketballNPC(role="teammate", seed=seed)
190
+ self._opponent = BasketballNPC(role="opponent", seed=seed + 50,
191
+ skill_override={"defense": 0.55, "agility": 0.60})
192
+
193
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
194
+ dt = 0.016
195
+ self._tick += 1
196
+
197
+ hp = body.hand_pos("R")
198
+ hand = np.array([hp[0], hp[1], hp[2]], np.float32)
199
+ dist_hand_ball = float(np.linalg.norm(hand - self._ball.pos))
200
+ ball_held = dist_hand_ball < _HAND_R
201
+
202
+ reward = 0.0
203
+
204
+ # ── Dribbling ─────────────────────────────────────────────────────
205
+ if self._phase == "dribble":
206
+ if ball_held and float(action[9]) < -0.1:
207
+ self._ball.apply_impulse(np.array([0.0, -4.0 * abs(float(action[9])), 0.0], np.float32))
208
+ prev_y = self._ball.pos[1]
209
+ self._ball.step(dt)
210
+ if prev_y > _DRIBBLE_H and self._ball.pos[1] <= _DRIBBLE_H + 0.02:
211
+ self._bounces += 1
212
+ reward += 0.5
213
+ if self._bounces >= 10:
214
+ self._phase = "move"
215
+ reward += 2.0
216
+
217
+ # ── Pohyb ─────────────────────────────────────────────────────────
218
+ elif self._phase == "move":
219
+ self._ball.pos = hand + np.array([0.0, -0.3, 0.0], np.float32)
220
+ hoop_dist = abs(hand[0] - _HOOP_X)
221
+ reward += max(0.0, (3.0 - hoop_dist) * 0.25)
222
+ if hoop_dist < 2.5:
223
+ self._phase = "shoot"
224
+
225
+ # ── Střelba ───────────────────────────────────────────────────────
226
+ elif self._phase == "shoot":
227
+ if dist_hand_ball < _HAND_R and self._ball.vel[1] < 0.5:
228
+ self._ball.vel = np.array([
229
+ action[6] * 2.5 + 1.8,
230
+ abs(action[7]) * 4.0 + 3.8,
231
+ action[8] * 0.3,
232
+ ], np.float32)
233
+ self._ball.step(dt)
234
+ if self._ball.pos[1] > 2.5:
235
+ reward += 0.2
236
+ if not self._agent_scored:
237
+ horiz = float(np.sqrt(
238
+ (self._ball.pos[0] - _HOOP_X)**2 + self._ball.pos[2]**2
239
+ ))
240
+ if horiz < _HOOP_R and abs(self._ball.pos[1] - _HOOP_H) < 0.35:
241
+ self._agent_scored = True
242
+ reward += 15.0
243
+
244
+ # ── NPC update ────────────────────────────────────────────────────
245
+ agent_pos_3d = np.array([hp[0], 1.0, hp[2]], np.float32)
246
+
247
+ # Spoluhráč
248
+ tm_result = self._teammate.step(agent_pos_3d, self._ball.pos,
249
+ ball_held_by_agent=ball_held, dt=dt)
250
+ if tm_result["scored"]:
251
+ self._npc_scored += 1
252
+ reward += 8.0 # spoluhráčův koš = bonus pro agenta
253
+
254
+ # Bonus za vedení spoluhráče do dobré pozice
255
+ tm_pos = tm_result["pos"]
256
+ tm_hoop_dist = math.hypot(float(tm_pos[0]) - _HOOP_X, float(tm_pos[2]))
257
+ if tm_hoop_dist < 3.0 and tm_result["mode"] == "OPEN_RUN":
258
+ reward += 0.5
259
+
260
+ # Soupeř
261
+ opp_result = self._opponent.step(agent_pos_3d, self._ball.pos,
262
+ ball_held_by_agent=ball_held, dt=dt)
263
+ if opp_result["stole"]:
264
+ reward -= 5.0
265
+ self._phase = "dribble"
266
+ self._bounces = 0
267
+ if opp_result["scored"]:
268
+ self._enemy_scored += 1
269
+ reward -= 10.0
270
+
271
+ alive = 0.02 if not body.is_fallen() else -2.0
272
+ action_pen = float(np.mean(np.square(action))) * 0.002
273
+ return float(reward + alive - action_pen)
274
+
275
+ def is_done(self, body, world_env) -> bool:
276
+ if body.is_fallen():
277
+ return True
278
+ if self._enemy_scored >= 3:
279
+ return True
280
+ return world_env._tick >= self.max_steps
281
+
282
+ def success_condition(self, body, world_env) -> bool:
283
+ total_scored = int(self._agent_scored) + self._npc_scored
284
+ return total_scored >= 2 and self._enemy_scored == 0
embodied/tasks/basketball_npc.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ┌─────────────────────────────────────────────────────────────────────────┐
2
+ # │ SUBSYSTÉM: VoxForge 3D Voxel Engine — Character Studio + NPC AI │
3
+ # │ Modul: Basketball NPC (protihráč / spoluhráč) │
4
+ # │ Závislost: CharacterStudio blueprint → PhantomCortex Task Layer │
5
+ # └─────────────────────────────────────────────────────────────────────────┘
6
+ """
7
+ BasketballNPC — AI hráč generovaný přes VoxForge Character Studio.
8
+
9
+ Kategorie "basketball_player" (registrovaná v CharacterStudio):
10
+ skills: shooting, defense, agility, stamina
11
+ behavior_tags: dribble, defend, shoot, rebound
12
+
13
+ Dva módy:
14
+ role="opponent" — brání obruč, snaží se ukrást míč
15
+ role="teammate" — přibíhá za otevřenou pozicí, signalizuje nahrávku
16
+
17
+ NPC fyzika (zjednodušená — bez VirtualBody rigidní fyziky):
18
+ - Pozice (x, z) na hřišti, pohybuje se po ploše
19
+ - State machine: IDLE → DEFEND → CHASE → SHOOT → REBOUND
20
+ - Detekce dosahu míče (steal radius) a košového dosahu (shoot radius)
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import math
25
+ import random
26
+ from dataclasses import dataclass, field
27
+ from typing import Optional, Tuple
28
+
29
+ import numpy as np
30
+
31
+ import sys, os
32
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
33
+
34
+ from engine.character_studio.studio import CharacterStudio, CharacterStudioConfig
35
+ from engine.character_studio.categories import (
36
+ CharacterCategory,
37
+ register_custom_character_category,
38
+ )
39
+
40
+ # ── Registrace basketball kategorie v CharacterStudio ─────────────────────
41
+
42
+ _BASKETBALL_CATEGORY = CharacterCategory(
43
+ category_id = "basketball_player",
44
+ name = "Basketball Player",
45
+ description = "Athletic MPC for basketball court — can defend, shoot and rebound.",
46
+ default_species= "humans",
47
+ default_district="sports_district",
48
+ behavior_tags = ("dribble", "defend", "shoot", "rebound", "teamplay"),
49
+ skill_bias = {
50
+ "shooting": 0.72,
51
+ "defense": 0.68,
52
+ "agility": 0.80,
53
+ "stamina": 0.75,
54
+ "teamplay": 0.65,
55
+ },
56
+ schedule = ("warmup", "play", "halftime", "play", "cooldown"),
57
+ city_category_affinity=("classic_city", "cybercity"),
58
+ equipment_tags = ("basketball_shoes", "jersey"),
59
+ custom = True,
60
+ )
61
+
62
+
63
+ def _ensure_category_registered():
64
+ """Zaregistruj basketball kategorii při prvním importu."""
65
+ try:
66
+ register_custom_character_category(_BASKETBALL_CATEGORY)
67
+ except Exception:
68
+ pass # Může být již zaregistrována
69
+
70
+
71
+ _ensure_category_registered()
72
+
73
+
74
+ # ── NPC konstanty ─────────────────────────────────────────────────────────
75
+
76
+ _COURT_X_MIN, _COURT_X_MAX = 0.0, 14.0 # šířka hřiště [m]
77
+ _COURT_Z_MIN, _COURT_Z_MAX = -7.5, 7.5 # hloubka hřiště [m]
78
+ _HOOP_X = 4.0 # X pozice obruče (koš agenta)
79
+ _HOOP_Z = 0.0
80
+ _NPC_HOOP_X= -4.0 # NPC koš (na opačné straně)
81
+
82
+ _STEAL_RADIUS = 0.6 # m — dosah ukradení míče
83
+ _SHOOT_RADIUS = 3.5 # m — dosah střelby
84
+ _DEFEND_DIST = 2.0 # m — vzdálenost obrany (mezi agentem a košem)
85
+
86
+
87
+ @dataclass
88
+ class NPCState:
89
+ x: float = 6.0 # pozice X na hřišti
90
+ z: float = 0.0 # pozice Z
91
+ vx: float = 0.0
92
+ vz: float = 0.0
93
+ mode: str = "DEFEND" # IDLE | DEFEND | CHASE | SHOOT | REBOUND
94
+ stamina: float = 1.0
95
+ shoot_cooldown: int = 0
96
+ shoot_success_prob: float = 0.45
97
+
98
+
99
+ class BasketballNPC:
100
+ """
101
+ AI basketbalový hráč vygenerovaný z CharacterStudio blueprintu.
102
+
103
+ Args:
104
+ role: "opponent" nebo "teammate"
105
+ seed: seed pro RNG
106
+ skill_override: dict přepíše dovednosti z blueprintu
107
+ """
108
+
109
+ def __init__(self, role: str = "opponent", seed: int = 0,
110
+ skill_override: Optional[dict] = None):
111
+ self.role = role
112
+ self.rng = random.Random(seed)
113
+ self.np_rng = np.random.default_rng(seed)
114
+
115
+ # Vygeneruj blueprint přes CharacterStudio
116
+ studio = CharacterStudio(CharacterStudioConfig(seed=seed))
117
+ self.blueprint = studio.create_mpc(
118
+ "basketball_player",
119
+ display_name=f"NPC-{role.upper()}-{seed:03d}",
120
+ custom_data={"role": role},
121
+ )
122
+
123
+ # Dovednosti z blueprintu
124
+ skills = dict(self.blueprint.skills)
125
+ if skill_override:
126
+ skills.update(skill_override)
127
+ self.shooting = float(skills.get("shooting", 0.60))
128
+ self.defense = float(skills.get("defense", 0.60))
129
+ self.agility = float(skills.get("agility", 0.70))
130
+ self.stamina_max = float(skills.get("stamina", 0.75))
131
+
132
+ # Fyzický stav
133
+ start_x = 8.0 if role == "opponent" else 2.0
134
+ self.state = NPCState(
135
+ x=start_x, z=0.0,
136
+ shoot_success_prob=0.25 + self.shooting * 0.5,
137
+ )
138
+ self.max_speed = 2.5 + self.agility * 2.5 # m/step @ 16ms → ~1.5–4 m/s
139
+
140
+ # ── Hlavní update krok ────────────────────────────────────────────────
141
+
142
+ def step(self, agent_pos: np.ndarray, ball_pos: np.ndarray,
143
+ ball_held_by_agent: bool, dt: float = 0.016) -> dict:
144
+ """
145
+ Jeden simulační krok NPC.
146
+
147
+ Args:
148
+ agent_pos: [x, y, z] agenta
149
+ ball_pos: [x, y, z] míče
150
+ ball_held_by_agent: True pokud agent drží míč
151
+ dt: krok simulace [s]
152
+
153
+ Returns:
154
+ dict s: pos, mode, stole_ball, scored
155
+ """
156
+ s = self.state
157
+ ax, az = float(agent_pos[0]), float(agent_pos[2])
158
+ bx, bz = float(ball_pos[0]), float(ball_pos[2])
159
+
160
+ stole = False
161
+ scored = False
162
+
163
+ if s.shoot_cooldown > 0:
164
+ s.shoot_cooldown -= 1
165
+
166
+ # Stamina odpadá pomalu
167
+ s.stamina = max(0.2, s.stamina - 0.0001)
168
+
169
+ # ── Stavový automat ───────────────────────────────────────────────
170
+ if self.role == "opponent":
171
+ stole, scored = self._opponent_step(ax, az, bx, bz, ball_held_by_agent, dt)
172
+ else:
173
+ stole, scored = self._teammate_step(ax, az, bx, bz, ball_held_by_agent, dt)
174
+
175
+ # Ohraničení hřiště
176
+ s.x = max(_COURT_X_MIN, min(_COURT_X_MAX, s.x))
177
+ s.z = max(_COURT_Z_MIN, min(_COURT_Z_MAX, s.z))
178
+
179
+ return {
180
+ "pos": np.array([s.x, 1.0, s.z], dtype=np.float32),
181
+ "mode": s.mode,
182
+ "stole": stole,
183
+ "scored": scored,
184
+ }
185
+
186
+ # ── Protihráč AI ──────────────────────────────────────────────────────
187
+
188
+ def _opponent_step(self, ax, az, bx, bz, ball_held: bool, dt: float):
189
+ s = self.state
190
+ stole = scored = False
191
+
192
+ dist_to_ball = math.hypot(s.x - bx, s.z - bz)
193
+ dist_to_agent = math.hypot(s.x - ax, s.z - az)
194
+
195
+ if ball_held:
196
+ # Bran agenta: jdi mezi agenta a koš
197
+ mid_x = (ax + _HOOP_X) / 2.0
198
+ mid_z = az * 0.6
199
+ s.mode = "DEFEND"
200
+ self._move_toward(mid_x, mid_z, dt, speed_frac=0.85)
201
+
202
+ # Pokus ukrást míč
203
+ if dist_to_agent < _STEAL_RADIUS:
204
+ steal_chance = self.defense * 0.15 * s.stamina
205
+ if self.rng.random() < steal_chance:
206
+ stole = True
207
+ s.mode = "SHOOT"
208
+ else:
209
+ # Míč volný → chase
210
+ if dist_to_ball < _SHOOT_RADIUS and s.shoot_cooldown == 0:
211
+ s.mode = "SHOOT"
212
+ scored = self._attempt_shot()
213
+ s.shoot_cooldown = 45
214
+ else:
215
+ s.mode = "CHASE"
216
+ self._move_toward(bx, bz, dt, speed_frac=1.0)
217
+
218
+ return stole, scored
219
+
220
+ # ── Spoluhráč AI ──────────────────────────────────────────────────────
221
+
222
+ def _teammate_step(self, ax, az, bx, bz, ball_held: bool, dt: float):
223
+ s = self.state
224
+ stole = scored = False
225
+
226
+ dist_to_hoop = math.hypot(s.x - _HOOP_X, s.z - _HOOP_Z)
227
+
228
+ if ball_held:
229
+ # Agent drží → běžím na otevřenou pozici u koše
230
+ open_x = _HOOP_X - 1.5 + self.rng.uniform(-0.5, 0.5)
231
+ open_z = self.rng.uniform(-1.5, 1.5)
232
+ s.mode = "OPEN_RUN"
233
+ self._move_toward(open_x, open_z, dt, speed_frac=0.9)
234
+ else:
235
+ # Míč volný → pomoz agentovi
236
+ if dist_to_hoop < _SHOOT_RADIUS and s.shoot_cooldown == 0:
237
+ s.mode = "SHOOT"
238
+ scored = self._attempt_shot()
239
+ s.shoot_cooldown = 60
240
+ else:
241
+ s.mode = "REBOUND"
242
+ self._move_toward(bx, bz, dt, speed_frac=0.75)
243
+
244
+ return stole, scored
245
+
246
+ # ── Pohyb ─────────────────────────────────────────────────────────────
247
+
248
+ def _move_toward(self, tx: float, tz: float, dt: float, speed_frac: float = 1.0):
249
+ s = self.state
250
+ dx = tx - s.x
251
+ dz = tz - s.z
252
+ dist = math.hypot(dx, dz)
253
+ if dist < 0.05:
254
+ return
255
+ speed = self.max_speed * speed_frac * s.stamina * dt
256
+ step = min(speed, dist)
257
+ s.x += (dx / dist) * step
258
+ s.z += (dz / dist) * step
259
+
260
+ # ── Střelba ───────────────────────────────────────────────────────────
261
+
262
+ def _attempt_shot(self) -> bool:
263
+ dist_to_hoop = math.hypot(self.state.x - _HOOP_X, self.state.z - _HOOP_Z)
264
+ dist_factor = max(0.0, 1.0 - dist_to_hoop / (_SHOOT_RADIUS * 1.5))
265
+ prob = self.state.shoot_success_prob * dist_factor * self.state.stamina
266
+ return self.rng.random() < prob
267
+
268
+ # ── Diagnostika ───────────────────────────────────────────────────────
269
+
270
+ def get_obs(self) -> np.ndarray:
271
+ """4D pozorování NPC pro agenta: [rel_x, rel_z, mode_enc, stamina]."""
272
+ mode_enc = {"IDLE": 0.0, "DEFEND": 0.25, "CHASE": 0.5,
273
+ "SHOOT": 0.75, "REBOUND": 1.0, "OPEN_RUN": 0.6}.get(
274
+ self.state.mode, 0.0)
275
+ return np.array([
276
+ self.state.x / 14.0, # norm. X pozice
277
+ self.state.z / 7.5, # norm. Z pozice
278
+ mode_enc, # mód NPC
279
+ self.state.stamina, # únava
280
+ ], dtype=np.float32)
281
+
282
+ def reset(self):
283
+ start_x = 8.0 if self.role == "opponent" else 2.0
284
+ self.state = NPCState(
285
+ x=start_x, z=0.0,
286
+ shoot_success_prob=0.25 + self.shooting * 0.5,
287
+ )
embodied/tasks/drive_city.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DriveCity — RL task pro jízdu hypercar v městském prostředí.
3
+
4
+ Mapa: Procedurálně generovaná síť 110 km silnic (CityGenerator):
5
+ - Centrum: hustá síť 68 km ulic (50–70 km/h), 21 semaforů, 5 kruháčů
6
+ - Boulevard ring: okružní třída 26 km (70 km/h)
7
+ - Dálnice D1: 9 km (130 km/h), nájezdy/výjezdy, mýtnice
8
+ - Tunely: 3 podzemní úseky (80 km/h)
9
+ - Trasa: 23 km přes 28 waypointů
10
+
11
+ Odměnová funkce:
12
+ +progress_reward : postup po trase (per-krok)
13
+ +waypoint_bonus : dosažení průjezdního bodu (+5.0)
14
+ +speed_reward : udržování cílové rychlosti v dané zóně
15
+ +tunnel_bonus : průjezd tunelem bez incidentu (+3.0)
16
+ +speed_bump_regen: EM tlumiče rekuperují energii z nárazu na retardér
17
+ -off_road_pen : uzávěrka / mimo vozovku
18
+ -red_light_pen : průjezd na červenou (-8.0)
19
+ -crash_pen : havárie / převrácení (-20.0)
20
+ +traffic_flow : plynulé projíždění provozu bez kolizí
21
+ -traffic_crash : náraz do vozidla provozu (-25.0 až -40.0 dle rychlosti)
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import math
26
+ import random
27
+ from dataclasses import dataclass, field
28
+ from typing import Dict, List, Optional, Tuple
29
+
30
+ import numpy as np
31
+
32
+ from embodied.tasks.base import Task
33
+ from embodied.vehicle_body import VehicleBody
34
+ from engine.city_builder.traffic import IntersectionNetwork, TrafficLight
35
+ from engine.city_builder.city_gen import CityGenerator, build_traffic_network, build_long_route
36
+ from engine.city_builder.road_network import RoadNetwork
37
+ from engine.city_builder.traffic_sim import TrafficSimulation
38
+
39
+
40
+ # ── Definice trasy ────────────────────────────────────────────────────────────
41
+
42
+ @dataclass
43
+ class Waypoint:
44
+ x: float
45
+ z: float
46
+ speed_limit_kmh: float = 80.0
47
+ is_intersection: bool = False
48
+ light_id: Optional[str] = None
49
+ road_width_m: float = 7.0
50
+ label: str = ""
51
+
52
+ def dist_to(self, x: float, z: float) -> float:
53
+ return math.sqrt((self.x - x) ** 2 + (self.z - z) ** 2)
54
+
55
+
56
+ def _build_city_route(road_net: Optional[RoadNetwork] = None) -> List[Waypoint]:
57
+ """23 km trasa přes celou síť — 28 waypointů."""
58
+ if road_net is not None:
59
+ raw = build_long_route(road_net)
60
+ else:
61
+ raw = build_long_route(CityGenerator(seed=42).generate())
62
+ return [
63
+ Waypoint(
64
+ x=wp["x"], z=wp["z"],
65
+ speed_limit_kmh=wp["speed_limit_kmh"],
66
+ is_intersection=wp["is_intersection"],
67
+ light_id=wp["light_id"],
68
+ road_width_m=wp["road_width_m"],
69
+ label=wp["label"],
70
+ )
71
+ for wp in raw
72
+ ]
73
+
74
+
75
+ # ── Uzávěry dopravy ───────────────────────────────────────────────────────────
76
+
77
+ @dataclass
78
+ class RoadClosure:
79
+ x: float
80
+ z: float
81
+ radius_m: float = 12.0
82
+ label: str = ""
83
+ active: bool = True
84
+
85
+ def blocks(self, x: float, z: float) -> bool:
86
+ return self.active and math.sqrt((x - self.x) ** 2 + (z - self.z) ** 2) < self.radius_m
87
+
88
+
89
+ def _random_closures(route: List[Waypoint], rng: random.Random,
90
+ road_net: Optional[RoadNetwork] = None) -> List[RoadClosure]:
91
+ """Uzávěrky: 2–4 z trasy + navíc ze silniční sítě (pokud dostupná)."""
92
+ candidates = route[2:-2]
93
+ k = min(rng.randint(2, 4), len(candidates))
94
+ chosen = rng.sample(candidates, k=k)
95
+ closures = [RoadClosure(wp.x + rng.uniform(-15, 15), wp.z + rng.uniform(-15, 15),
96
+ radius_m=10.0, label=f"closure_{wp.label}") for wp in chosen]
97
+ # Přidej uzávěry z road_network (předem generované)
98
+ if road_net:
99
+ for seg in road_net.segments.values():
100
+ if "closure" in seg.features:
101
+ mx, mz = seg.midpoint()
102
+ closures.append(RoadClosure(mx, mz, radius_m=12.0, label=f"closure_{seg.seg_id}"))
103
+ return closures
104
+
105
+
106
+ # ── Pomocné funkce ────────────────────────────────────────────────────────────
107
+
108
+ def _heading_to_waypoint(px: float, pz: float, wp: Waypoint) -> float:
109
+ """Úhel (rad) od aktuální pozice k cílovému bodu."""
110
+ return math.atan2(wp.z - pz, wp.x - px)
111
+
112
+
113
+ def _angular_error(heading: float, target_heading: float) -> float:
114
+ """Normalizovaná úhlová odchylka v [-π, π]."""
115
+ err = (target_heading - heading) % (2 * math.pi)
116
+ if err > math.pi:
117
+ err -= 2 * math.pi
118
+ return err
119
+
120
+
121
+ # ── DriveCity Task ────────────────────────────────────────────────────────────
122
+
123
+ class DriveCity(Task):
124
+ """
125
+ Tréninkový task pro jízdu hypercar v městském prostředí.
126
+
127
+ Observation (ze strany rl_loop.py):
128
+ Mimo standardní body_state[73] rl_loop předá DriveCity jako
129
+ external_task_obs: 18D telemetrie VehicleBody + 8D city context.
130
+ Celkem 26D → přidáno do goal_vec jako extended obs.
131
+ """
132
+
133
+ name = "drive_city"
134
+ terrain = "city"
135
+ max_steps = 3000
136
+ reset_noise: float = 0.15
137
+
138
+ WAYPOINT_RADIUS = 22.0
139
+ OFF_ROAD_PENALTY = -5.0
140
+ RED_LIGHT_PENALTY = -8.0
141
+ CRASH_PENALTY = -20.0
142
+ WAYPOINT_BONUS = 5.0
143
+ ALIVE_BONUS = 0.05
144
+ REGEN_SCALE = 0.002
145
+ PROGRESS_SCALE = 0.04
146
+ SPEED_REWARD_SCALE = 0.015
147
+
148
+ def __init__(self, seed: int = 0, city_seed: int = 42,
149
+ traffic_density: float = 1.0):
150
+ self._rng = random.Random(seed)
151
+ self._road_net = CityGenerator(seed=city_seed, closure_count=6).generate()
152
+ self._route = _build_city_route(self._road_net)
153
+ self._network = build_traffic_network(self._road_net)
154
+ self._traffic = TrafficSimulation(
155
+ self._road_net, density_scale=traffic_density, seed=seed, max_cars=120)
156
+ self._traffic.set_traffic_lights(self._network)
157
+ self._closures: List[RoadClosure] = []
158
+ self._wp_idx = 0
159
+ self._sim_time = 0.0
160
+ self._vehicle = VehicleBody.phantom_vertex()
161
+ self._last_dist = 0.0
162
+ self._done = False
163
+ self._success = False
164
+ self._episode_regen = 0.0
165
+ self._in_tunnel = False
166
+ self._traffic_collisions = 0
167
+
168
+ # ── Task interface ────────────────────────────────────────────────────────
169
+
170
+ def reset(self, body, world_env) -> None:
171
+ start = self._route[0]
172
+ self._vehicle.reset(position=(start.x, 0.0, start.z), heading_rad=0.0)
173
+ self._wp_idx = 1
174
+ self._sim_time = 0.0
175
+ self._done = False
176
+ self._success = False
177
+ self._episode_regen = 0.0
178
+ self._closures = _random_closures(self._route, self._rng, self._road_net)
179
+ self._in_tunnel = False
180
+ self._last_dist = self._route[1].dist_to(start.x, start.z)
181
+ self._traffic_collisions = 0
182
+ self._last_threat = None
183
+ self._last_rays = None
184
+ self._traffic.reset()
185
+
186
+ # Reset semaforů — náhodný offset pro různorodost
187
+ for lt in self._network.lights.values():
188
+ lt.timer_s = self._rng.uniform(0.0, lt.cycle_s)
189
+
190
+ # Řidič: krátký burst dopředu → vozidlo nezačíná úplně stojí
191
+ self._vehicle.step(0.12, 0.0, 0.0)
192
+
193
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
194
+ """
195
+ Provede fyzikální krok vozidla a vrátí skalar odměny.
196
+
197
+ action[0] = throttle (-1 až +1, kladné = plyn)
198
+ action[1] = brake (0 až 1)
199
+ action[2] = steer (-1 až +1)
200
+ """
201
+ throttle = float(np.clip(action[0], 0.0, 1.0))
202
+ brake = float(np.clip(-min(action[0], 0.0), 0.0, 1.0))
203
+ steer = float(np.clip(action[2] if len(action) > 2 else 0.0, -1.0, 1.0))
204
+
205
+ tel = self._vehicle.step(throttle, brake, steer)
206
+ self._sim_time += self._vehicle.DT
207
+
208
+ px, _, pz = self._vehicle.position()
209
+ reward = self.ALIVE_BONUS
210
+
211
+ # ── Postup k cílovému bodu ────────────────────────────────────────────
212
+ if self._wp_idx < len(self._route):
213
+ wp = self._route[self._wp_idx]
214
+ dist = wp.dist_to(px, pz)
215
+ progress = (self._last_dist - dist) * self.PROGRESS_SCALE
216
+ reward += max(-0.5, progress)
217
+ self._last_dist = dist
218
+
219
+ # Rychlostní zóna
220
+ target_speed = wp.speed_limit_kmh
221
+ speed_err = abs(tel.speed_kmh - target_speed) / max(target_speed, 1.0)
222
+ reward += self.SPEED_REWARD_SCALE * max(0.0, 1.0 - speed_err)
223
+
224
+ # Dosažení průjezdního bodu
225
+ if dist < self.WAYPOINT_RADIUS:
226
+ reward += self.WAYPOINT_BONUS
227
+ self._wp_idx += 1
228
+ if self._wp_idx >= len(self._route):
229
+ self._success = True
230
+ self._done = True
231
+ reward += 50.0
232
+ else:
233
+ self._last_dist = self._route[self._wp_idx].dist_to(px, pz)
234
+
235
+ # ── Semafory ──────────────────────────────────────────────────────────
236
+ self._network.step(self._sim_time, self._vehicle.DT)
237
+ wp_now = self._route[min(self._wp_idx, len(self._route) - 1)]
238
+ if wp_now.is_intersection and wp_now.light_id:
239
+ lt = self._network.lights.get(wp_now.light_id)
240
+ if lt:
241
+ dist_to_light = wp_now.dist_to(px, pz)
242
+ if dist_to_light < 18.0:
243
+ direction = "NS" if abs(wp_now.z - pz) > abs(wp_now.x - px) else "EW"
244
+ if not lt.allows(direction) and tel.speed_kmh > 8.0:
245
+ reward += self.RED_LIGHT_PENALTY
246
+
247
+ # ── Uzávěry dopravy ───────────────────────────────────────────────────
248
+ for closure in self._closures:
249
+ if closure.blocks(px, pz):
250
+ reward += self.OFF_ROAD_PENALTY * 0.5
251
+
252
+ # ── Tunel ─────────────────────────────────────────────────────────────
253
+ seg = self._road_net.segment_at(px, pz)
254
+ if seg and seg.tunnel:
255
+ if not self._in_tunnel:
256
+ reward += 3.0 # bonus za vstup do tunelu bez incidentu
257
+ self._in_tunnel = True
258
+ else:
259
+ self._in_tunnel = False
260
+
261
+ # ── Retardéry — EM tlumiče HX-1 rekuperují energii z nárazu ─────────────
262
+ if seg and "speed_bump" in seg.features:
263
+ damper_regen = tel.speed_kmh * 0.0003
264
+ self._episode_regen += damper_regen * self._vehicle.DT / 3600.0
265
+ reward += damper_regen * 0.004
266
+
267
+ # ── Rekuperace ────────────────────────────────────────────────────────
268
+ regen = tel.regen_kw
269
+ self._episode_regen += regen * self._vehicle.DT / 3600.0
270
+ reward += regen * self.REGEN_SCALE
271
+
272
+ # ── Dopravní provoz — fyzika kolizí + TTC prevence ───────────────────
273
+ agent_heading = self._vehicle.heading()
274
+ agent_speed_ms = tel.speed_kmh / 3.6
275
+ traffic_events = self._traffic.step(
276
+ self._vehicle.DT, px, pz, agent_speed_ms, agent_heading)
277
+ for ev in traffic_events:
278
+ self._traffic_collisions += 1
279
+ if ev.is_crash:
280
+ crash_pen = -25.0 - min(15.0, ev.delta_v_agent * 2.0)
281
+ reward += crash_pen
282
+ self._done = True
283
+ else:
284
+ reward -= 3.0
285
+
286
+ # TTC (time-to-collision) preventivní penalizace — trestá PŘED kolizí
287
+ threat = self._traffic.closest_threat_info(
288
+ px, pz, agent_speed_ms, agent_heading)
289
+ self._last_threat = threat
290
+ ttc_n = threat["ttc_norm"] # 0=bezpečno, 1=kolize za <1s
291
+ closing = threat["closing_norm"] # 0=stojí, 1=uzavírá 30 m/s
292
+ if ttc_n > 0.0 and closing > 0.0:
293
+ # Penalizace roste kvadraticky s blízkostí
294
+ reward -= ttc_n * ttc_n * closing * 4.0
295
+ # Odměna za udržení bezpečného odstupu (dist_norm blízko 1 = daleko)
296
+ elif threat["dist_norm"] > 0.5:
297
+ reward += 0.008
298
+
299
+ # ── Havárie (vlastní vozidlo) ─────────────────────────────────────────
300
+ if self._vehicle.is_crashed():
301
+ reward += self.CRASH_PENALTY
302
+ self._done = True
303
+
304
+ return float(reward)
305
+
306
+ def is_done(self, body, world_env) -> bool:
307
+ return self._done or world_env._tick >= self.max_steps
308
+
309
+ def success_condition(self, body, world_env) -> bool:
310
+ return self._success
311
+
312
+ # ── City context observation ──────────────────────────────────────────────
313
+
314
+ def city_obs(self) -> np.ndarray:
315
+ """
316
+ 8D city context vektor pro rozšíření observation.
317
+
318
+ [0] vzdálenost k dalšímu WP (normalizovaná)
319
+ [1] úhlová odchylka k dalšímu WP (normalizovaná)
320
+ [2] limit rychlosti v aktuální zóně (normalizovaný)
321
+ [3] semafor: 1.0 = zelená, 0.5 = žlutá, 0.0 = červená
322
+ [4] uzávěra blízko (0/1)
323
+ [5] index WP / celkový počet WP (postup trasou)
324
+ [6] telemetrie battery_soc
325
+ [7] telemetrie supercap_soc
326
+ """
327
+ tel = self._vehicle.state.telemetry
328
+ px, _, pz = self._vehicle.position()
329
+ heading = self._vehicle.heading()
330
+ obs = np.zeros(8, dtype=np.float32)
331
+ wp_count = len(self._route)
332
+
333
+ if self._wp_idx < wp_count:
334
+ wp = self._route[self._wp_idx]
335
+ dist = wp.dist_to(px, pz)
336
+ t_head = _heading_to_waypoint(px, pz, wp)
337
+ ang_err = _angular_error(heading, t_head)
338
+ obs[0] = float(np.clip(dist / 200.0, 0.0, 1.0))
339
+ obs[1] = float(ang_err / math.pi)
340
+ obs[2] = wp.speed_limit_kmh / 130.0
341
+
342
+ light_val = 0.5
343
+ if wp.is_intersection and wp.light_id:
344
+ lt = self._network.lights.get(wp.light_id)
345
+ if lt:
346
+ if lt.phase in ("NS_GREEN", "EW_GREEN"):
347
+ light_val = 1.0
348
+ elif lt.phase in ("NS_YELLOW", "EW_YELLOW"):
349
+ light_val = 0.5
350
+ else:
351
+ light_val = 0.0
352
+ obs[3] = light_val
353
+
354
+ obs[4] = 1.0 if any(c.blocks(px, pz) for c in self._closures) else 0.0
355
+ obs[5] = float(self._wp_idx) / max(wp_count - 1, 1)
356
+ obs[6] = tel.battery_soc
357
+ obs[7] = self._traffic.traffic_density(px, pz, radius=60.0)
358
+ return obs
359
+
360
+ def vehicle_obs(self) -> np.ndarray:
361
+ """18D telemetrie VehicleBody."""
362
+ return np.array(self._vehicle.observation_vec(), dtype=np.float32)
363
+
364
+ def full_obs(self) -> np.ndarray:
365
+ """Kompletní 26D observation: 18D telemetrie + 8D city context."""
366
+ return np.concatenate([self.vehicle_obs(), self.city_obs()])
367
+
368
+ def get_task_state(self, body_state: np.ndarray) -> np.ndarray:
369
+ """
370
+ Přepíše body_state[0:36] telemetrií vozidla, city contextem, TTC infem a raycasts.
371
+ state[0:18] = vehicle telemetrie
372
+ state[18:26] = city_obs (8D)
373
+ state[26] = closest_threat dist_norm (1=daleko, 0=těsně vedle)
374
+ state[27] = TTC norm (0=bezpečno, 1=kolize za <1s)
375
+ state[28:36] = 8D raycast echolokace (1=prázdno, 0=auto těsně vedle)
376
+ """
377
+ state = body_state.copy()
378
+ obs26 = self.full_obs() # 18D vozidlo + 8D city
379
+ state[0:26] = obs26
380
+
381
+ # Dej agentovi explicitní info o nejbližší hrozbě
382
+ px, _, pz = self._vehicle.position()
383
+ threat = getattr(self, "_last_threat", None)
384
+ if threat is None:
385
+ tel = self._vehicle.state.telemetry
386
+ threat = self._traffic.closest_threat_info(
387
+ px, pz, tel.speed_kmh / 3.6, self._vehicle.heading())
388
+ state[26] = float(threat["dist_norm"])
389
+ state[27] = float(threat["ttc_norm"])
390
+
391
+ self._last_rays = None # subclass DriveCity_Echo přepíše city_obs místo tohoto
392
+
393
+ return state
394
+
395
+ # ── Info / debug ──────────────────────────────────────────────────────────
396
+
397
+ def progress_info(self) -> Dict[str, object]:
398
+ tel = self._vehicle.state.telemetry
399
+ return {
400
+ "waypoint": self._wp_idx,
401
+ "waypoints_total": len(self._route),
402
+ "speed_kmh": round(tel.speed_kmh, 1),
403
+ "battery_soc": round(tel.battery_soc, 3),
404
+ "supercap_soc": round(tel.supercap_soc, 3),
405
+ "regen_kwh": round(self._episode_regen, 4),
406
+ "drs_open": tel.drs_open,
407
+ "rear_steer_deg": round(tel.rear_steer_deg, 2),
408
+ "drift_index": round(tel.drift_index, 3),
409
+ "closures": len(self._closures),
410
+ "success": self._success,
411
+ "traffic_cars": self._traffic.active_count,
412
+ "traffic_collisions": self._traffic_collisions,
413
+ }
embodied/tasks/drive_city_echo.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DriveCity_Echo — DriveCity s 8D raycast echolokací místo generických city_obs.
3
+
4
+ city_obs layout (8D):
5
+ [0] dist k dalšímu WP (zachováno — nutné pro navigaci)
6
+ [1] úhel k dalšímu WP (zachováno)
7
+ [2] limit rychlosti (zachováno)
8
+ [3] semafor (zachováno)
9
+ [4] raycast 0° = přímo vpředu (1=volno, 0=auto těsně)
10
+ [5] raycast 45° = vpředu vpravo
11
+ [6] raycast 315°= vpředu vlevo
12
+ [7] raycast 90° = přímo vpravo
13
+
14
+ A/B srovnání:
15
+ drive_city — closest_dist + TTC (1 hrozba, bez směru)
16
+ drive_city_echo — 4 navigační + 4 raycasts (vidí KDE auta jsou)
17
+ """
18
+ from __future__ import annotations
19
+ import math
20
+ import numpy as np
21
+ from embodied.tasks.drive_city import DriveCity, _heading_to_waypoint, _angular_error
22
+
23
+
24
+ class DriveCity_Echo(DriveCity):
25
+ """DriveCity kde city_obs[4:8] jsou 4 raycasts vpřed/vpravo/vlevo."""
26
+
27
+ name = "drive_city_echo"
28
+
29
+ def city_obs(self) -> np.ndarray:
30
+ """8D: 4D navigace (WP/limit/light) + 4D raycasts (vpřed, ±45°, vpravo)."""
31
+ tel = self._vehicle.state.telemetry
32
+ px, _, pz = self._vehicle.position()
33
+ heading = self._vehicle.heading()
34
+ obs = np.zeros(8, dtype=np.float32)
35
+ wp_count = len(self._route)
36
+
37
+ # ── Navigace (zachováno z DriveCity) ─────────────────────────────────
38
+ if self._wp_idx < wp_count:
39
+ wp = self._route[self._wp_idx]
40
+ dist = wp.dist_to(px, pz)
41
+ t_head = _heading_to_waypoint(px, pz, wp)
42
+ ang_err = _angular_error(heading, t_head)
43
+ obs[0] = float(np.clip(dist / 200.0, 0.0, 1.0))
44
+ obs[1] = float(ang_err / math.pi)
45
+ obs[2] = wp.speed_limit_kmh / 130.0
46
+
47
+ light_val = 0.5
48
+ if wp.is_intersection and wp.light_id:
49
+ lt = self._network.lights.get(wp.light_id)
50
+ if lt:
51
+ if lt.phase in ("NS_GREEN", "EW_GREEN"):
52
+ light_val = 1.0
53
+ elif lt.phase in ("NS_YELLOW", "EW_YELLOW"):
54
+ light_val = 0.5
55
+ else:
56
+ light_val = 0.0
57
+ obs[3] = light_val
58
+
59
+ # ── 4 raycasts: vpřed (0°), vpředvpravo (45°), vpředvlevo (315°), vpravo (90°) ──
60
+ all_rays = self._traffic.raycast_obs(px, pz, heading, max_dist=60.0, n_rays=8)
61
+ self._last_rays = all_rays
62
+ # all_rays[0]=0°, [1]=45°, [2]=90°, [3]=135°, [4]=180°, [5]=225°, [6]=270°, [7]=315°
63
+ obs[4] = all_rays[0] # přímo vpředu
64
+ obs[5] = all_rays[1] # vpředu vpravo (45°)
65
+ obs[6] = all_rays[7] # vpředu vlevo (315°)
66
+ obs[7] = all_rays[2] # přímo vpravo (90°)
67
+
68
+ return obs
embodied/tasks/drive_city_polaris.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ┌─────────────────────────────────────────────────────────────────────────┐
2
+ # │ SUBSYSTÉM: PhantomCortex Cognitive Neural Engine — Task Layer │
3
+ # │ Modul: DriveCity_Polaris — jízda po městě s POLARIS koprocesorem │
4
+ # │ Fyzika: PolarisServer existuje ve VoxForge světě (x=50, y=0.5, z=5) │
5
+ # └─────────────────────────────────────────────────────────────────────────┘
6
+ """
7
+ DriveCity_Polaris — identický task jako DriveCity, ale akce je blendovaná
8
+ s POLARIS serverem nahrátým z předchozího drive checkpointu.
9
+
10
+ Architektura:
11
+ RL agent (PhantomCortex) → 75% akce
12
+ POLARIS server (distillace drive checkpointu) → 25% akce
13
+ ────────────────────────────────────────────────────────
14
+ blended_action = 0.75 * rl_action + 0.25 * polaris_action
15
+
16
+ POLARIS se inicializuje z agent_drive_city.npz:
17
+ → bank_0: W1 drive checkpoint (co auto "vidí")
18
+ → bank_1: W_action drive checkpoint (jak řídí)
19
+ → bank_2: W1@W_action (přímá politika řízení)
20
+ → bank_3: W_disc (hodnotí situace na silnici)
21
+
22
+ Server je fyzicky umístěn v mapě (x=50, y=0.5, z=5) — za centrem.
23
+ POLARIS se online učí z každé RL akce (Hebbian, každé 4 kroky).
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ import os, sys
29
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
30
+
31
+ import numpy as np
32
+
33
+ # Zdědíme celý DriveCity task (reward, waypoints, traffic, vše)
34
+ from .drive_city import DriveCity
35
+
36
+ log = logging.getLogger("phantom.embodied")
37
+
38
+ _DRIVE_CKPT = "checkpoints/agent_drive_city.npz"
39
+ _POLARIS_BLEND = 0.25 # 25% POLARIS, 75% RL
40
+
41
+
42
+ class DriveCity_Polaris(DriveCity):
43
+ """
44
+ DriveCity + POLARIS server koprocesor.
45
+
46
+ Server se inicializuje JEDNOU při prvním reset() — Hebbian learning
47
+ se akumuluje přes epizody (POLARIS se učí z každého průjezdu).
48
+ """
49
+ name = "drive_city_polaris"
50
+
51
+ polaris_server = None
52
+ polaris_agent_id = "drive_polaris"
53
+ polaris_blend = _POLARIS_BLEND
54
+ _polaris_ready = False # flag: server už byl inicializován
55
+
56
+ def reset(self, body, world_env):
57
+ super().reset(body, world_env)
58
+
59
+ # Server se inicializuje jen jednou — Hebbian learning přetrvává
60
+ if not self._polaris_ready:
61
+ self._init_polaris_server()
62
+ self._polaris_ready = True
63
+
64
+ def _init_polaris_server(self):
65
+ try:
66
+ from engine.hardware.polaris_server import PolarisServer
67
+ from engine.hardware.phantom_uploader import PhantomCortexUploader
68
+
69
+ srv = PolarisServer(
70
+ position=(50.0, 0.5, 5.0),
71
+ name="PolarisServer-DRIVE-01",
72
+ rng_seed=7777,
73
+ )
74
+
75
+ ckpt_path = _DRIVE_CKPT if os.path.exists(_DRIVE_CKPT) else "checkpoints/agent_latest.npz"
76
+ if os.path.exists(ckpt_path):
77
+ uploader = PhantomCortexUploader(ckpt_path)
78
+ uploader.upload_to_server(srv)
79
+ log.info(
80
+ "[DriveCity_Polaris] POLARIS server INIT z %s blend=%.0f%% "
81
+ "(přetrvává přes epizody — Hebbian akumulace)",
82
+ os.path.basename(ckpt_path), _POLARIS_BLEND * 100
83
+ )
84
+ else:
85
+ log.warning("[DriveCity_Polaris] checkpoint nenalezen, POLARIS startuje náhodně")
86
+
87
+ srv.connect(self.polaris_agent_id, task_type="locomotion")
88
+ self.polaris_server = srv
89
+
90
+ except Exception as e:
91
+ log.warning("[DriveCity_Polaris] POLARIS init selhal (%s) — bez POLARIS", e)
92
+ self.polaris_server = None
93
+
94
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
95
+ # Standardní DriveCity reward (žádná změna)
96
+ reward = super().step_reward(body, world_env, action)
97
+
98
+ # Diagnostika serveru každých 200 kroků
99
+ if self.polaris_server is not None and world_env._tick % 200 == 0:
100
+ diag = self.polaris_server.diagnostics()
101
+ if diag.health != "ONLINE":
102
+ log.warning(
103
+ "[POLARIS-DRIVE] Server health=%s PWR=%.0fW CLT=%.1f°C",
104
+ diag.health, diag.total_power_w, diag.coolant["outlet_temp_c"]
105
+ )
106
+
107
+ return reward
embodied/tasks/household_ball.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BallTask — agent chytí míč a odvalí ho do cílové zóny.
3
+ Odměna: přiblížení k míči + přiblížení míče k cíli + alive bonus.
4
+ """
5
+ from __future__ import annotations
6
+ import sys, os
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
8
+
9
+ import numpy as np
10
+ from engine.physics.rigid_body import vec3, length
11
+ from embodied.tasks.base import Task
12
+ from embodied.interactive_objects import BallObject
13
+
14
+
15
+ class BallTask(Task):
16
+ name = "household_ball"
17
+ terrain = "flat"
18
+ max_steps = 400
19
+ reset_noise: float = 0.25
20
+
21
+ TARGET_POS = vec3(2.0, 0.12, 0.0) # cílová zóna
22
+
23
+ def __init__(self):
24
+ self._ball: BallObject = None
25
+ self._prev_ball_dist = 0.0
26
+
27
+ def reset(self, body, world_env):
28
+ rng = np.random.default_rng(world_env._tick if hasattr(world_env, '_tick') else 0)
29
+ spawn = vec3(
30
+ float(rng.uniform(-0.3, 0.3)),
31
+ 0.12,
32
+ float(rng.uniform(0.5, 1.0)),
33
+ )
34
+ self._ball = BallObject(world_env.phys, spawn, obj_id="task_ball")
35
+ self._prev_ball_dist = float(length(self._ball.position - self.TARGET_POS))
36
+ world_env._objects = getattr(world_env, "_objects", {})
37
+ world_env._objects["task_ball"] = self._ball
38
+
39
+ def step_reward(self, body, world_env, action) -> float:
40
+ if self._ball is None:
41
+ return 0.0
42
+
43
+ hand_R = body.hand_pos("R")
44
+ hand_dist = float(length(self._ball.position - hand_R))
45
+ ball_dist = float(length(self._ball.position - self.TARGET_POS))
46
+
47
+ approach_hand = max(0.0, 0.5 - hand_dist) * 0.4
48
+ approach_ball = (self._prev_ball_dist - ball_dist) * 3.0
49
+ proximity = 0.30 if hand_dist < 0.25 else (0.10 if hand_dist < 0.6 else 0.0)
50
+ alive = 0.05
51
+ action_pen = float(np.mean(np.abs(action))) * 0.02
52
+
53
+ self._prev_ball_dist = ball_dist
54
+ return approach_hand + approach_ball + proximity + alive - action_pen
55
+
56
+ def is_done(self, body, world_env) -> bool:
57
+ if body.is_fallen():
58
+ return True
59
+ if self.success_condition(body, world_env):
60
+ return True
61
+ return world_env._tick >= self.max_steps
62
+
63
+ def success_condition(self, body, world_env) -> bool:
64
+ if self._ball is None:
65
+ return False
66
+ return float(length(self._ball.position - self.TARGET_POS)) < 0.30
embodied/tasks/household_door.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Household task: walk to the door, push/reach the handle, then step through.
3
+
4
+ The door is in the south wall (z≈0) with a gap at x=13..17.
5
+ Handle target: (15, 3, 1). After touching the handle the door "opens" and
6
+ the agent must move to z < -1 (outside the room).
7
+
8
+ Phases:
9
+ 1. Walk to door handle (approach with hand_R)
10
+ 2. Touch handle (hand within 0.4m → door open flag)
11
+ 3. Step through gap (torso z < 0)
12
+ """
13
+ from __future__ import annotations
14
+ import numpy as np
15
+ from .base import Task
16
+ import sys, os
17
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
18
+ from engine.physics.rigid_body import vec3, length
19
+
20
+
21
+ class HouseholdDoorTask(Task):
22
+ name = "household_door"
23
+ terrain = "household"
24
+ max_steps = 1000
25
+
26
+ HANDLE_POS = vec3(15.0, 3.0, 1.5) # just inside south wall gap
27
+ OUTSIDE_Z = -0.5 # target z after passing through
28
+
29
+ def reset(self, body, world_env):
30
+ body.reset(vec3(8.0, 2.0, 6.0))
31
+ self._door_open = False
32
+ self._prev_handle_dist = self._handle_dist(body)
33
+ self._prev_torso_z = float(body.torso_pos()[2])
34
+
35
+ def _handle_dist(self, body) -> float:
36
+ return float(length(body.hand_pos("R") - self.HANDLE_POS))
37
+
38
+ def step_reward(self, body, world_env, action):
39
+ alive = 0.03 if not body.is_fallen() else -2.0
40
+ action_pen = float(np.mean(np.square(action))) * 0.003
41
+
42
+ if not self._door_open:
43
+ dist = self._handle_dist(body)
44
+ approach = (self._prev_handle_dist - dist) * 5.0
45
+ self._prev_handle_dist = dist
46
+ # Dense gradient from any distance — beats alive at every range
47
+ if dist < 0.40: proximity = 2.0
48
+ elif dist < 1.50: proximity = 0.7
49
+ elif dist < 4.00: proximity = 0.20
50
+ elif dist < 8.00: proximity = 0.06
51
+ else: proximity = 0.02
52
+ if dist < 0.4:
53
+ self._door_open = True
54
+ return approach + proximity + 8.0 + alive - action_pen
55
+ return approach + proximity + alive - action_pen
56
+ else:
57
+ tz = float(body.torso_pos()[2])
58
+ progress = (self._prev_torso_z - tz) * 4.0
59
+ self._prev_torso_z = tz
60
+ return progress + 0.5 + alive - action_pen
61
+
62
+ def success_condition(self, body, world_env):
63
+ return self._door_open and float(body.torso_pos()[2]) < self.OUTSIDE_Z
embodied/tasks/household_pick.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Household task: pick a cup off the table and carry it to the kitchen counter.
3
+
4
+ Phases:
5
+ 1. Approach cup — reward: Δ(distance hand_R → cup)
6
+ 2. Grasp cup — reward: holding bonus per step
7
+ 3. Carry to target — reward: Δ(distance cup → counter target)
8
+ """
9
+ from __future__ import annotations
10
+ import numpy as np
11
+ from .base import Task
12
+ import sys, os
13
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
14
+ from engine.physics.rigid_body import vec3, length
15
+
16
+
17
+ class HouseholdPickTask(Task):
18
+ name = "household_pick"
19
+ terrain = "household"
20
+ max_steps = 1200
21
+ reset_noise = 0.30 # higher exploration for manipulation
22
+
23
+ # Cup: on table (surface y=3, table at x=10-14, z=14-18)
24
+ CUP_POS = vec3(12.0, 3.5, 16.0)
25
+ # Target: on counter — blíže než dříve pro snazší curriculum
26
+ TARGET_POS = vec3(14.0, 3.5, 19.0)
27
+
28
+ def reset(self, body, world_env):
29
+ # Spawn blíže k hrnku (bylo 12m daleko, nyní ~1.5m)
30
+ body.reset(vec3(10.5, 2.0, 15.5))
31
+ # Add cup as interactive object (re-adds each episode via fresh world)
32
+ world_env.add_interactive_object(
33
+ "cup", "cup", self.CUP_POS,
34
+ size=vec3(0.08, 0.14, 0.08), mass=0.3,
35
+ )
36
+ self._prev_hand_dist = self._hand_dist(body, world_env)
37
+ self._prev_target_dist = float(length(self.CUP_POS - self.TARGET_POS))
38
+ self._holding_steps = 0
39
+
40
+ def _hand_dist(self, body, world_env) -> float:
41
+ hp = body.hand_pos("R")
42
+ cp = world_env.get_object_pos("cup")
43
+ return float(length(hp - cp))
44
+
45
+ def step_reward(self, body, world_env, action):
46
+ hand_id = "hand_R"
47
+ grip = float(action[9])
48
+ hp = body.hand_pos("R")
49
+
50
+ # Update grasp state
51
+ world_env.check_grasp(hand_id, hp, grip)
52
+ hand_vel = body.bodies[hand_id].velocity
53
+ world_env.update_grasped(
54
+ {hand_id: hp},
55
+ {hand_id: hand_vel},
56
+ )
57
+
58
+ alive = 0.04 if not body.is_fallen() else -2.0
59
+ action_pen = float(np.mean(np.square(action))) * 0.003
60
+
61
+ if world_env.is_broken("cup"):
62
+ return -5.0 + alive - action_pen
63
+
64
+ if world_env.is_grasped("cup"):
65
+ self._holding_steps += 1
66
+ cup_pos = world_env.get_object_pos("cup")
67
+ tgt_dist = float(length(cup_pos - self.TARGET_POS))
68
+ carry_prog = (self._prev_target_dist - tgt_dist) * 8.0
69
+ holding = 1.5 # zvýšeno z 1.0: silnější signál pro udržení uchopení
70
+ self._prev_target_dist = tgt_dist
71
+ return carry_prog + holding + alive - action_pen
72
+ else:
73
+ hand_dist = self._hand_dist(body, world_env)
74
+ approach = (self._prev_hand_dist - hand_dist) * 6.0
75
+ self._prev_hand_dist = hand_dist
76
+ # Dense proximity gradient — any range beats alive-only
77
+ if hand_dist < 0.25: proximity = 2.0
78
+ elif hand_dist < 0.60: proximity = 0.8
79
+ elif hand_dist < 1.50: proximity = 0.25
80
+ elif hand_dist < 3.50: proximity = 0.08
81
+ else: proximity = 0.02
82
+ # Pre-grasp bonus: ruka blízko + zavřená → +3.0 jednorázově (klíčový signál!)
83
+ pre_grasp = 3.0 if (hand_dist < 0.35 and grip < -0.25) else 0.0
84
+ return approach + proximity + pre_grasp + alive - action_pen
85
+
86
+ def food_event(self, body, world_env) -> tuple:
87
+ """KFP: uchopení hrníčku = dumpling — přímá metabolická odměna."""
88
+ if world_env.is_grasped("cup"):
89
+ return True, 0.40, 0.20 # drží hrníček → velký dumpling
90
+ hand_dist = self._hand_dist(body, world_env)
91
+ if hand_dist < 0.35:
92
+ return True, 0.08, 0.03 # ruka blízko → krekr (zárodek motivace)
93
+ return False, 0.0, 0.0
94
+
95
+ def success_condition(self, body, world_env):
96
+ if "cup" not in world_env.interactive_objects:
97
+ return False
98
+ cup_pos = world_env.get_object_pos("cup")
99
+ return float(length(cup_pos - self.TARGET_POS)) < 1.0
embodied/tasks/household_sit.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Household task: navigate to the chair and sit down.
3
+
4
+ Chair seat: x=8-10, z=20-22, surface y=3.
5
+ Chair centre: (9, 3, 21).
6
+
7
+ "Sitting" = torso is near chair XZ AND torso y ≤ 3.2 (low enough to be seated)
8
+ AND not fallen (torso y still ≥ 1.5).
9
+ """
10
+ from __future__ import annotations
11
+ import numpy as np
12
+ from .base import Task
13
+ import sys, os
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
15
+ from engine.physics.rigid_body import vec3, length
16
+
17
+
18
+ class HouseholdSitTask(Task):
19
+ name = "household_sit"
20
+ terrain = "household"
21
+ max_steps = 1000
22
+
23
+ CHAIR_POS = vec3(9.0, 3.0, 21.0)
24
+ SIT_Y_MAX = 3.8 # torso must be this low
25
+ SIT_Y_MIN = 1.5 # but not fallen
26
+ SIT_XZ_DIST = 1.5 # within 1.5m of chair centre (XZ plane)
27
+
28
+ def reset(self, body, world_env):
29
+ body.reset(vec3(8.0, 2.0, 4.0))
30
+ self._prev_dist = self._chair_dist(body)
31
+ self._seated_steps = 0
32
+
33
+ def _chair_dist(self, body) -> float:
34
+ tp = body.torso_pos()
35
+ dx = float(tp[0]) - float(self.CHAIR_POS[0])
36
+ dz = float(tp[2]) - float(self.CHAIR_POS[2])
37
+ return float(np.sqrt(dx*dx + dz*dz))
38
+
39
+ def _is_seated(self, body) -> bool:
40
+ tp = body.torso_pos()
41
+ ty = float(tp[1])
42
+ xz = self._chair_dist(body)
43
+ return xz < self.SIT_XZ_DIST and self.SIT_Y_MIN <= ty <= self.SIT_Y_MAX
44
+
45
+ def step_reward(self, body, world_env, action):
46
+ alive = 0.03 if not body.is_fallen() else -2.0
47
+ action_pen = float(np.mean(np.square(action))) * 0.003
48
+
49
+ dist = self._chair_dist(body)
50
+ approach = (self._prev_dist - dist) * 4.0
51
+ self._prev_dist = dist
52
+
53
+ # Dense proximity gradient — agent spawns 18m from chair
54
+ if dist < 1.5: proximity = 1.0
55
+ elif dist < 5.0: proximity = 0.25
56
+ elif dist < 12.: proximity = 0.07
57
+ else: proximity = 0.02
58
+
59
+ seated_bonus = 0.0
60
+ if self._is_seated(body):
61
+ self._seated_steps += 1
62
+ seated_bonus = 3.0
63
+
64
+ return approach + proximity + seated_bonus + alive - action_pen
65
+
66
+ def success_condition(self, body, world_env):
67
+ # Must stay seated for 30 consecutive steps
68
+ return self._seated_steps >= 30
embodied/tasks/household_stack.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Household task: pick up 3 blocks from the floor and stack them on the table.
3
+
4
+ Blocks start on the floor at (18,1,10), (20,1,10), (22,1,10).
5
+ Target: each block within 0.8m of table centre (12, 3, 16).
6
+
7
+ Strategy: agent earns reward per block it moves onto the table surface.
8
+ It must pick each block (grip_R), carry it, and release near target.
9
+ """
10
+ from __future__ import annotations
11
+ import numpy as np
12
+ from .base import Task
13
+ import sys, os
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
15
+ from engine.physics.rigid_body import vec3, length
16
+
17
+
18
+ _BLOCK_STARTS = [
19
+ vec3(18.0, 1.5, 10.0),
20
+ vec3(20.0, 1.5, 10.0),
21
+ vec3(22.0, 1.5, 10.0),
22
+ ]
23
+ _TABLE_TARGET = vec3(12.0, 3.5, 16.0)
24
+ _BLOCK_IDS = ["block_0", "block_1", "block_2"]
25
+
26
+
27
+ class HouseholdStackTask(Task):
28
+ name = "household_stack"
29
+ terrain = "household"
30
+ max_steps = 1000
31
+ reset_noise = 0.30
32
+
33
+ def reset(self, body, world_env):
34
+ body.reset(vec3(8.0, 2.0, 4.0))
35
+ for bid, bpos in zip(_BLOCK_IDS, _BLOCK_STARTS):
36
+ world_env.add_interactive_object(
37
+ bid, "block", bpos,
38
+ size=vec3(0.15, 0.15, 0.15), mass=0.5,
39
+ )
40
+ self._prev_dists = {bid: float(length(_BLOCK_STARTS[i] - _TABLE_TARGET))
41
+ for i, bid in enumerate(_BLOCK_IDS)}
42
+ self._on_table: set = set()
43
+ self._current_target = _BLOCK_IDS[0] # work on blocks in order
44
+ self._prev_hand_dist = self._nearest_block_dist(body, world_env)
45
+
46
+ def _block_dist_to_table(self, world_env, bid: str) -> float:
47
+ return float(length(world_env.get_object_pos(bid) - _TABLE_TARGET))
48
+
49
+ def _nearest_block_dist(self, body, world_env) -> float:
50
+ hp = body.hand_pos("R")
51
+ unplaced = [b for b in _BLOCK_IDS if b not in self._on_table]
52
+ if not unplaced:
53
+ return 0.0
54
+ return min(float(length(hp - world_env.get_object_pos(b))) for b in unplaced)
55
+
56
+ def step_reward(self, body, world_env, action):
57
+ hand_id = "hand_R"
58
+ grip = float(action[9])
59
+ hp = body.hand_pos("R")
60
+ world_env.check_grasp(hand_id, hp, grip)
61
+ world_env.update_grasped(
62
+ {hand_id: hp},
63
+ {hand_id: body.bodies[hand_id].velocity},
64
+ )
65
+
66
+ alive = 0.03 if not body.is_fallen() else -2.0
67
+ action_pen = float(np.mean(np.square(action))) * 0.003
68
+ reward = alive - action_pen
69
+
70
+ # Detect newly placed blocks
71
+ for bid in _BLOCK_IDS:
72
+ if bid not in self._on_table:
73
+ d = self._block_dist_to_table(world_env, bid)
74
+ if d < 0.8:
75
+ self._on_table.add(bid)
76
+ reward += 15.0
77
+
78
+ # Approach reward: hand toward nearest unplaced block
79
+ hand_dist = self._nearest_block_dist(body, world_env)
80
+ if not world_env.is_grasped(self._current_target or "block_0"):
81
+ approach = (self._prev_hand_dist - hand_dist) * 6.0
82
+ # Dense gradient — agent starts 12m from blocks, needs signal from far
83
+ if hand_dist < 0.30: proximity = 2.0
84
+ elif hand_dist < 1.00: proximity = 0.6
85
+ elif hand_dist < 4.00: proximity = 0.15
86
+ elif hand_dist < 10.0: proximity = 0.05
87
+ else: proximity = 0.01
88
+ # Pre-grasp bonus: ruka blízko bloku + zavřená → silný signál pro uchopení
89
+ pre_grasp = 3.0 if (hand_dist < 0.35 and grip < -0.25) else 0.0
90
+ reward += approach + proximity + pre_grasp
91
+ self._prev_hand_dist = hand_dist
92
+
93
+ # Progress reward toward current target
94
+ if self._current_target:
95
+ bid = self._current_target
96
+ d = self._block_dist_to_table(world_env, bid)
97
+ progress = (self._prev_dists[bid] - d) * 3.0
98
+ self._prev_dists[bid] = d
99
+ reward += progress
100
+ if bid in self._on_table:
101
+ remaining = [b for b in _BLOCK_IDS if b not in self._on_table]
102
+ self._current_target = remaining[0] if remaining else None
103
+
104
+ return reward
105
+
106
+ def food_event(self, body, world_env) -> tuple:
107
+ """KFP: uchopení bloku = dumpling, ruka blízko = krekr."""
108
+ ct = self._current_target or "block_0"
109
+ if world_env.is_grasped(ct):
110
+ return True, 0.40, 0.20
111
+ hand_dist = self._nearest_block_dist(body, world_env)
112
+ if hand_dist < 0.35:
113
+ return True, 0.08, 0.03
114
+ return False, 0.0, 0.0
115
+
116
+ def success_condition(self, body, world_env):
117
+ return len(self._on_table) >= 3
embodied/tasks/household_sweep.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Household task: sweep / knock three cups off the table edge.
3
+
4
+ Cups start on the table at x=10-14, z=14-18 (surface y=3).
5
+ "Off table" = cup y drops below 2.5 (fell off the edge).
6
+
7
+ The agent earns reward each time a cup drops, plus approach reward
8
+ for the hand_R moving toward cups still on the table.
9
+ """
10
+ from __future__ import annotations
11
+ import numpy as np
12
+ from .base import Task
13
+ import sys, os
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
15
+ from engine.physics.rigid_body import vec3, length
16
+
17
+
18
+ _SWEEP_CUPS = [
19
+ ("sweep_cup_0", vec3(11.0, 3.5, 15.0)),
20
+ ("sweep_cup_1", vec3(12.5, 3.5, 16.0)),
21
+ ("sweep_cup_2", vec3(13.0, 3.5, 17.0)),
22
+ ]
23
+
24
+
25
+ class HouseholdSweepTask(Task):
26
+ name = "household_sweep"
27
+ terrain = "household"
28
+ max_steps = 800
29
+
30
+ def reset(self, body, world_env):
31
+ body.reset(vec3(8.0, 2.0, 4.0))
32
+ for cid, cpos in _SWEEP_CUPS:
33
+ world_env.add_interactive_object(
34
+ cid, "cup", cpos,
35
+ size=vec3(0.08, 0.14, 0.08), mass=0.3,
36
+ )
37
+ self._knocked: set = set()
38
+ self._prev_hand_dist = self._nearest_cup_dist(body, world_env)
39
+
40
+ def _nearest_cup_dist(self, body, world_env) -> float:
41
+ hp = body.hand_pos("R")
42
+ dists = []
43
+ for cid, _ in _SWEEP_CUPS:
44
+ if cid not in self._knocked:
45
+ dists.append(float(length(hp - world_env.get_object_pos(cid))))
46
+ return min(dists) if dists else 0.0
47
+
48
+ def step_reward(self, body, world_env, action):
49
+ alive = 0.03 if not body.is_fallen() else -2.0
50
+ action_pen = float(np.mean(np.square(action))) * 0.003
51
+ reward = alive - action_pen
52
+
53
+ for cid, _ in _SWEEP_CUPS:
54
+ if cid not in self._knocked:
55
+ cup_y = float(world_env.get_object_pos(cid)[1])
56
+ broken = world_env.is_broken(cid)
57
+ if cup_y < 2.5 or broken:
58
+ self._knocked.add(cid)
59
+ reward += 12.0 + (4.0 if broken else 0.0)
60
+
61
+ if len(self._knocked) < len(_SWEEP_CUPS):
62
+ d = self._nearest_cup_dist(body, world_env)
63
+ approach = (self._prev_hand_dist - d) * 6.0
64
+ # Sweep bonus: ruka velmi blízko hrníčku = aktivní sweep pokus
65
+ sweep_attempt = 4.0 if d < 0.30 else 0.0
66
+ # Agent spawns at (8,2,4), cups at ~(12,3.5,15-17) → ~13m away
67
+ if d < 0.40: proximity = 2.0
68
+ elif d < 1.50: proximity = 0.7
69
+ elif d < 5.00: proximity = 0.18
70
+ elif d < 12.0: proximity = 0.05
71
+ else: proximity = 0.01
72
+ self._prev_hand_dist = d
73
+ reward += approach + proximity + sweep_attempt
74
+
75
+ return reward
76
+
77
+ def success_condition(self, body, world_env):
78
+ return len(self._knocked) >= 3
embodied/tasks/ice_driving.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ IceDrivingTask — jízda HX-1 na ledu / sněhu.
3
+
4
+ Emergentní chování které by mělo vzniknout:
5
+ - Dřívější brzdění (mu×0.14 → brzdná dráha ~7× delší)
6
+ - Dřívější zatáčení (musí nacouvat nebo flick před zatáčkou)
7
+ - Rekuperační brzdění místo kotoučů (plynulejší, bez blokování kol)
8
+ - Scandinavian flick / pendulum: přehoz karoserie pro řízený oversteer
9
+ - Throttle na výjezdu ze zatáčky: trakce AWD = šoférovat výkonem ven ze zatáčky
10
+ - "Brzdit zpátečkou": EM motory jako generátory = engine-braking bez lockup
11
+
12
+ Povrchové podmínky:
13
+ "ice" — průhledný led, mu_factor=0.14 (frozen lake, závodní situace)
14
+ "snow" — utlačený sníh, mu_factor=0.28 (rallye styl)
15
+ "black_ice" — náhodné zákeřné klouzání, mu_factor=0.07 (nebezpečné)
16
+ "mixed" — střídá se led / sníh / dry patches (nejreálnější)
17
+
18
+ Odměnová funkce:
19
+ +progress : postup po trase
20
+ +goal_bonus : dosažení cíle (+80)
21
+ +smooth_ctrl: plynulé vstupy (bez agresivních skoků)
22
+ +regen_brake: rekuperační brzdění místo kotoučů (+bonus)
23
+ +controlled_drift: drift_index 0.15–0.55 = řízený → bonus
24
+ -spin_pen : drift_index > 0.75 = vykrouceníí → penalizace
25
+ -crash_pen : havárie (-30)
26
+ -late_brake : pozdní prudké brzdění v zatáčce → penalizace (naučí jet předem)
27
+
28
+ Observation (28D):
29
+ [0:18] vehicle telemetrie
30
+ [18] vzdálenost k cíli (norm)
31
+ [19] úhel k cíli
32
+ [20] aktuální mu_factor (agent vidí klouzavost)
33
+ [21] drift_index
34
+ [22] speed_norm
35
+ [23] regen_kw_norm
36
+ [24] prev_steer (paměť vstupu pro smooth penalty)
37
+ [25] prev_throttle
38
+ [26] slip_ratio_front (odhad z telemetrie)
39
+ [27] black_ice_nearby (0/1 — random zákeřné místo)
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import math
44
+ import random
45
+ from dataclasses import dataclass
46
+ from typing import List, Optional, Tuple
47
+
48
+ import numpy as np
49
+
50
+ from embodied.tasks.base import Task
51
+ from embodied.vehicle_body import VehicleBody
52
+ from engine.city_builder.city_gen import CityGenerator, build_traffic_network
53
+ from engine.city_builder.road_network import RoadNetwork
54
+ from engine.city_builder.traffic_sim import TrafficSimulation
55
+ from engine.vehicles.tire_physics import SurfaceCondition
56
+
57
+
58
+ # ── Konstanty ──────────────────────────────────────────────────────────────────
59
+
60
+ _GOAL_RADIUS = 35.0
61
+ _CRASH_PEN = -30.0
62
+ _SPIN_PEN = -2.0 # per krok při drift_index > 0.75
63
+ _LATE_BRAKE_PEN = -0.8 # pozdní prudké brzdění v zatáčce
64
+ _DRIFT_OPT_LO = 0.15 # ideální drift_index minimum (řízený slip)
65
+ _DRIFT_OPT_HI = 0.55 # ideální drift_index maximum
66
+
67
+ # Tréninkové body na malém okruhu (ice track) — krátké zatáčky, různé poloměry
68
+ _ICE_TRACK = [
69
+ # (x, z, label)
70
+ ( 0, 0, "start"),
71
+ ( 400, 0, "T1_braking"),
72
+ ( 600, 200, "T2_hairpin"),
73
+ ( 600, 500, "T3_fast"),
74
+ ( 400, 700, "T4_chicane_a"),
75
+ ( 200, 700, "T4_chicane_b"),
76
+ ( 0, 500, "T5_long"),
77
+ (-200, 200, "T6_kink"),
78
+ ( 0, 0, "finish"),
79
+ ]
80
+
81
+
82
+ def _dist(ax, az, bx, bz) -> float:
83
+ return math.sqrt((ax - bx) ** 2 + (az - bz) ** 2)
84
+
85
+
86
+ # ── Task ──────────────────────────────────────────────────────────────────────
87
+
88
+ class IceDrivingTask(Task):
89
+ """
90
+ Trénuje agenta na ledových/sněžných podmínkách.
91
+ Emergentní: plynulé vstupy, rekuperační brzdění, řízený drift.
92
+ """
93
+
94
+ name = "ice_driving"
95
+ terrain = "ice"
96
+ max_steps = 3000
97
+ reset_noise = 0.05 # malý noise — led je citlivý
98
+
99
+ def __init__(self,
100
+ surface: str = "ice",
101
+ seed: int = 0,
102
+ city_seed: int = 42):
103
+ assert surface in ("ice", "snow", "black_ice", "mixed"), f"Neznámý povrch: {surface}"
104
+ self.surface_name = surface
105
+ self._rng = random.Random(seed)
106
+ self._road_net = CityGenerator(seed=city_seed, closure_count=0).generate()
107
+ self._network = build_traffic_network(self._road_net)
108
+ # Na ledu je minimální provoz (1–2 auta)
109
+ self._traffic = TrafficSimulation(
110
+ self._road_net, density_scale=0.05, seed=seed, max_cars=5)
111
+ self._vehicle = VehicleBody.phantom_vertex()
112
+ self._surface = self._make_surface(surface)
113
+ self._track = _ICE_TRACK
114
+ self._wp_idx = 1
115
+ self._prev_dist = 0.0
116
+ self._prev_steer = 0.0
117
+ self._prev_thr = 0.0
118
+ self._elapsed_s = 0.0
119
+ self._total_regen = 0.0
120
+ self._spin_steps = 0
121
+ self._late_brake_pen = 0.0
122
+ self._reached_finish = False
123
+ # Zákeřné místo černého ledu (mění se každou epizodu)
124
+ self._black_ice_pos: Optional[Tuple[float, float]] = None
125
+
126
+ def _make_surface(self, name: str, vary: bool = False) -> SurfaceCondition:
127
+ if name == "ice":
128
+ s = SurfaceCondition.ice()
129
+ elif name == "snow":
130
+ s = SurfaceCondition.snow()
131
+ elif name == "black_ice":
132
+ s = SurfaceCondition.black_ice()
133
+ else: # mixed
134
+ choice = self._rng.choice(["ice", "snow", "ice"])
135
+ s = SurfaceCondition.ice() if choice == "ice" else SurfaceCondition.snow()
136
+ # Drobná variace mu_factor (±10%) pro rozmanitost epizod
137
+ if vary:
138
+ s.mu_factor *= self._rng.uniform(0.9, 1.1)
139
+ return s
140
+
141
+ # ── Reset ──────────────────────────────────────────────────────────────────
142
+
143
+ def reset(self, body, world_env) -> None:
144
+ self._surface = self._make_surface(self.surface_name, vary=True)
145
+ self._wp_idx = 1
146
+ self._elapsed_s = 0.0
147
+ self._total_regen = 0.0
148
+ self._spin_steps = 0
149
+ self._late_brake_pen = 0.0
150
+ self._reached_finish = False
151
+ self._prev_steer = 0.0
152
+ self._prev_thr = 0.0
153
+
154
+ start = self._track[0]
155
+ self._vehicle.reset(position=(start[0], 0.0, start[1]), heading_rad=0.0)
156
+ self._prev_dist = _dist(start[0], start[1],
157
+ self._track[1][0], self._track[1][1])
158
+ self._traffic.reset()
159
+
160
+ # Náhodné místo černého ledu (mixed/black_ice mód)
161
+ if self.surface_name in ("black_ice", "mixed"):
162
+ ti = self._rng.randint(1, len(self._track) - 2)
163
+ self._black_ice_pos = (
164
+ self._track[ti][0] + self._rng.uniform(-30, 30),
165
+ self._track[ti][1] + self._rng.uniform(-30, 30),
166
+ )
167
+ else:
168
+ self._black_ice_pos = None
169
+
170
+ # ── Step reward ────────────────────────────────────────────────────────────
171
+
172
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
173
+ throttle = float(np.clip(action[0], 0.0, 1.0))
174
+ brake = float(np.clip(-min(action[0], 0.0), 0.0, 1.0))
175
+ steer = float(np.clip(action[2] if len(action) > 2 else 0.0, -1.0, 1.0))
176
+
177
+ # Aplikuj povrchovou podmínku na vehicle před krokem
178
+ # (interně snižuje dostupnou trakci)
179
+ self._vehicle.set_surface_mu(self._surface.mu_factor)
180
+
181
+ # Černý led: náhodné zákeřné snížení gripu
182
+ px, _, pz = self._vehicle.position()
183
+ mu_effective = self._surface.mu_factor
184
+ if self._black_ice_pos:
185
+ bx, bz = self._black_ice_pos
186
+ if _dist(px, pz, bx, bz) < 20.0:
187
+ mu_effective *= 0.45 # zhoršení o 55%
188
+ self._vehicle.set_surface_mu(mu_effective)
189
+
190
+ tel = self._vehicle.step(throttle, brake, steer)
191
+ dt = self._vehicle.DT
192
+ self._elapsed_s += dt
193
+
194
+ px, _, pz = self._vehicle.position()
195
+ reward = 0.0
196
+
197
+ # ── Postup k cílovému bodu ────────────────────────────────────────────
198
+ if self._wp_idx < len(self._track):
199
+ wx, wz = self._track[self._wp_idx][0], self._track[self._wp_idx][1]
200
+ dist = _dist(px, pz, wx, wz)
201
+ progress = (self._prev_dist - dist) * 0.08
202
+ reward += max(-0.4, progress)
203
+ self._prev_dist = dist
204
+
205
+ if dist < _GOAL_RADIUS:
206
+ reward += 10.0 # průjezdní bod
207
+ self._wp_idx += 1
208
+ if self._wp_idx >= len(self._track):
209
+ self._reached_finish = True
210
+ reward += 80.0
211
+ else:
212
+ nx, nz = self._track[self._wp_idx][0], self._track[self._wp_idx][1]
213
+ self._prev_dist = _dist(px, pz, nx, nz)
214
+
215
+ # ── Plynulost vstupů (klíčová na ledu) ───────────────────────────────
216
+ steer_jerk = abs(steer - self._prev_steer)
217
+ thr_jerk = abs(throttle - self._prev_thr)
218
+ smooth_bonus = max(0.0, 0.04 - steer_jerk * 0.06 - thr_jerk * 0.04)
219
+ reward += smooth_bonus
220
+ self._prev_steer = steer
221
+ self._prev_thr = throttle
222
+
223
+ # ── Rekuperační brzdění (EM motory jako generátory) ───────────────────
224
+ regen = tel.regen_kw
225
+ self._total_regen += regen * dt / 3600.0
226
+ # Bonus za regen-brzdění (místo kotouče) — specificky při deceleraci
227
+ if brake > 0.1 and regen > 5.0:
228
+ reward += regen * 0.005 * self._surface.regen_factor
229
+ elif regen > 0:
230
+ reward += regen * 0.002 * self._surface.regen_factor
231
+
232
+ # ── Řízený drift vs. spin-out ──────────────────────────────────────────
233
+ drift = tel.drift_index
234
+ if _DRIFT_OPT_LO <= drift <= _DRIFT_OPT_HI:
235
+ # Řízený slip = rally styl, agent se to aktivně naučí
236
+ reward += 0.15 * (1.0 - abs(drift - 0.35) / 0.20)
237
+ elif drift > 0.75:
238
+ # Vytoč → trestej
239
+ self._spin_steps += 1
240
+ reward += _SPIN_PEN * min(1.0, (drift - 0.75) / 0.25)
241
+
242
+ # ── Pozdní prudké brzdění v zatáčce ───────────────────────────────────
243
+ # Detekce: agent brzdí ostře (brake > 0.5) a zároveň steer > 0.3
244
+ # → na ledu to vede ke spin-outu → penalizuj dřív než nastane
245
+ if brake > 0.5 and abs(steer) > 0.3:
246
+ late_pen = brake * abs(steer) * 0.4
247
+ reward -= late_pen
248
+ self._late_brake_pen += late_pen
249
+
250
+ # ── EM tlumiče (retardéry/nerovnosti) ────────────────────────────────
251
+ seg = self._road_net.segment_at(px, pz)
252
+ if seg and "speed_bump" in seg.features:
253
+ damper_regen = tel.speed_kmh * 0.0003
254
+ self._total_regen += damper_regen * dt / 3600.0
255
+ reward += damper_regen * 0.004
256
+
257
+ # ── Časový tlak ────────────────────────────────────────────────────────
258
+ reward -= 0.015 # mírný tlak (menší než timed_nav)
259
+
260
+ # ── Havárie ────────────────────────────────────────────────────────────
261
+ if self._vehicle.is_crashed():
262
+ reward += _CRASH_PEN
263
+
264
+ return float(reward)
265
+
266
+ # ── Terminace ──────────────────────────────────────────────────────────────
267
+
268
+ def is_done(self, body, world_env) -> bool:
269
+ if self._reached_finish:
270
+ return True
271
+ if self._vehicle.is_crashed():
272
+ return True
273
+ return world_env._tick >= self.max_steps
274
+
275
+ def success_condition(self, body, world_env) -> bool:
276
+ return self._reached_finish
277
+
278
+ def food_event(self, body, world_env) -> tuple:
279
+ if self._reached_finish:
280
+ return True, 1.0, 0.35
281
+ return False, 0.0, 0.0
282
+
283
+ # ── State override ─────────────────────────────────────────────────────────
284
+
285
+ def get_task_state(self, body_state: np.ndarray) -> np.ndarray:
286
+ state = body_state.copy()
287
+ state[0:18] = np.array(self._vehicle.observation_vec(), dtype=np.float32)
288
+
289
+ px, _, pz = self._vehicle.position()
290
+ tel = self._vehicle.state.telemetry
291
+
292
+ if self._wp_idx < len(self._track):
293
+ wx, wz = self._track[self._wp_idx][0], self._track[self._wp_idx][1]
294
+ dist = _dist(px, pz, wx, wz)
295
+ heading = self._vehicle.heading()
296
+ goal_h = math.atan2(wz - pz, wx - px)
297
+ ang_err = (goal_h - heading + math.pi) % (2 * math.pi) - math.pi
298
+ else:
299
+ dist, ang_err = 0.0, 0.0
300
+
301
+ # Černý led v okolí?
302
+ black_ice_near = 0.0
303
+ if self._black_ice_pos:
304
+ bx, bz = self._black_ice_pos
305
+ if _dist(px, pz, bx, bz) < 40.0:
306
+ black_ice_near = 1.0
307
+
308
+ state[18] = float(np.clip(dist / 800.0, 0.0, 1.0))
309
+ state[19] = float(ang_err / math.pi)
310
+ state[20] = float(self._surface.mu_factor) # agent vidí klouzavost!
311
+ state[21] = float(tel.drift_index)
312
+ state[22] = float(np.clip(tel.speed_kmh / 140.0, 0.0, 1.0))
313
+ state[23] = float(np.clip(tel.regen_kw / 50.0, 0.0, 1.0))
314
+ state[24] = float(self._prev_steer)
315
+ state[25] = float(self._prev_thr)
316
+ state[26] = float(np.clip(abs(tel.drift_index) * 2.0, 0.0, 1.0))
317
+ state[27] = black_ice_near
318
+ return state
319
+
320
+ # ── Info ───────────────────────────────────────────────────────────────────
321
+
322
+ def episode_info(self) -> dict:
323
+ tel = self._vehicle.state.telemetry
324
+ return {
325
+ "surface": self.surface_name,
326
+ "mu_factor": round(self._surface.mu_factor, 3),
327
+ "waypoints_done": self._wp_idx - 1,
328
+ "elapsed_s": round(self._elapsed_s, 1),
329
+ "spin_steps": self._spin_steps,
330
+ "late_brake_pen": round(self._late_brake_pen, 2),
331
+ "regen_kwh": round(self._total_regen, 4),
332
+ "drift_index": round(tel.drift_index, 3),
333
+ "speed_kmh": round(tel.speed_kmh, 1),
334
+ "finished": self._reached_finish,
335
+ }
336
+
337
+
338
+ # ── Varianty ──────────────────────────────────────────────────────────────────
339
+
340
+ class IceDrivingIce(IceDrivingTask):
341
+ """Průhledný led — rallye na zamrzlém jezeře."""
342
+ name = "ice_driving_ice"
343
+ def __init__(self, seed=0): super().__init__(surface="ice", seed=seed)
344
+
345
+ class IceDrivingSnow(IceDrivingTask):
346
+ """Utlačený sníh — rallye ve Finsku."""
347
+ name = "ice_driving_snow"
348
+ def __init__(self, seed=0): super().__init__(surface="snow", seed=seed)
349
+
350
+ class IceDrivingBlackIce(IceDrivingTask):
351
+ """Zákeřný černý led — agresivní, nečekané klouzání."""
352
+ name = "ice_driving_black_ice"
353
+ def __init__(self, seed=0): super().__init__(surface="black_ice", seed=seed)
354
+
355
+ class IceDrivingMixed(IceDrivingTask):
356
+ """Smíšený povrch — střídá led, sníh, suché záplaty."""
357
+ name = "ice_driving_mixed"
358
+ def __init__(self, seed=0): super().__init__(surface="mixed", seed=seed)
embodied/tasks/ice_driving_polaris.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ┌─────────────────────────────────────────────────────────────────────────┐
2
+ # │ SUBSYSTÉM: PhantomCortex Cognitive Neural Engine — Task Layer │
3
+ # │ Modul: IceDriving_Polaris — jízda na ledu s POLARIS koprocesorem │
4
+ # │ Fyzika: PolarisServer existuje ve VoxForge světě (x=60, y=0.5, z=15)│
5
+ # └─────────────────────────────────────────────────────────────────────────┘
6
+ """
7
+ IceDriving_Polaris — ice/snow/mixed jízda s POLARIS serverem jako koprocesorem.
8
+
9
+ Architektura:
10
+ RL agent (PhantomCortex) → 75% akce
11
+ POLARIS server (distillace drive checkpointu) → 25% akce
12
+ ────────────────────────────────────────────────────────
13
+ blended_action = 0.75 * rl_action + 0.25 * polaris_action
14
+
15
+ POLARIS se inicializuje z agent_drive_city.npz (nejlepší dostupný checkpoint).
16
+ Na ledu může drive-city znalost:
17
+ + přispět plynulostí řízení (smooth steering)
18
+ + pomoci s brzdovými vzory
19
+ - nebo překazit (agresivní vstupy nevhodné pro led)
20
+
21
+ Server je fyzicky umístěn v mapě (x=60, y=0.5, z=15) — u okraje ice tracku.
22
+ POLARIS se online učí z každé RL akce (Hebbian, každé 4 kroky).
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ import os, sys
28
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
29
+
30
+ import numpy as np
31
+
32
+ from .ice_driving import IceDrivingMixed, IceDrivingIce, IceDrivingSnow, IceDrivingBlackIce
33
+
34
+ log = logging.getLogger("phantom.embodied")
35
+
36
+ _DRIVE_CKPT = "checkpoints/agent_drive_city.npz"
37
+ _POLARIS_BLEND = 0.25
38
+
39
+
40
+ class IceDriving_Polaris(IceDrivingMixed):
41
+ """
42
+ IceDrivingMixed + POLARIS server koprocesor.
43
+
44
+ Server se inicializuje JEDNOU při prvním reset() — Hebbian learning
45
+ akumuluje přes epizody (POLARIS se učí z každého průjezdu po ledu).
46
+ """
47
+ name = "ice_driving_polaris"
48
+
49
+ polaris_server = None
50
+ polaris_agent_id = "ice_polaris"
51
+ polaris_blend = _POLARIS_BLEND
52
+ _polaris_ready = False
53
+
54
+ def reset(self, body, world_env):
55
+ super().reset(body, world_env)
56
+
57
+ if not self._polaris_ready:
58
+ self._init_polaris_server()
59
+ self._polaris_ready = True
60
+
61
+ def _init_polaris_server(self):
62
+ try:
63
+ from engine.hardware.polaris_server import PolarisServer
64
+ from engine.hardware.phantom_uploader import PhantomCortexUploader
65
+
66
+ srv = PolarisServer(
67
+ position=(60.0, 0.5, 15.0),
68
+ name="PolarisServer-ICE-01",
69
+ rng_seed=8888,
70
+ )
71
+
72
+ ckpt_path = _DRIVE_CKPT if os.path.exists(_DRIVE_CKPT) else "checkpoints/agent_latest.npz"
73
+ if os.path.exists(ckpt_path):
74
+ uploader = PhantomCortexUploader(ckpt_path)
75
+ uploader.upload_to_server(srv)
76
+ log.info(
77
+ "[IceDriving_Polaris] POLARIS server INIT z %s blend=%.0f%% "
78
+ "(přetrvává přes epizody — Hebbian akumulace)",
79
+ os.path.basename(ckpt_path), _POLARIS_BLEND * 100
80
+ )
81
+ else:
82
+ log.warning("[IceDriving_Polaris] checkpoint nenalezen, POLARIS startuje náhodně")
83
+
84
+ srv.connect(self.polaris_agent_id, task_type="locomotion")
85
+ self.polaris_server = srv
86
+
87
+ except Exception as e:
88
+ log.warning("[IceDriving_Polaris] POLARIS init selhal (%s) — bez POLARIS", e)
89
+ self.polaris_server = None
90
+
91
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
92
+ reward = super().step_reward(body, world_env, action)
93
+
94
+ if self.polaris_server is not None and world_env._tick % 200 == 0:
95
+ diag = self.polaris_server.diagnostics()
96
+ if diag.health != "ONLINE":
97
+ log.warning(
98
+ "[POLARIS-ICE] Server health=%s PWR=%.0fW CLT=%.1f°C",
99
+ diag.health, diag.total_power_w, diag.coolant["outlet_temp_c"]
100
+ )
101
+
102
+ return reward
103
+
104
+
105
+ # ── Varianty pro jednotlivé povrchy ───────────────────────────────────────────
106
+
107
+ class IceDrivingIce_Polaris(IceDrivingIce):
108
+ """Čistý led + POLARIS."""
109
+ name = "ice_driving_ice_polaris"
110
+ polaris_server = None
111
+ polaris_agent_id = "ice_polaris_pure"
112
+ polaris_blend = _POLARIS_BLEND
113
+ _polaris_ready = False
114
+
115
+ def reset(self, body, world_env):
116
+ super().reset(body, world_env)
117
+ if not self._polaris_ready:
118
+ self._init_polaris_server()
119
+ self._polaris_ready = True
120
+
121
+ def _init_polaris_server(self):
122
+ try:
123
+ from engine.hardware.polaris_server import PolarisServer
124
+ from engine.hardware.phantom_uploader import PhantomCortexUploader
125
+ srv = PolarisServer(position=(60.0, 0.5, 15.0), name="PolarisServer-ICE-PURE", rng_seed=9001)
126
+ ckpt = _DRIVE_CKPT if os.path.exists(_DRIVE_CKPT) else "checkpoints/agent_latest.npz"
127
+ if os.path.exists(ckpt):
128
+ PhantomCortexUploader(ckpt).upload_to_server(srv)
129
+ log.info("[IceDrivingIce_Polaris] INIT z %s blend=%.0f%%", os.path.basename(ckpt), _POLARIS_BLEND * 100)
130
+ srv.connect(self.polaris_agent_id, task_type="locomotion")
131
+ self.polaris_server = srv
132
+ except Exception as e:
133
+ log.warning("[IceDrivingIce_Polaris] POLARIS init selhal (%s)", e)
134
+ self.polaris_server = None
135
+
136
+
137
+ class IceDrivingBlackIce_Polaris(IceDrivingBlackIce):
138
+ """Černý led + POLARIS (nejtěžší varianta)."""
139
+ name = "ice_driving_black_ice_polaris"
140
+ polaris_server = None
141
+ polaris_agent_id = "ice_polaris_black"
142
+ polaris_blend = _POLARIS_BLEND
143
+ _polaris_ready = False
144
+
145
+ def reset(self, body, world_env):
146
+ super().reset(body, world_env)
147
+ if not self._polaris_ready:
148
+ self._init_polaris_server()
149
+ self._polaris_ready = True
150
+
151
+ def _init_polaris_server(self):
152
+ try:
153
+ from engine.hardware.polaris_server import PolarisServer
154
+ from engine.hardware.phantom_uploader import PhantomCortexUploader
155
+ srv = PolarisServer(position=(60.0, 0.5, 15.0), name="PolarisServer-ICE-BLACK", rng_seed=9002)
156
+ ckpt = _DRIVE_CKPT if os.path.exists(_DRIVE_CKPT) else "checkpoints/agent_latest.npz"
157
+ if os.path.exists(ckpt):
158
+ PhantomCortexUploader(ckpt).upload_to_server(srv)
159
+ log.info("[IceDrivingBlackIce_Polaris] INIT z %s blend=%.0f%%", os.path.basename(ckpt), _POLARIS_BLEND * 100)
160
+ srv.connect(self.polaris_agent_id, task_type="locomotion")
161
+ self.polaris_server = srv
162
+ except Exception as e:
163
+ log.warning("[IceDrivingBlackIce_Polaris] POLARIS init selhal (%s)", e)
164
+ self.polaris_server = None
embodied/tasks/maze.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task: navigate from spawn to goal in the maze."""
2
+ from __future__ import annotations
3
+ import numpy as np
4
+ from .base import Task
5
+ import sys, os
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
7
+ from engine.physics.rigid_body import vec3, length
8
+
9
+
10
+ class MazeTask(Task):
11
+ name = "maze"
12
+ terrain = "maze"
13
+ max_steps = 2000
14
+
15
+ def reset(self, body, world_env):
16
+ body.reset(vec3(4.0, 2.0, 4.0))
17
+ self._goal = getattr(world_env, "maze_goal", vec3(58.0, 1.0, 58.0))
18
+ self._prev_dist = float(length(body.torso_pos() - self._goal))
19
+
20
+ def step_reward(self, body, world_env, action):
21
+ pos = body.torso_pos()
22
+ dist = float(length(pos - self._goal))
23
+ # Dense reward: getting closer
24
+ approach = (self._prev_dist - dist) * 3.0
25
+ alive = 0.1 if not body.is_fallen() else -3.0
26
+ at_goal = 5.0 if dist < 3.0 else 0.0
27
+ action_pen = float(np.sum(np.square(action))) * 0.003
28
+ self._prev_dist = dist
29
+ return approach + alive + at_goal - action_pen
30
+
31
+ def is_done(self, body, world_env):
32
+ dist = float(length(body.torso_pos() - self._goal))
33
+ return body.is_fallen() or dist < 2.5 or world_env._tick >= self.max_steps
34
+
35
+ def success_condition(self, body, world_env):
36
+ return float(length(body.torso_pos() - self._goal)) < 2.5
embodied/tasks/media_watch.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MediaWatchTask — agent se musí otočit k obrazovce a sledovat ji (udržovat pohled)
3
+ zatímco hraje reproduktor. Testuje schopnost orientace k senzorickému podnětu.
4
+
5
+ Odměna: visibility score (jak moc monitor vidí) × zesílení reproduktoru
6
+ + bonus za udržení pohledu po N po sobě jdoucích krocích.
7
+ """
8
+ from __future__ import annotations
9
+ import sys, os
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
11
+
12
+ import numpy as np
13
+ from engine.physics.rigid_body import vec3, Vec3, length, normalize
14
+ from embodied.tasks.base import Task
15
+ from embodied.media_objects import MediaRoom
16
+
17
+
18
+ class MediaWatchTask(Task):
19
+ name = "media_watch"
20
+ terrain = "flat"
21
+ max_steps = 300
22
+ reset_noise: float = 0.12
23
+
24
+ # Kolik kroků musí agent sledovat obrazovku, aby byl úkol splněn
25
+ GAZE_TARGET = 80
26
+ # Minimální visibility score pro "dívám se"
27
+ GAZE_THRESH = 0.35
28
+
29
+ def __init__(self):
30
+ self._media: MediaRoom = None
31
+ self._gaze_steps = 0
32
+ self._patterns = ["stripe", "circle", "checkers", "arrow_left", "arrow_right"]
33
+ self._pat_idx = 0
34
+
35
+ def reset(self, body, world_env):
36
+ pat = self._patterns[self._pat_idx % len(self._patterns)]
37
+ self._pat_idx += 1
38
+ self._media = MediaRoom(world_env.phys, pattern=pat)
39
+ self._gaze_steps = 0
40
+ world_env._objects = getattr(world_env, "_objects", {})
41
+ world_env._objects["monitor_main"] = self._media.monitor
42
+ world_env._objects["speaker_main"] = self._media.speaker
43
+
44
+ def _get_forward(self, body) -> Vec3:
45
+ """Přibližný dopředný vektor hlavy agenta (z pohybu hlavy)."""
46
+ try:
47
+ head = body.bodies["head"].position
48
+ torso = body.bodies["torso"].position
49
+ fwd = head - torso
50
+ fwd_np = np.array([fwd[0], 0.0, fwd[2]], dtype=np.float64)
51
+ n = float(np.linalg.norm(fwd_np))
52
+ if n < 0.001:
53
+ return vec3(0, 0, -1)
54
+ fwd_np /= n
55
+ return vec3(float(fwd_np[0]), 0.0, float(fwd_np[2]))
56
+ except Exception:
57
+ return vec3(0, 0, -1)
58
+
59
+ def step_reward(self, body, world_env, action) -> float:
60
+ if self._media is None:
61
+ return 0.0
62
+
63
+ self._media.step()
64
+
65
+ # Pozice oka = hlava agenta
66
+ try:
67
+ eye = body.bodies["head"].position
68
+ except Exception:
69
+ eye = body.torso_pos()
70
+
71
+ forward = self._get_forward(body)
72
+ vis = self._media.monitor.visibility_from(eye, forward)
73
+ _, amp = self._media.speaker.heard_by(eye)
74
+
75
+ if vis >= self.GAZE_THRESH:
76
+ self._gaze_steps += 1
77
+ else:
78
+ self._gaze_steps = max(0, self._gaze_steps - 1)
79
+
80
+ gaze_bonus = 0.0
81
+ if self._gaze_steps >= self.GAZE_TARGET:
82
+ gaze_bonus = 5.0 # splněno!
83
+
84
+ alive = 0.03
85
+ action_pen = float(np.mean(np.abs(action))) * 0.01
86
+ return vis * 0.5 + amp * 0.1 + gaze_bonus + alive - action_pen
87
+
88
+ def is_done(self, body, world_env) -> bool:
89
+ if body.is_fallen():
90
+ return True
91
+ if self.success_condition(body, world_env):
92
+ return True
93
+ return world_env._tick >= self.max_steps
94
+
95
+ def success_condition(self, body, world_env) -> bool:
96
+ return self._gaze_steps >= self.GAZE_TARGET
embodied/tasks/obstacle_course.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Vojenská překážková dráha (opičí dráha).
3
+
4
+ Agent startuje na x=4, musí projet celou trasou (+X směr) a zdolat:
5
+ 1. Hurdle 1 (x=16-17) — přeskočit nízkou zeď
6
+ 2. Hurdle 2 (x=26-27) — přeskočit nízkou zeď
7
+ 3. Wall climb (x=35-38) — vylézt na vysokou zeď (h=4)
8
+ 4. Descent (x=39-43) — sejít ze schodů dolů
9
+ 5. Low crawl (x=46-53) — proplazit se nízkým tunelem
10
+ 6. Jump pit (x=55-57) — přeskočit jámu (bez země)
11
+ 7. Finish (x=60+) — cíl
12
+
13
+ Reward:
14
+ - Živý bonus každý krok (+0.03)
15
+ - Pokrok vpřed (delta X × weight, závisí na sekci)
16
+ - Checkpoint bonusy při prvním dosažení každého CP
17
+ - Výšková odměna při šplhání
18
+ - Penalizace za stojení na místě
19
+ - SUCCESS bonus při dosažení cíle
20
+ """
21
+ from __future__ import annotations
22
+ import numpy as np
23
+ from .base import Task
24
+ import sys, os
25
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
26
+ from engine.physics.rigid_body import vec3
27
+
28
+
29
+ # Checkpoints: (min_x, name, bonus)
30
+ _CHECKPOINTS = [
31
+ (14.0, "start_run", 5.0),
32
+ (20.0, "hurdle_1", 12.0),
33
+ (30.0, "hurdle_2", 12.0),
34
+ (40.0, "wall_climb", 20.0),
35
+ (44.0, "descent", 10.0),
36
+ (54.0, "crawl", 18.0),
37
+ (58.0, "pit_jump", 25.0),
38
+ (61.0, "finish", 40.0),
39
+ ]
40
+
41
+ # Forward progress weight per X zone (harder section → higher reward density)
42
+ def _progress_weight(x: float) -> float:
43
+ if x < 14: return 1.0 # free run
44
+ if x < 22: return 2.5 # hurdle 1 zone
45
+ if x < 32: return 2.5 # hurdle 2 zone
46
+ if x < 44: return 3.5 # wall climb + descent
47
+ if x < 55: return 2.0 # crawl tunnel
48
+ if x < 59: return 4.0 # pit jump
49
+ return 1.5 # after finish line
50
+
51
+
52
+ class ObstacleCourseTask(Task):
53
+ name = "obstacle_course"
54
+ terrain = "military"
55
+ max_steps = 600
56
+ reset_noise = 0.32 # high noise: jumping/climbing needs varied exploration
57
+
58
+ def __init__(self):
59
+ self._fallen_flag = False
60
+ self._prev_x = 4.0
61
+ self._prev_y = 2.0
62
+ self._reached_cps: set = set()
63
+ self._stall_counter = 0
64
+ self._best_x = 4.0
65
+
66
+ def reset(self, body, world_env):
67
+ body.reset(vec3(4.0, 2.0, 32.0))
68
+ self._prev_x = 4.0
69
+ self._prev_y = 2.0
70
+ self._reached_cps = set()
71
+ self._stall_counter = 0
72
+ self._best_x = 4.0
73
+ self._fallen_flag = False
74
+
75
+ def is_done(self, body, world_env) -> bool:
76
+ if body.is_fallen():
77
+ self._fallen_flag = True
78
+ if self._fallen_flag:
79
+ return True
80
+ if self.success_condition(body, world_env):
81
+ return True
82
+ return world_env._tick >= self.max_steps
83
+
84
+ def step_reward(self, body, world_env, action):
85
+ pos = body.torso_pos()
86
+ curr_x = float(pos[0])
87
+ curr_y = float(pos[1])
88
+
89
+ reward = 0.0
90
+
91
+ # Při pádu: velká jednorázová penalizace + konec
92
+ if body.is_fallen():
93
+ return -15.0
94
+
95
+ reward += 0.03 # alive bonus
96
+
97
+ # Action energy cost (discourage wasteful flailing)
98
+ action_pen = float(np.mean(np.square(action))) * 0.004
99
+ reward -= action_pen
100
+
101
+ # Forward progress (zone-weighted)
102
+ dx = curr_x - self._prev_x
103
+ w = _progress_weight(self._prev_x)
104
+ reward += dx * w
105
+
106
+ # Height bonus in wall-climb zone (encourage going UP)
107
+ if 33.0 < curr_x < 43.0:
108
+ dy = curr_y - self._prev_y
109
+ if dy > 0:
110
+ reward += dy * 4.0
111
+
112
+ # Stall penalty: if agent isn't making X progress
113
+ if curr_x <= self._best_x + 0.05:
114
+ self._stall_counter += 1
115
+ if self._stall_counter > 80:
116
+ reward -= 0.05 # gentle push to keep moving
117
+ else:
118
+ self._stall_counter = 0
119
+ self._best_x = max(self._best_x, curr_x)
120
+
121
+ # Checkpoint bonuses (one-time per checkpoint)
122
+ for min_x, cp_name, bonus in _CHECKPOINTS:
123
+ if cp_name not in self._reached_cps and curr_x >= min_x:
124
+ self._reached_cps.add(cp_name)
125
+ reward += bonus
126
+
127
+ self._prev_x = curr_x
128
+ self._prev_y = curr_y
129
+ return reward
130
+
131
+ def success_condition(self, body, world_env):
132
+ return float(body.torso_pos()[0]) >= 61.0 and not body.is_fallen()
embodied/tasks/polaris_server_task.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ┌─────────────────────────────────────────────────────────────────────────┐
2
+ # │ SUBSYSTÉM: PhantomCortex Cognitive Neural Engine — Task Layer │
3
+ # │ Modul: PolarisServerTask — Terminator se připojuje k serveru │
4
+ # │ Fyzika: Server existuje ve VoxForge světě na souřadnicích (48,0,10) │
5
+ # └─────────────────────────────────────────────────────────────────────────┘
6
+ """
7
+ PolarisServerTask — Terminator naviguje k PolarisServer v továrně
8
+ a připojí se přes CAP-Bus. Po připojení server přebírá část výpočtů.
9
+
10
+ Scénář:
11
+ 1. Server je fyzicky umístěn na konci továrny (x=48, y=0.5, z=10)
12
+ 2. Terminator musí dojít do vzdálenosti ≤ 3 m od serveru
13
+ 3. Po připojení: inference přes server (MOTOR board → fine motor reflexy)
14
+ 4. Server dělá Hebbian update každý krok kdy Terminator provádí správný pohyb
15
+ 5. Cíl: dojít k serveru + provést 50 kroků se serverem připojeným
16
+
17
+ Odměna:
18
+ +progress — pohyb směrem k serveru
19
+ +5.0 — úspěšné připojení k serveru
20
+ +0.1/krok — server-guided kroky (s ověřením konzistence akce)
21
+ -0.5 — každý krok kde server reportuje DEGRADED/OFFLINE health
22
+ SUCCESS — 50 kroků po připojení s avg odměnou > 3.0
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import numpy as np
27
+ from .base import Task
28
+
29
+ import sys, os
30
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
31
+ from engine.physics.rigid_body import vec3, length
32
+ from engine.hardware.terminator_body import TerminatorBody
33
+ from engine.hardware.polaris_server import PolarisServer
34
+
35
+
36
+ _SERVER_POS = np.array([48.0, 0.5, 10.0], dtype=np.float32)
37
+ _CONNECT_DIST = 3.0 # m — vzdálenost pro připojení
38
+ _MIN_GUIDED_STEPS = 50 # kroků připojeného provozu pro SUCCESS
39
+
40
+
41
+ class PolarisServerTask(Task):
42
+ """
43
+ Terminator → navigace k PolarisServer → připojení → guided trénink.
44
+
45
+ Ukazuje jak VoxForge fyzický server funguje jako distribuovaný
46
+ neurální koprocesor pro agenta.
47
+ """
48
+ name = "polaris_server"
49
+ terrain = "flat"
50
+ max_steps = 1000
51
+ reset_noise = 0.1
52
+ body_class = TerminatorBody
53
+
54
+ def reset(self, body, world_env):
55
+ body.reset(vec3(2.0, 2.0, 10.0))
56
+
57
+ # Server fyzicky existuje v továrně — vytvoříme ho při resetu
58
+ self._server = PolarisServer.factory_room(factory_x=48.0)
59
+
60
+ self._connected = False
61
+ self._guided_steps = 0
62
+ self._guided_reward = 0.0
63
+ self._prev_dist = float(np.linalg.norm(
64
+ body.torso_pos()[:3] - _SERVER_POS
65
+ ))
66
+ self._is_terminator = hasattr(body, "get_hardware_state")
67
+ self._body = body
68
+
69
+ # Připojení se vyřeší automaticky při prvním přiblížení
70
+
71
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
72
+ if body.is_fallen():
73
+ return -10.0
74
+
75
+ pos = np.array(body.torso_pos()[:3], dtype=np.float32)
76
+ dist = float(np.linalg.norm(pos - _SERVER_POS))
77
+ reward = 0.02 # živý bonus
78
+
79
+ # Fáze 1: navigace k serveru
80
+ if not self._connected:
81
+ progress = self._prev_dist - dist
82
+ reward += progress * 2.0 # motivace přibližovat se
83
+
84
+ # Připojení!
85
+ if dist <= _CONNECT_DIST:
86
+ self._connected = True
87
+ self._server.connect("terminator_1", task_type="fine_motor")
88
+ reward += 5.0
89
+ self._log_connect(dist)
90
+
91
+ # Fáze 2: guided kroky přes server
92
+ else:
93
+ state_vec = body.get_state()
94
+
95
+ # Server provede inference (MOTOR board)
96
+ server_action = self._server.infer("terminator_1", state_vec)
97
+
98
+ # Konzistence: jak moc se RL akce shoduje se serverovou?
99
+ consistency = float(
100
+ 1.0 - np.mean(np.abs(action - server_action)) / 2.0
101
+ )
102
+ reward += 0.1 * consistency
103
+
104
+ # Server health penalty
105
+ diag = self._server.diagnostics()
106
+ if diag.health == "DEGRADED":
107
+ reward -= 0.5
108
+ elif diag.health == "OFFLINE":
109
+ reward -= 2.0
110
+
111
+ # Hebbian: server se učí z každého dobrého pohybu
112
+ if consistency > 0.6:
113
+ self._server.learn("terminator_1", state_vec, action)
114
+
115
+ self._guided_steps += 1
116
+ self._guided_reward += reward
117
+
118
+ self._prev_dist = dist
119
+ return float(reward)
120
+
121
+ def _log_connect(self, dist: float):
122
+ diag = self._server.diagnostics()
123
+ import logging
124
+ log = logging.getLogger("phantom.embodied")
125
+ log.info(
126
+ " [PolarisServer] Terminator PŘIPOJEN @ dist=%.2fm server=%s "
127
+ "boards=%s coolant_out=%.1f°C",
128
+ dist, diag.health,
129
+ [(b["role"], b["health"]) for b in diag.board_states],
130
+ diag.coolant["outlet_temp_c"],
131
+ )
132
+
133
+ def is_done(self, body, world_env) -> bool:
134
+ if body.is_fallen():
135
+ return True
136
+ if self.success_condition(body, world_env):
137
+ return True
138
+ return world_env._tick >= self.max_steps
139
+
140
+ def success_condition(self, body, world_env) -> bool:
141
+ return (
142
+ self._connected
143
+ and self._guided_steps >= _MIN_GUIDED_STEPS
144
+ and not body.is_fallen()
145
+ )
146
+
147
+ def server_report(self) -> str:
148
+ """Volitelný výpis stavu serveru po epizodě."""
149
+ if not hasattr(self, "_server"):
150
+ return "(server nenastaven)"
151
+ return self._server.status_line()
embodied/tasks/reach.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task: reach a randomly placed target with the right hand.
2
+
3
+ Opravené tvarování odměny — silné proximity bonusy zabraňují alive-only lokálnímu optimu.
4
+ """
5
+ from __future__ import annotations
6
+ import numpy as np
7
+ from .base import Task
8
+ import sys, os
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
10
+ from engine.physics.rigid_body import vec3, length
11
+
12
+
13
+ class ReachTask(Task):
14
+ name = "reach"
15
+ terrain = "flat"
16
+ max_steps = 300
17
+ reset_noise: float = 0.25 # vyšší průzkum pro motoriku ruky
18
+
19
+ def reset(self, body, world_env):
20
+ body.reset(vec3(16.0, 2.0, 16.0))
21
+ rng = np.random.default_rng(world_env._tick + 1)
22
+ # Cíl: v dosahu ruky (blíže než dříve pro snazší učení)
23
+ angle = rng.uniform(0, 2 * np.pi)
24
+ r = rng.uniform(0.3, 0.65) # blíže (bylo 0.4–0.9)
25
+ self._target = body.torso_pos() + vec3(
26
+ float(r * np.cos(angle)),
27
+ float(rng.uniform(0.3, 0.7)),
28
+ float(r * np.sin(angle)),
29
+ )
30
+ self._prev_dist = self._hand_dist(body)
31
+
32
+ def _hand_dist(self, body):
33
+ hand = body.bodies["hand_R"]
34
+ return float(length(hand.position - self._target))
35
+
36
+ def step_reward(self, body, world_env, action):
37
+ dist = self._hand_dist(body)
38
+
39
+ # Dense proximity — even random exploration yields gradient from 1m range
40
+ if dist < 0.12: proximity = 3.0
41
+ elif dist < 0.25: proximity = 1.5
42
+ elif dist < 0.45: proximity = 0.6
43
+ elif dist < 0.70: proximity = 0.20
44
+ elif dist < 1.00: proximity = 0.06
45
+ else: proximity = 0.0
46
+
47
+ approach = (self._prev_dist - dist) * 4.0
48
+ at_target = 6.0 if dist < 0.12 else 0.0
49
+ alive = 0.03 if not body.is_fallen() else -3.0
50
+ # Penalizuj pohyb nohou (agent nesmí chodit pryč od cíle)
51
+ leg_pen = float(np.mean(np.square(action[0:4]))) * 0.15
52
+ arm_pen = float(np.mean(np.square(action[4:8]))) * 0.005
53
+ self._prev_dist = dist
54
+ return approach + proximity + at_target + alive - leg_pen - arm_pen
55
+
56
+ def success_condition(self, body, world_env):
57
+ return self._hand_dist(body) < 0.15
embodied/tasks/reach_arm.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ReachArmTask — mikroúloha pro trénink natahování paže.
3
+
4
+ Agent stojí na místě, cíl je vždy přímo vedle jeho ruky (1–3 kroky paže).
5
+ Nezabývá se chůzí — pouze pohyb ramen/loktů/zápěstí.
6
+ Slouží jako stepping stone před household_pick.
7
+ """
8
+ from __future__ import annotations
9
+ import numpy as np
10
+ from .base import Task
11
+ import sys, os
12
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
13
+ from engine.physics.rigid_body import vec3, length
14
+
15
+
16
+ class ReachArmTask(Task):
17
+ name = "reach_arm"
18
+ terrain = "flat"
19
+ max_steps = 200
20
+ reset_noise: float = 0.30
21
+
22
+ def reset(self, body, world_env):
23
+ body.reset(vec3(16.0, 2.0, 16.0))
24
+ rng = np.random.default_rng(world_env._tick + 7)
25
+ # Cíl vždy nahoru nebo do strany — čistě paže, žádná chůze
26
+ dx = float(rng.uniform(0.2, 0.55)) * float(rng.choice([-1, 1]))
27
+ dy = float(rng.uniform(0.1, 0.55))
28
+ dz = float(rng.uniform(-0.3, 0.3))
29
+ torso = body.torso_pos()
30
+ self._target = torso + vec3(dx, dy, dz)
31
+ self._prev_dist = self._dist(body)
32
+ self._best_dist = self._prev_dist
33
+
34
+ def _dist(self, body) -> float:
35
+ return float(length(body.bodies["hand_R"].position - self._target))
36
+
37
+ def step_reward(self, body, world_env, action) -> float:
38
+ dist = self._dist(body)
39
+ self._best_dist = min(self._best_dist, dist)
40
+
41
+ # Hustá odměna za blízkost — průběžná, ne jen při dosažení
42
+ if dist < 0.10: prox = 3.0
43
+ elif dist < 0.20: prox = 1.50
44
+ elif dist < 0.35: prox = 0.60
45
+ elif dist < 0.55: prox = 0.20
46
+ else: prox = 0.0
47
+
48
+ approach = (self._prev_dist - dist) * 6.0
49
+ success_b = 8.0 if dist < 0.10 else 0.0
50
+ alive = 0.04 if not body.is_fallen() else -3.0
51
+
52
+ # Penalizuj pouze pohyb nohou (kroky 0–3), paže jsou žádoucí
53
+ leg_pen = float(np.mean(np.square(action[0:4]))) * 0.05
54
+ # Drobná penalizace švihu gripem (nechceme zbytečně svírat)
55
+ grip_pen = float(action[9] ** 2) * 0.02
56
+
57
+ self._prev_dist = dist
58
+ return approach + prox + success_b + alive - leg_pen - grip_pen
59
+
60
+ def success_condition(self, body, world_env) -> bool:
61
+ return self._dist(body) < 0.10
embodied/tasks/shape_sorter.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ShapeSorterTask — agent musí vzít tři tvary (čtverec, trojúhelník, hvězda)
3
+ a vložit je do správných otvorů ve zdi.
4
+
5
+ Odměna: za každý vložený tvar +5.0, průběžně přiblížení k nejbližšímu tvaru.
6
+ """
7
+ from __future__ import annotations
8
+ import sys, os
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
10
+
11
+ import numpy as np
12
+ from engine.physics.rigid_body import vec3, length
13
+ from embodied.tasks.base import Task
14
+ from embodied.interactive_objects import ShapeSorterWall
15
+
16
+
17
+ class ShapeSorterTask(Task):
18
+ name = "shape_sorter"
19
+ terrain = "flat"
20
+ max_steps = 600
21
+ reset_noise: float = 0.30
22
+
23
+ def __init__(self):
24
+ self._wall: ShapeSorterWall = None
25
+ self._prev_hand_dist = 1.0
26
+ self._prev_inserted = 0
27
+
28
+ def reset(self, body, world_env):
29
+ rng = np.random.default_rng(world_env._tick if hasattr(world_env, '_tick') else 0)
30
+ self._wall = ShapeSorterWall(world_env.phys, rng=rng)
31
+ self._prev_inserted = 0
32
+ hand_R = body.hand_pos("R")
33
+ nearest = self._wall.nearest_piece(hand_R)
34
+ self._prev_hand_dist = (
35
+ float(length(nearest.body.position - hand_R)) if nearest else 2.0
36
+ )
37
+ world_env._objects = getattr(world_env, "_objects", {})
38
+ world_env._objects["shape_wall"] = self._wall
39
+
40
+ def step_reward(self, body, world_env, action) -> float:
41
+ if self._wall is None:
42
+ return 0.0
43
+
44
+ hand_R = body.hand_pos("R")
45
+ inserted = self._wall.check_insertions()
46
+
47
+ # Bonus za každý nově vložený tvar
48
+ insert_bonus = (inserted - self._prev_inserted) * 5.0
49
+ self._prev_inserted = inserted
50
+
51
+ # Průběžné přiblížení k nejbližšímu nevloženému tvaru
52
+ nearest = self._wall.nearest_piece(hand_R)
53
+ if nearest is not None:
54
+ hand_dist = float(length(nearest.body.position - hand_R))
55
+ approach = (self._prev_hand_dist - hand_dist) * 2.0
56
+ proximity = 0.25 if hand_dist < 0.25 else (0.08 if hand_dist < 0.7 else 0.0)
57
+ self._prev_hand_dist = hand_dist
58
+ else:
59
+ approach, proximity = 0.0, 0.0
60
+
61
+ alive = 0.04
62
+ action_pen = float(np.mean(np.abs(action))) * 0.02
63
+ return insert_bonus + approach + proximity + alive - action_pen
64
+
65
+ def is_done(self, body, world_env) -> bool:
66
+ if body.is_fallen():
67
+ return True
68
+ if self.success_condition(body, world_env):
69
+ return True
70
+ return world_env._tick >= self.max_steps
71
+
72
+ def success_condition(self, body, world_env) -> bool:
73
+ if self._wall is None:
74
+ return False
75
+ return self._wall.all_inserted()
embodied/tasks/stairs.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task: climb the staircase to the top."""
2
+ from __future__ import annotations
3
+ import numpy as np
4
+ from .base import Task
5
+ import sys, os
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
7
+ from engine.physics.rigid_body import vec3
8
+
9
+
10
+ class StairsTask(Task):
11
+ name = "stairs"
12
+ terrain = "stairs"
13
+ max_steps = 1200
14
+
15
+ def reset(self, body, world_env):
16
+ body.reset(vec3(4.0, 2.0, 32.0))
17
+ self._prev_x = 4.0
18
+
19
+ def step_reward(self, body, world_env, action):
20
+ curr_x = float(body.torso_pos()[0])
21
+ height = float(body.bodies["torso"].position[1])
22
+ progress = (curr_x - self._prev_x) * 1.5
23
+ height_r = height * 0.3
24
+ alive = 0.2 if not body.is_fallen() else -2.0
25
+ action_pen = float(np.sum(np.square(action))) * 0.005
26
+ self._prev_x = curr_x
27
+ return progress + height_r + alive - action_pen
28
+
29
+ def success_condition(self, body, world_env):
30
+ return float(body.torso_pos()[0]) > 50.0 and float(body.bodies["torso"].position[1]) > 10.0
embodied/tasks/stand.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task: stay upright as long as possible."""
2
+ from __future__ import annotations
3
+ import numpy as np
4
+ from .base import Task
5
+ import sys, os
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
7
+ from engine.physics.rigid_body import vec3
8
+
9
+
10
+ class StandTask(Task):
11
+ name = "stand"
12
+ terrain = "flat"
13
+ max_steps = 400
14
+
15
+ def reset(self, body, world_env):
16
+ body.reset(vec3(16.0, 2.0, 16.0))
17
+
18
+ def step_reward(self, body, world_env, action):
19
+ torso = body.bodies["torso"]
20
+ # Reward for high torso, penalty for large actions
21
+ height_reward = float(torso.position[1]) * 0.5
22
+ stability = 1.0 - min(abs(float(torso.rotation[0])) + abs(float(torso.rotation[2])), 1.0)
23
+ action_penalty = float(np.sum(np.square(action))) * 0.01
24
+ return height_reward + stability * 0.3 - action_penalty
25
+
26
+ def success_condition(self, body, world_env):
27
+ return world_env._tick >= self.max_steps and not body.is_fallen()
embodied/tasks/terminator_factory.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Factory manipulation task for heavy humanoid robot tools."""
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+
6
+ from embodied.tasks.base import Task
7
+ from embodied.tasks.terminator_tools import run_tool_cycle
8
+
9
+
10
+ class TerminatorFactoryTask(Task):
11
+ name = "terminator_factory"
12
+ terrain = "factory_floor"
13
+ max_steps = 1400
14
+ reset_noise = 0.10
15
+
16
+ def reset(self, body, world_env) -> None:
17
+ self._completed = 0
18
+ self._tool_cycle = ["gripper", "welder", "factory_lifter"]
19
+
20
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
21
+ tool_id = self._tool_cycle[self._completed % len(self._tool_cycle)]
22
+ telemetry = run_tool_cycle(tool_id, action)
23
+ reward = 0.05 + float(telemetry["stability"])
24
+ if telemetry["tool_ready"]:
25
+ self._completed += 1
26
+ reward += 2.0
27
+ return float(reward)
28
+
29
+ def success_condition(self, body, world_env) -> bool:
30
+ return self._completed >= 9
embodied/tasks/terminator_stand.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ┌─────────────────────────────────────────────────────────────────────────┐
2
+ # │ SUBSYSTÉM: PhantomCortex Cognitive Neural Engine — Task Layer │
3
+ # │ Modul: TerminatorStand (RL task pro Terminator tělo) │
4
+ # │ Závislost: VoxForge 3D Engine / TerminatorBody (EHA-C + POLARIS) │
5
+ # └─────────────────────────────────────────────────────────────────────────┘
6
+ """
7
+ TerminatorStand — demonstrační task pro TerminatorBody (POLARIS + EHA-C).
8
+
9
+ Cíl: Terminator se udržuje vestoje co nejdéle.
10
+ Bonus: POLARIS online Hebbovské učení ze zkušenosti.
11
+ Diagnostika: hardware stav EHA-C + POLARIS zaznamenán v info.
12
+
13
+ Kompatibilní s run_embodied.py přes TASK_REGISTRY (task_type="terminator_stand").
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import numpy as np
19
+
20
+ from embodied.tasks.base import Task
21
+ import sys, os
22
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
23
+ from engine.physics.rigid_body import vec3
24
+ from engine.hardware.terminator_body import TerminatorBody
25
+
26
+
27
+
28
+
29
+ log = logging.getLogger("phantom.terminator")
30
+
31
+
32
+ class TerminatorStandTask(Task):
33
+ """
34
+ Terminator (POLARIS + 10× EHA-C) se udržuje vestoje.
35
+
36
+ Stav: standardní 28D z TerminatorBody.get_state() — PhantomCortex přijme beze změny.
37
+ Akce: 10D — hip L/R, knee L/R, shoulder L/R, elbow L/R, grip L/R.
38
+
39
+ Reward:
40
+ +torso_height × 0.5 — odměna za výšku
41
+ +stability × 0.3 — odměna za rovnováhu (náklon)
42
+ -action_penalty × 0.01 — trest za velké příkazy
43
+ -joint_fault × 2.0 — trest za přehřátý/vypnutý EHA-C kloub
44
+ +recup_bonus × 0.001 — bonus za rekuperovanou energii
45
+ """
46
+ name = "terminator_stand"
47
+ terrain = "flat"
48
+ max_steps = 500
49
+ body_class = TerminatorBody
50
+
51
+ def reset(self, body, world_env):
52
+ self._body_is_terminator = isinstance(body, TerminatorBody)
53
+ body.reset(vec3(16.0, 2.0, 16.0))
54
+ self._prev_recup = 0.0
55
+
56
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
57
+ torso = body.bodies["torso"]
58
+ height_r = float(torso.position[1]) * 0.5
59
+ stability = 1.0 - min(
60
+ abs(float(torso.rotation[0])) + abs(float(torso.rotation[2])), 1.0
61
+ )
62
+ action_pen = float(np.sum(np.square(action))) * 0.01
63
+ reward = height_r + stability * 0.3 - action_pen
64
+
65
+ if self._body_is_terminator:
66
+ hw = body.get_hardware_state()
67
+ # Trest za výpadky EHA-C
68
+ for jname, jstate in hw["joints"].items():
69
+ if jstate["health"] in ("SHUTDOWN", "FAULT"):
70
+ reward -= 2.0
71
+ break
72
+ # Bonus za rekuperaci energie
73
+ recup_now = hw["energy"]["recup_j"]
74
+ reward += (recup_now - self._prev_recup) * 0.001
75
+ self._prev_recup = recup_now
76
+
77
+ # POLARIS online učení: aktuální stav → prováděná akce
78
+ state_vec = body.get_state()
79
+ body.polaris_learn(state_vec, action)
80
+
81
+ return float(reward)
82
+
83
+ def is_done(self, body, world_env) -> bool:
84
+ if body.is_fallen():
85
+ return True
86
+ if self._body_is_terminator and body.any_joint_shutdown():
87
+ log.warning("Terminator: EHA-C joint shutdown — epizoda ukončena")
88
+ return True
89
+ return world_env._tick >= self.max_steps
90
+
91
+ def success_condition(self, body, world_env) -> bool:
92
+ return world_env._tick >= self.max_steps and not body.is_fallen()
embodied/tasks/terminator_tools.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ┌─────────────────────────────────────────────────────────────────────────┐
2
+ # │ SUBSYSTÉM: PhantomCortex Cognitive Neural Engine — Task Layer │
3
+ # │ Modul: Jemná motorika — nářadí (svářečka, šroub, montáž) │
4
+ # │ Závislost: TerminatorBody (EHA-C wrist + POLARIS fine-motor reflexy) │
5
+ # └─────────────────────────────────────────────────────────────────────────┘
6
+ """
7
+ Tři tasky jemné motoriky pro TerminatorBody.
8
+
9
+ TerminatorWeldTask — svařování podél čáry (2 m, přesnost ±8 cm)
10
+ Fáze 1: Uchopit svářecí hořák (tool pickup)
11
+ Fáze 2: Přiblížit špičku k začátku svaru (x=12, y=3.5, z=16)
12
+ Fáze 3: Jet podél svaru (dx+), udržovat vzdálenost 4–10 cm od povrchu
13
+ Kvalita: akumuluje se za každý krok se správnou vzdáleností + stabilitou ruky
14
+
15
+ TerminatorScrewTask — utahování šroubů (5 šroubů, rotation + downforce)
16
+ Fáze 1: Přiblížit šroubovák ke šroubu (±5 cm přesnost)
17
+ Fáze 2: Zatlačit dolů (downforce > 0.3) + rotovat zápěstím
18
+ Kvalita: torque × time — čím pevněji a déle, tím lépe
19
+
20
+ TerminatorAssemblyTask — zasunutí kolíku do díry (±3 cm přesnost, nejtěžší)
21
+ Fáze 1: Uchopit kolík
22
+ Fáze 2: Navigovat kolík nad dírou (přesnost ±3 cm)
23
+ Fáze 3: Zasunout dolů (udržet osu, nenaklánět)
24
+ SUCCESS: kolík v díře, odchylka < 2 cm
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import math
29
+ import numpy as np
30
+ from .base import Task
31
+
32
+ import sys, os
33
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
34
+ from engine.physics.rigid_body import vec3, length
35
+ from engine.hardware.terminator_body import TerminatorBody
36
+
37
+
38
+ # ─────────────────────────────────────────────────────────────────────────────
39
+ # SVÁŘEČKA
40
+ # ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ class TerminatorWeldTask(Task):
43
+ """
44
+ Terminator svaří 2m šev na ocelové desce.
45
+
46
+ Klíčové pro POLARIS:
47
+ - Zápěstní EHA-C musí udržet přesný úhel hořáku
48
+ - Ruka se pohybuje pomalu a stabilně (minimální velocity)
49
+ - Hebbian: každý správný krok = posílení vzoru "stabilní zápěstí + pohyb vpřed"
50
+ """
51
+ name = "terminator_weld"
52
+ terrain = "household"
53
+ max_steps = 1500
54
+ reset_noise = 0.20
55
+ body_class = TerminatorBody
56
+
57
+ # Svařovací šev: přímá čára na stole
58
+ WELD_START = np.array([12.0, 3.5, 16.0], np.float32)
59
+ WELD_END = np.array([14.0, 3.5, 16.0], np.float32)
60
+ WELD_LEN = 2.0 # m
61
+ TORCH_POS = np.array([11.5, 3.5, 16.0], np.float32) # hořák leží vedle švu
62
+
63
+ # Optimální vzdálenost špičky hořáku od povrchu
64
+ OPT_DIST_MIN = 0.04 # 4 cm
65
+ OPT_DIST_MAX = 0.10 # 10 cm
66
+
67
+ def reset(self, body, world_env):
68
+ body.reset(vec3(10.5, 2.0, 15.5))
69
+ world_env.add_interactive_object(
70
+ "torch", "torch", vec3(*self.TORCH_POS),
71
+ size=vec3(0.04, 0.30, 0.04), mass=0.8,
72
+ )
73
+ self._phase = "pickup"
74
+ self._weld_progress = 0.0 # 0.0–1.0 (kolik švu je hotovo)
75
+ self._weld_quality = 0.0 # akumulovaná kvalita
76
+ self._prev_hand_dist = self._dist_to_torch(body, world_env)
77
+ self._prev_weld_x = self.WELD_START[0]
78
+ self._holding_steps = 0
79
+ self._stability_sum = 0.0
80
+ self._is_terminator = hasattr(body, "get_hardware_state")
81
+
82
+ def _dist_to_torch(self, body, world_env) -> float:
83
+ hp = body.hand_pos("R")
84
+ tp = world_env.get_object_pos("torch")
85
+ return float(length(hp - tp))
86
+
87
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
88
+ grip = float(action[9])
89
+ hp = body.hand_pos("R")
90
+
91
+ world_env.check_grasp("hand_R", hp, grip)
92
+ world_env.update_grasped(
93
+ {"hand_R": hp},
94
+ {"hand_R": body.bodies["hand_R"].velocity},
95
+ )
96
+
97
+ alive = 0.04 if not body.is_fallen() else -3.0
98
+ action_pen = float(np.mean(np.square(action))) * 0.002
99
+ reward = alive - action_pen
100
+
101
+ # ── Fáze 1: Uchopit hořák ────────────────────────────────────────
102
+ if self._phase == "pickup":
103
+ dist = self._dist_to_torch(body, world_env)
104
+ approach = (self._prev_hand_dist - dist) * 6.0
105
+ reward += approach
106
+ if dist < 0.20: reward += 2.0
107
+ if dist < 0.10 and grip < -0.2: reward += 3.0
108
+ self._prev_hand_dist = dist
109
+
110
+ if world_env.is_grasped("torch"):
111
+ self._phase = "approach_weld"
112
+ reward += 10.0 # uchopit hořák = velká odměna
113
+
114
+ # ── Fáze 2: Přiblížit k začátku švu ─────────────────────────────
115
+ elif self._phase == "approach_weld":
116
+ torch_pos = world_env.get_object_pos("torch")
117
+ dist_to_start = float(np.linalg.norm(torch_pos - self.WELD_START))
118
+ reward += max(0.0, (2.0 - dist_to_start) * 1.5)
119
+ reward += 0.8 # drží hořák
120
+ if dist_to_start < 0.15:
121
+ self._phase = "weld"
122
+ reward += 8.0
123
+
124
+ # ── Fáze 3: Svařování ─────────────────────────────────────────────
125
+ elif self._phase == "weld":
126
+ torch_pos = world_env.get_object_pos("torch")
127
+
128
+ # Vzdálenost špičky od povrchu stolu (y=3.5)
129
+ surface_dist = abs(float(torch_pos[1]) - 3.5)
130
+
131
+ # Laterální přesnost (Z odchylka od švu)
132
+ z_error = abs(float(torch_pos[2]) - self.WELD_START[2])
133
+
134
+ # Pokrok podél švu (X osa)
135
+ weld_x = float(torch_pos[0])
136
+ dx = weld_x - self._prev_weld_x
137
+ progress = max(0.0, dx) / self.WELD_LEN
138
+ self._weld_progress = min(1.0, self._weld_progress + progress)
139
+ self._prev_weld_x = weld_x
140
+
141
+ # Stabilita ruky (nízká velocity = jemný pohyb)
142
+ hand_vel = body.bodies["hand_R"].velocity
143
+ stability = max(0.0, 1.0 - float(length(hand_vel)) * 3.0)
144
+ self._stability_sum += stability
145
+
146
+ # Kvalita svaru: správná vzdálenost + stabilita + přesnost Z
147
+ in_range = self.OPT_DIST_MIN <= surface_dist <= self.OPT_DIST_MAX
148
+ z_ok = z_error < 0.08
149
+ if in_range and z_ok:
150
+ weld_quality = stability * 2.0
151
+ self._weld_quality += weld_quality
152
+ reward += weld_quality
153
+ reward += progress * 15.0 # pokrok vpřed
154
+ else:
155
+ reward -= z_error * 0.5 # trest za odchylku
156
+
157
+ reward += 0.5 # drží hořák
158
+
159
+ # POLARIS fine-motor update (pohyb hořáku podél čáry)
160
+ if self._is_terminator and in_range and z_ok:
161
+ body.polaris_learn(body.get_state(), action)
162
+
163
+ # Dokončení švu
164
+ if self._weld_progress >= 0.95:
165
+ quality_bonus = self._weld_quality * 0.5
166
+ reward += 50.0 + quality_bonus
167
+
168
+ return float(reward)
169
+
170
+ def is_done(self, body, world_env) -> bool:
171
+ if body.is_fallen(): return True
172
+ if self._weld_progress >= 0.95: return True
173
+ return world_env._tick >= self.max_steps
174
+
175
+ def success_condition(self, body, world_env) -> bool:
176
+ return self._weld_progress >= 0.95
177
+
178
+ def get_weld_stats(self) -> dict:
179
+ return {
180
+ "progress": round(self._weld_progress * 100, 1),
181
+ "quality": round(self._weld_quality, 2),
182
+ "stability": round(self._stability_sum / max(1, self._holding_steps), 3),
183
+ }
184
+
185
+
186
+ # ─────────────────────────────────────────────────────────────────────────────
187
+ # ŠROUBOVÁK
188
+ # ─────────────────────────────────────────────────────────────────────────────
189
+
190
+ class TerminatorScrewTask(Task):
191
+ """
192
+ Terminator utáhne 5 šroubů šroubovákem.
193
+
194
+ Klíčové: kombinace downforce (EHA-C loket dolů) + rotace zápěstí (grip action).
195
+ POLARIS se naučí koordinaci loket↓ + zápěstí→ jako jeden reflex.
196
+ """
197
+ name = "terminator_screw"
198
+ terrain = "household"
199
+ max_steps = 1800
200
+ reset_noise = 0.18
201
+ body_class = TerminatorBody
202
+
203
+ SCREWS = [
204
+ np.array([12.0, 3.5, 15.5], np.float32),
205
+ np.array([12.5, 3.5, 15.5], np.float32),
206
+ np.array([13.0, 3.5, 15.5], np.float32),
207
+ np.array([13.5, 3.5, 15.5], np.float32),
208
+ np.array([14.0, 3.5, 15.5], np.float32),
209
+ ]
210
+ DRIVER_POS = np.array([11.5, 3.5, 15.5], np.float32)
211
+
212
+ def reset(self, body, world_env):
213
+ body.reset(vec3(10.5, 2.0, 15.0))
214
+ world_env.add_interactive_object(
215
+ "screwdriver", "tool", vec3(*self.DRIVER_POS),
216
+ size=vec3(0.03, 0.25, 0.03), mass=0.4,
217
+ )
218
+ self._phase = "pickup"
219
+ self._screws_done = 0
220
+ self._current_screw = 0
221
+ self._torque_accum = 0.0
222
+ self._prev_hand_dist = self._dist_hand_to(body, world_env, self.DRIVER_POS)
223
+ self._is_terminator = hasattr(body, "get_hardware_state")
224
+
225
+ def _dist_hand_to(self, body, world_env, target: np.ndarray) -> float:
226
+ hp = body.hand_pos("R")
227
+ return float(np.linalg.norm(np.array([hp[0], hp[1], hp[2]]) - target))
228
+
229
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
230
+ grip = float(action[9])
231
+ hp = body.hand_pos("R")
232
+
233
+ world_env.check_grasp("hand_R", hp, grip)
234
+ world_env.update_grasped(
235
+ {"hand_R": hp},
236
+ {"hand_R": body.bodies["hand_R"].velocity},
237
+ )
238
+
239
+ alive = 0.04 if not body.is_fallen() else -3.0
240
+ action_pen = float(np.mean(np.square(action))) * 0.002
241
+ reward = alive - action_pen
242
+
243
+ # ── Pickup ───────────────────────────────────────────────────────
244
+ if self._phase == "pickup":
245
+ dist = self._dist_hand_to(body, world_env, self.DRIVER_POS)
246
+ reward += (self._prev_hand_dist - dist) * 5.0
247
+ if dist < 0.15: reward += 1.5
248
+ if dist < 0.08 and grip < -0.2: reward += 2.0
249
+ self._prev_hand_dist = dist
250
+ if world_env.is_grasped("screwdriver"):
251
+ self._phase = "approach_screw"
252
+ reward += 8.0
253
+
254
+ # ── Přiblížit k šroubu ────────────────────────────────────────────
255
+ elif self._phase == "approach_screw":
256
+ if self._current_screw >= len(self.SCREWS):
257
+ return reward + 30.0
258
+ screw_pos = self.SCREWS[self._current_screw]
259
+ driver_pos = world_env.get_object_pos("screwdriver")
260
+ dist = float(np.linalg.norm(
261
+ np.array([driver_pos[0], driver_pos[1], driver_pos[2]]) - screw_pos
262
+ ))
263
+ reward += max(0.0, (1.5 - dist) * 2.0)
264
+ reward += 0.3 # drží nástroj
265
+ if dist < 0.06:
266
+ self._phase = "screw"
267
+ self._torque_accum = 0.0
268
+ reward += 5.0
269
+
270
+ # ── Šroubování ────────────────────────────────────────────────────
271
+ elif self._phase == "screw":
272
+ driver_pos = world_env.get_object_pos("screwdriver")
273
+ screw_pos = self.SCREWS[self._current_screw]
274
+ dist = float(np.linalg.norm(
275
+ np.array([driver_pos[0], driver_pos[1], driver_pos[2]]) - screw_pos
276
+ ))
277
+
278
+ # Downforce: loket dolů (action[6] < 0 = loket dolů)
279
+ downforce = max(0.0, -float(action[6]))
280
+ # Rotace zápěstí: grip oscilace (simuluje rotaci šroubováku)
281
+ wrist_rot = abs(float(action[9]))
282
+ torque = downforce * wrist_rot
283
+
284
+ if dist < 0.08:
285
+ self._torque_accum += torque
286
+ reward += torque * 3.0
287
+
288
+ # POLARIS: naučit koordinaci loket↓ + grip
289
+ if self._is_terminator and torque > 0.1:
290
+ body.polaris_learn(body.get_state(), action)
291
+
292
+ if self._torque_accum > 8.0: # dost torque = šroub utažen
293
+ self._screws_done += 1
294
+ self._current_screw += 1
295
+ self._torque_accum = 0.0
296
+ reward += 12.0
297
+ if self._current_screw < len(self.SCREWS):
298
+ self._phase = "approach_screw"
299
+ else:
300
+ self._phase = "done"
301
+ else:
302
+ reward -= 0.1 # ztratil kontakt
303
+
304
+ elif self._phase == "done":
305
+ reward += 1.0 # hotovo, čeká na konec epizody
306
+
307
+ return float(reward)
308
+
309
+ def success_condition(self, body, world_env) -> bool:
310
+ return self._screws_done >= len(self.SCREWS)
311
+
312
+
313
+ # ─────────────────────────────────────────────────────────────────────────────
314
+ # MONTÁŽ — KOLÍK DO DÍRY (nejtěžší)
315
+ # ─────────────────────────────────────────────────────────────────────────────
316
+
317
+ class TerminatorAssemblyTask(Task):
318
+ """
319
+ Terminator zasune válcový kolík do přesné díry (tolerance ±2 cm).
320
+
321
+ Toto je nejtěžší task jemné motoriky:
322
+ - Musí udržet kolík svisle (nenaklánět) při zasouvání
323
+ - Tolerance ±2 cm vyžaduje přesnou koordinaci obou rukou
324
+ - POLARIS se naučí "svislý grip" jako dedikovaný reflex
325
+ """
326
+ name = "terminator_assembly"
327
+ terrain = "household"
328
+ max_steps = 2000
329
+ reset_noise = 0.15
330
+ body_class = TerminatorBody
331
+
332
+ PEG_POS = np.array([12.0, 3.5, 16.0], np.float32) # kolík
333
+ HOLE_POS = np.array([13.5, 3.5, 16.0], np.float32) # díra
334
+ HOLE_R = 0.04 # poloměr díry (4 cm tolerance)
335
+ INSERT_DEPTH = 0.15 # m jak hluboko zasunout
336
+
337
+ def reset(self, body, world_env):
338
+ body.reset(vec3(11.0, 2.0, 15.5))
339
+ world_env.add_interactive_object(
340
+ "peg", "peg", vec3(*self.PEG_POS),
341
+ size=vec3(0.04, 0.20, 0.04), mass=0.3,
342
+ )
343
+ self._phase = "pickup"
344
+ self._inserted = False
345
+ self._insert_depth = 0.0
346
+ self._tilt_penalty = 0.0
347
+ self._prev_dist = self._hand_to_peg(body, world_env)
348
+ self._is_terminator = hasattr(body, "get_hardware_state")
349
+
350
+ def _hand_to_peg(self, body, world_env) -> float:
351
+ hp = body.hand_pos("R")
352
+ pp = world_env.get_object_pos("peg")
353
+ return float(length(hp - pp))
354
+
355
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
356
+ grip = float(action[9])
357
+ hp = body.hand_pos("R")
358
+
359
+ world_env.check_grasp("hand_R", hp, grip)
360
+ world_env.update_grasped(
361
+ {"hand_R": hp},
362
+ {"hand_R": body.bodies["hand_R"].velocity},
363
+ )
364
+
365
+ alive = 0.05 if not body.is_fallen() else -3.0
366
+ action_pen = float(np.mean(np.square(action))) * 0.002
367
+ reward = alive - action_pen
368
+
369
+ # ── Pickup kolíku ────────────────────────────────────────────────
370
+ if self._phase == "pickup":
371
+ dist = self._hand_to_peg(body, world_env)
372
+ reward += (self._prev_dist - dist) * 7.0
373
+ if dist < 0.15: reward += 2.0
374
+ if dist < 0.08 and grip < -0.3: reward += 4.0
375
+ self._prev_dist = dist
376
+ if world_env.is_grasped("peg"):
377
+ self._phase = "align"
378
+ reward += 12.0
379
+
380
+ # ── Zarovnat kolík nad díru ────────────────────────────────────────
381
+ elif self._phase == "align":
382
+ peg_pos = world_env.get_object_pos("peg")
383
+ peg_arr = np.array([peg_pos[0], peg_pos[1], peg_pos[2]])
384
+ # XZ vzdálenost od díry (ignoruj Y — výška)
385
+ xz_dist = float(np.sqrt(
386
+ (peg_arr[0] - self.HOLE_POS[0])**2 +
387
+ (peg_arr[2] - self.HOLE_POS[2])**2
388
+ ))
389
+ reward += max(0.0, (2.0 - xz_dist) * 3.0)
390
+ reward += 0.5 # drží kolík
391
+
392
+ # Stabilita (nenaklánět)
393
+ hand_vel = float(length(body.bodies["hand_R"].velocity))
394
+ reward -= hand_vel * 0.3
395
+
396
+ if xz_dist < self.HOLE_R * 2:
397
+ self._phase = "insert"
398
+ reward += 15.0
399
+
400
+ # ── Zasouvání ─────────────────────────────────────────────────────
401
+ elif self._phase == "insert":
402
+ peg_pos = world_env.get_object_pos("peg")
403
+ peg_arr = np.array([peg_pos[0], peg_pos[1], peg_pos[2]])
404
+
405
+ xz_dist = float(np.sqrt(
406
+ (peg_arr[0] - self.HOLE_POS[0])**2 +
407
+ (peg_arr[2] - self.HOLE_POS[2])**2
408
+ ))
409
+
410
+ # Pohyb dolů (loket dolů = action[6] < 0)
411
+ downward = max(0.0, -float(action[6]))
412
+ if xz_dist < self.HOLE_R:
413
+ # V díře → zasouvej dolů
414
+ dy = max(0.0, float(peg_arr[1]) - (self.HOLE_POS[1] - self._insert_depth))
415
+ self._insert_depth += downward * 0.002
416
+ self._insert_depth = min(self._insert_depth, self.INSERT_DEPTH)
417
+ reward += downward * 5.0
418
+ reward += 1.0 # v díře
419
+
420
+ # POLARIS: nácvik přesného svislého zasouvání
421
+ if self._is_terminator:
422
+ body.polaris_learn(body.get_state(), action)
423
+
424
+ if self._insert_depth >= self.INSERT_DEPTH * 0.9:
425
+ self._inserted = True
426
+ reward += 60.0 # SUCCESS!
427
+ else:
428
+ # Mimo díru — penalizace za odchylku
429
+ reward -= xz_dist * 5.0
430
+ reward -= 0.5
431
+
432
+ # Stabilita při zasouvání
433
+ hand_vel = float(length(body.bodies["hand_R"].velocity))
434
+ tilt_pen = hand_vel * 0.5
435
+ self._tilt_penalty += tilt_pen
436
+ reward -= tilt_pen
437
+
438
+ return float(reward)
439
+
440
+ def is_done(self, body, world_env) -> bool:
441
+ if body.is_fallen(): return True
442
+ if self._inserted: return True
443
+ return world_env._tick >= self.max_steps
444
+
445
+ def success_condition(self, body, world_env) -> bool:
446
+ return self._inserted
embodied/tasks/timed_nav.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TimedNavigationTask — dojeď na cílový bod ve městě za daný čas.
3
+
4
+ Tři módy emergentního chování:
5
+
6
+ mode="legal" — přísné penalizace za rychlost
7
+ → agent se učí OPTIMALIZOVAT TRASU: zkratky, volné ulice,
8
+ vyhýbání uzávěrkám, správný výběr silnic (alej vs boulevard).
9
+ Porušení limitu = silná penalizace → nevyplatí se.
10
+
11
+ mode="realistic" — měkké penalizace, časový bonus
12
+ → agent se učí VYVAŽOVAT: na prázdné D1 pojede 150, v centru
13
+ dodrží 50 (semafory + ret.). Emerguje realistické řidičské
14
+ chování jako u skutečného řidiče ve spěchu.
15
+
16
+ mode="race" — žádné rychlostní penalizace, jen čas
17
+ → čistý závodník: agent se naučí max rychlost na každém úseku,
18
+ ignoruje retardéry, semafory, jede nejkratší vzdáleností bez
19
+ ohledu na typ silnice.
20
+
21
+ Odměnová funkce:
22
+ +goal_bonus : dosažení cíle (+100 legal, +80 race)
23
+ +time_bonus : zbývající čas při dosažení cíle (0..+50)
24
+ +progress_reward : per-krok přiblížení k cíli
25
+ -time_penalty : per-krok odečet (tlak → nestůj!)
26
+ -speed_violation : překročení limitu (pouze legal/realistic)
27
+ -closure_pen : uzávěrka
28
+ -crash_pen : havárie -30
29
+ +regen_bonus : rekuperace (jen legal)
30
+
31
+ Observation (26D stejná jako DriveCity + rozšíření):
32
+ [0:18] vehicle telemetrie
33
+ [18:24] city context (WP dist, heading, light, closure...)
34
+ [24] vzdálenost k cíli (normalizovaná)
35
+ [25] zbývající čas (normalizovaný 0-1)
36
+ """
37
+ from __future__ import annotations
38
+
39
+ import math
40
+ import random
41
+ from dataclasses import dataclass
42
+ from typing import List, Optional, Tuple
43
+
44
+ import numpy as np
45
+
46
+ from embodied.tasks.base import Task
47
+ from embodied.vehicle_body import VehicleBody
48
+ from engine.city_builder.city_gen import CityGenerator, build_traffic_network
49
+ from engine.city_builder.road_network import RoadNetwork
50
+ from engine.city_builder.traffic import IntersectionNetwork
51
+ from engine.city_builder.traffic_sim import TrafficSimulation
52
+
53
+
54
+ # ── Konstanty ─────────────────────────────────────────────────────────────────
55
+
56
+ _GOAL_RADIUS = 30.0 # m — jak blízko = dosažení cíle
57
+ _CRASH_PEN = -30.0
58
+ _CLOSURE_PEN = -3.0
59
+
60
+ # Rychlostní penalizace dle módu (per km/h překročení / 100)
61
+ _SPEED_PEN = {
62
+ "legal": -0.08, # silná
63
+ "realistic": -0.02, # měkká
64
+ "race": 0.0, # žádná
65
+ }
66
+
67
+ # Časový bonus za cíl (base + zbývající čas)
68
+ _GOAL_BONUS = {
69
+ "legal": 100.0,
70
+ "realistic": 80.0,
71
+ "race": 60.0,
72
+ }
73
+
74
+ # Náhodné starty/cíle rozmístěné po celé síti
75
+ _CITY_POINTS = [
76
+ # (x, z, label)
77
+ (-1500, -1500, "start_JZ"),
78
+ ( 1500, -1500, "start_JV"),
79
+ ( 1500, 1500, "start_SV"),
80
+ (-1500, 1500, "start_SZ"),
81
+ ( 0, 0, "centrum"),
82
+ ( -600, -600, "centrum_JZ"),
83
+ ( 600, 600, "centrum_SV"),
84
+ (-2800, -2800, "D1_západ"),
85
+ ( 3000, -2800, "D1_východ"),
86
+ ( 800, -800, "tunel_jih"),
87
+ ( -800, 800, "tunel_sever"),
88
+ ( 1800, -1800, "ring_JV"),
89
+ (-1800, 1800, "ring_SZ"),
90
+ ( 0, 1200, "sever"),
91
+ ( 0, -2000, "jih_centrum"),
92
+ ]
93
+
94
+
95
+ # ── Helper ────────────────────────────────────────────────────────────────────
96
+
97
+ def _dist(ax, az, bx, bz) -> float:
98
+ return math.sqrt((ax - bx) ** 2 + (az - bz) ** 2)
99
+
100
+
101
+ def _optimal_time(dist_m: float, avg_speed_kmh: float = 65.0) -> float:
102
+ """Odhadovaný čas (s) pro zdatného řidiče na dané vzdálenosti."""
103
+ return dist_m / (avg_speed_kmh / 3.6)
104
+
105
+
106
+ # ── Task ──────────────────────────────────────────────────────────────────────
107
+
108
+ class TimedNavigationTask(Task):
109
+ """
110
+ Navigace bod→bod s časovým limitem.
111
+ Záměrně provokuje emergentní chování dle zvoleného módu.
112
+ """
113
+
114
+ name = "timed_nav"
115
+ terrain = "city"
116
+ max_steps = 4000 # 64 s fyzikálního času @ 16ms/step
117
+ reset_noise = 0.10
118
+
119
+ def __init__(self, mode: str = "realistic", seed: int = 0, city_seed: int = 42):
120
+ assert mode in ("legal", "realistic", "race"), f"Neznámý mód: {mode}"
121
+ self.mode = mode
122
+ self._rng = random.Random(seed)
123
+ self._road_net = CityGenerator(seed=city_seed, closure_count=4).generate()
124
+ self._network = build_traffic_network(self._road_net)
125
+ # Hustota provozu dle módu: race jede po skoro prázdných silnicích,
126
+ # realistic má normální provoz, legal jede v hustém provozu
127
+ _density = {"legal": 1.2, "realistic": 0.8, "race": 0.4}[mode]
128
+ self._traffic = TrafficSimulation(
129
+ self._road_net, density_scale=_density, seed=city_seed, max_cars=80)
130
+ self._traffic.set_traffic_lights(self._network)
131
+ self._vehicle = VehicleBody.phantom_vertex()
132
+ self._goal_pos: Tuple[float, float] = (0.0, 0.0)
133
+ self._goal_label = ""
134
+ self._start_pos: Tuple[float, float] = (0.0, 0.0)
135
+ self._time_limit_s = 120.0
136
+ self._elapsed_s = 0.0
137
+ self._reached_goal = False
138
+ self._dist_to_goal = 0.0
139
+ self._prev_dist = 0.0
140
+ self._sim_time = 0.0
141
+ self._speed_violations = 0
142
+ self._total_regen = 0.0
143
+ self._shortcut_bonus_given = False
144
+
145
+ # ── Reset ─────────────────────────────────────────────────────────────────
146
+
147
+ def reset(self, body, world_env) -> None:
148
+ # Náhodně vyber start a cíl (minimálně 800 m od sebe)
149
+ pts = _CITY_POINTS.copy()
150
+ self._rng.shuffle(pts)
151
+ start = pts[0]
152
+ goal = next((p for p in pts[1:] if _dist(p[0], p[1], start[0], start[1]) > 800), pts[-1])
153
+
154
+ self._start_pos = (start[0], start[1])
155
+ self._goal_pos = (goal[0], goal[1])
156
+ self._goal_label = goal[2] # label
157
+
158
+ dist_m = _dist(start[0], start[1], goal[0], goal[1])
159
+
160
+ # Časový limit: závisí na módu
161
+ # legal: velkorysejší (agent musí jet dobře, ne rychle)
162
+ # race: těsný (musí jet max)
163
+ margin = {"legal": 2.0, "realistic": 1.5, "race": 1.1}[self.mode]
164
+ self._time_limit_s = _optimal_time(dist_m) * margin
165
+ self._elapsed_s = 0.0
166
+ self._sim_time = 0.0
167
+ self._reached_goal = False
168
+ self._prev_dist = dist_m
169
+ self._dist_to_goal = dist_m
170
+ self._speed_violations = 0
171
+ self._total_regen = 0.0
172
+ self._shortcut_bonus_given = False
173
+ self._traffic_collisions = 0
174
+
175
+ self._vehicle.reset(position=(start[0], 0.0, start[1]), heading_rad=0.0)
176
+ self._traffic.reset()
177
+
178
+ # Randomizuj uzávěrky v silniční síti
179
+ for seg in self._road_net.segments.values():
180
+ if "closure" in seg.features:
181
+ seg.features.remove("closure")
182
+ eligible = [s for s in self._road_net.segments.values()
183
+ if s.road_type in ("city", "boulevard")]
184
+ for seg in self._rng.sample(eligible, k=min(4, len(eligible))):
185
+ seg.features.append("closure")
186
+
187
+ # ── Step reward ───────────────────────────────────────────────────────────
188
+
189
+ def step_reward(self, body, world_env, action: np.ndarray) -> float:
190
+ throttle = float(np.clip(action[0], 0.0, 1.0))
191
+ brake = float(np.clip(-min(action[0], 0.0), 0.0, 1.0))
192
+ steer = float(np.clip(action[2] if len(action) > 2 else 0.0, -1.0, 1.0))
193
+
194
+ tel = self._vehicle.step(throttle, brake, steer)
195
+ dt = self._vehicle.DT
196
+ self._elapsed_s += dt
197
+ self._sim_time += dt
198
+
199
+ px, _, pz = self._vehicle.position()
200
+ gx, gz = self._goal_pos
201
+ reward = 0.0
202
+
203
+ # ── Přiblížení k cíli ─────────────────────────────────────────────────
204
+ dist = _dist(px, pz, gx, gz)
205
+ progress = (self._prev_dist - dist) * 0.06
206
+ reward += max(-0.3, progress)
207
+ self._prev_dist = dist
208
+ self._dist_to_goal = dist
209
+
210
+ # ── Dosažení cíle ─────────────────────────────────────────────────────
211
+ if dist < _GOAL_RADIUS and not self._reached_goal:
212
+ self._reached_goal = True
213
+ time_remaining = max(0.0, self._time_limit_s - self._elapsed_s)
214
+ time_bonus = (time_remaining / self._time_limit_s) * 50.0
215
+ reward += _GOAL_BONUS[self.mode] + time_bonus
216
+ # Legal mód: bonus za rekuperaci (ekologický řidič)
217
+ if self.mode == "legal":
218
+ reward += self._total_regen * 5.0
219
+
220
+ # ── Rychlostní penalizace ─────────────────────────────────────────────
221
+ speed_pen = _SPEED_PEN[self.mode]
222
+ if speed_pen < 0.0:
223
+ seg = self._road_net.segment_at(px, pz)
224
+ limit = seg.speed_limit if seg else 50.0
225
+ excess = max(0.0, tel.speed_kmh - limit)
226
+ if excess > 5.0:
227
+ self._speed_violations += 1
228
+ reward += speed_pen * excess
229
+
230
+ # ── Semafor ──────────────────────────────────────────���────────────────
231
+ self._network.step(self._sim_time, dt)
232
+ if self.mode in ("legal", "realistic"):
233
+ near_junc = self._road_net.nearest_junction(px, pz)
234
+ if near_junc and near_junc.has_light and near_junc.dist_to(px, pz) < 20.0:
235
+ lt = self._network.lights.get(near_junc.light_id)
236
+ if lt:
237
+ direction = "EW" if abs(near_junc.pos[0] - px) > abs(near_junc.pos[1] - pz) else "NS"
238
+ if not lt.allows(direction) and tel.speed_kmh > 10.0:
239
+ reward -= 6.0 if self.mode == "legal" else 2.0
240
+
241
+ # ── Uzávěrky / retardéry ─────────────────────────────────────────────
242
+ if self._road_net.is_blocked(px, pz):
243
+ reward += _CLOSURE_PEN
244
+
245
+ seg = self._road_net.segment_at(px, pz)
246
+ if seg and "speed_bump" in seg.features:
247
+ # HX-1 adaptivní EM tlumiče rekuperují energii z komprese pružení
248
+ # Vyšší rychlost = větší komprese = více rekuperované energie
249
+ damper_regen = tel.speed_kmh * 0.0003
250
+ self._total_regen += damper_regen * dt / 3600.0
251
+ reward += damper_regen * 0.004
252
+
253
+ # ── Zkratka — alej / servisní silnice ────────────────────────────────
254
+ # legal mód: odměn za chytré využití aleje nebo service road
255
+ if self.mode == "legal" and seg and seg.road_type == "alley":
256
+ if not self._shortcut_bonus_given and progress > 0.05:
257
+ reward += 2.0 # objevil zkratku!
258
+ self._shortcut_bonus_given = True
259
+
260
+ # ── Rekuperace ────────────────────────────────────────────────────────
261
+ regen = tel.regen_kw
262
+ self._total_regen += regen * dt / 3600.0
263
+ if self.mode in ("legal", "realistic"):
264
+ reward += regen * 0.002
265
+
266
+ # ── Časový tlak ───────────────────────────────────────────────────────
267
+ time_left = self._time_limit_s - self._elapsed_s
268
+ # Per-krok penalizace (exponenciálně roste jak čas ubývá)
269
+ urgency = 1.0 + max(0.0, 1.0 - time_left / self._time_limit_s) * 2.0
270
+ reward -= 0.02 * urgency
271
+
272
+ # ── Dopravní provoz — fyzika kolizí ──────────────────────────────────
273
+ agent_heading = self._vehicle.heading()
274
+ agent_speed_ms = tel.speed_kmh / 3.6
275
+ traffic_events = self._traffic.step(dt, px, pz, agent_speed_ms, agent_heading)
276
+ for ev in traffic_events:
277
+ self._traffic_collisions += 1
278
+ if ev.is_crash:
279
+ crash_pen = -25.0 - min(15.0, ev.delta_v_agent * 2.0)
280
+ reward += crash_pen
281
+ else:
282
+ reward -= 2.0 if self.mode == "legal" else 1.0
283
+
284
+ # ── Havárie (vlastní vozidlo) ─────────────────────────────────────────
285
+ if self._vehicle.is_crashed():
286
+ reward += _CRASH_PEN
287
+
288
+ return float(reward)
289
+
290
+ # ── Terminace ─────────────────────────────────────────────────────────────
291
+
292
+ def is_done(self, body, world_env) -> bool:
293
+ if self._reached_goal:
294
+ return True
295
+ if self._elapsed_s >= self._time_limit_s:
296
+ return True
297
+ if self._vehicle.is_crashed():
298
+ return True
299
+ return world_env._tick >= self.max_steps
300
+
301
+ def success_condition(self, body, world_env) -> bool:
302
+ return self._reached_goal
303
+
304
+ def food_event(self, body, world_env) -> tuple:
305
+ if self._reached_goal:
306
+ return True, 1.0, 0.40
307
+ # Postupný přísun energie za přibližování
308
+ if self._dist_to_goal < 200.0:
309
+ return True, 0.10, 0.04
310
+ return False, 0.0, 0.0
311
+
312
+ # ── State override (stejný vzor jako DriveCity) ───────────────────────────
313
+
314
+ def get_task_state(self, body_state: np.ndarray) -> np.ndarray:
315
+ state = body_state.copy()
316
+ # [0:18] vehicle telemetrie
317
+ state[0:18] = np.array(self._vehicle.observation_vec(), dtype=np.float32)
318
+ # [18:24] základní city ctx
319
+ px, _, pz = self._vehicle.position()
320
+ gx, gz = self._goal_pos
321
+ dist_norm = float(np.clip(self._dist_to_goal / 5000.0, 0.0, 1.0))
322
+ heading = self._vehicle.heading()
323
+ goal_head = math.atan2(gz - pz, gx - px)
324
+ ang_err = (goal_head - heading + math.pi) % (2 * math.pi) - math.pi
325
+ time_norm = float(np.clip(1.0 - self._elapsed_s / max(self._time_limit_s, 1.0), 0.0, 1.0))
326
+ state[18] = dist_norm
327
+ state[19] = float(ang_err / math.pi) # směr k cíli
328
+ state[20] = time_norm # zbývající čas
329
+ state[21] = float(self._speed_violations / max(self._speed_violations + 1, 1))
330
+ state[22] = 1.0 if self._road_net.is_blocked(px, pz) else 0.0
331
+ state[23] = self._road_net.speed_limit_at(px, pz) / 130.0
332
+ state[24] = dist_norm
333
+ state[25] = time_norm
334
+ return state
335
+
336
+ # ── Info ──────────────────────────────────────────────────────────────────
337
+
338
+ def episode_info(self) -> dict:
339
+ tel = self._vehicle.state.telemetry
340
+ return {
341
+ "mode": self.mode,
342
+ "goal": self._goal_label,
343
+ "dist_to_goal_m": round(self._dist_to_goal, 1),
344
+ "elapsed_s": round(self._elapsed_s, 1),
345
+ "time_limit_s": round(self._time_limit_s, 1),
346
+ "time_remaining_s": round(max(0.0, self._time_limit_s - self._elapsed_s), 1),
347
+ "speed_violations": self._speed_violations,
348
+ "reached_goal": self._reached_goal,
349
+ "speed_kmh": round(tel.speed_kmh, 1),
350
+ "regen_kwh": round(self._total_regen, 4),
351
+ "traffic_cars": self._traffic.active_count,
352
+ "traffic_collisions": self._traffic_collisions,
353
+ }
354
+
355
+
356
+ # ── Tovární funkce ────────────────────────────────────────────────────────────
357
+
358
+ class TimedNavLegal(TimedNavigationTask):
359
+ """Přísná varianta — agent se musí naučit trasu, ne jen rychlost."""
360
+ name = "timed_nav_legal"
361
+ def __init__(self, seed=0): super().__init__(mode="legal", seed=seed)
362
+
363
+ class TimedNavRealistic(TimedNavigationTask):
364
+ """Realistická varianta — emerguje řidičské chování ve spěchu."""
365
+ name = "timed_nav_realistic"
366
+ def __init__(self, seed=0): super().__init__(mode="realistic", seed=seed)
367
+
368
+ class TimedNavRace(TimedNavigationTask):
369
+ """Závodní varianta — čistý rychlostní optimalizátor."""
370
+ name = "timed_nav_race"
371
+ def __init__(self, seed=0): super().__init__(mode="race", seed=seed)