Spaces:
Paused
Paused
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- embodied/__init__.py +0 -0
- embodied/anti_stasis.py +16 -0
- embodied/gemini_commander.py +416 -0
- embodied/goal_encoder.py +146 -0
- embodied/interactive_objects.py +374 -0
- embodied/intrinsic.py +290 -0
- embodied/language_goal_bridge.py +263 -0
- embodied/media_objects.py +276 -0
- embodied/observer.py +135 -0
- embodied/phantom_acc.py +110 -0
- embodied/phantom_biocor.py +120 -0
- embodied/phantom_cortex.py +121 -0
- embodied/phantom_memory.py +152 -0
- embodied/physics_world.py +651 -0
- embodied/pretrain/__init__.py +0 -0
- embodied/pretrain/bc_driving.py +416 -0
- embodied/pretrain/bc_ice_driving.py +419 -0
- embodied/pretrain/bc_pretrain.py +253 -0
- embodied/pretrain/motion_dataset.py +346 -0
- embodied/rl_loop.py +503 -0
- embodied/run_embodied.py +246 -0
- embodied/tasks/__init__.py +12 -0
- embodied/tasks/base.py +34 -0
- embodied/tasks/basketball.py +403 -0
- embodied/tasks/basketball_1v1.py +284 -0
- embodied/tasks/basketball_npc.py +287 -0
- embodied/tasks/drive_city.py +413 -0
- embodied/tasks/drive_city_echo.py +68 -0
- embodied/tasks/drive_city_polaris.py +107 -0
- embodied/tasks/household_ball.py +66 -0
- embodied/tasks/household_door.py +63 -0
- embodied/tasks/household_pick.py +99 -0
- embodied/tasks/household_sit.py +68 -0
- embodied/tasks/household_stack.py +117 -0
- embodied/tasks/household_sweep.py +78 -0
- embodied/tasks/ice_driving.py +358 -0
- embodied/tasks/ice_driving_polaris.py +164 -0
- embodied/tasks/maze.py +36 -0
- embodied/tasks/media_watch.py +96 -0
- embodied/tasks/obstacle_course.py +132 -0
- embodied/tasks/polaris_server_task.py +151 -0
- embodied/tasks/reach.py +57 -0
- embodied/tasks/reach_arm.py +61 -0
- embodied/tasks/shape_sorter.py +75 -0
- embodied/tasks/stairs.py +30 -0
- embodied/tasks/stand.py +27 -0
- embodied/tasks/terminator_factory.py +30 -0
- embodied/tasks/terminator_stand.py +92 -0
- embodied/tasks/terminator_tools.py +446 -0
- 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)
|