File size: 13,509 Bytes
3d46076
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
"""Autonomy engine (STAGE E): self-generated goals, compositional actions,
self-evaluation. The organism decides what to inspect, where to go, what to
repeat and what to avoid — humans do not issue per-task commands.

Brain-driven contract (mission rule 13): candidates are MODULATED by neural
state (motor readout activations, drives, prediction error); the engine never
bypasses the brain — it composes continuous control from it.
"""
import math
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple

import numpy as np

from src.common.determinism import derive_subseed


class GoalSource(str, Enum):
    NEED = "need"                 # unmet homeostatic need (energy, safety)
    CURIOSITY = "curiosity"       # internal drive for novelty
    PREDICTION_ERROR = "prediction_error"
    OPPORTUNITY = "opportunity"   # environmental (observed resource etc.)
    MEMORY = "memory"             # remembered success/failure
    SOCIAL = "social"


class GoalStatus(str, Enum):
    ACTIVE = "ACTIVE"
    ACHIEVED = "ACHIEVED"
    ABANDONED = "ABANDONED"


@dataclass
class Goal:
    goal_id: str
    source: str
    kind: str            # forage | explore | investigate | rest | socialize | avoid
    target: Optional[Tuple[int, int]]
    priority: float
    created_tick: int
    status: str = GoalStatus.ACTIVE.value
    progress: float = 0.0
    ticks_active: int = 0
    evidence: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        d = dict(self.__dict__)
        d["target"] = list(self.target) if self.target else None
        return d


@dataclass
class ActionCandidate:
    """Compositional, continuous body control (NOT a tiny action menu)."""
    kind: str                     # move | rest | investigate | communicate
    heading: float                # radians, continuous
    speed: float                  # 0..1 continuous
    duration: int                 # intended ticks
    intensity: float              # 0..1 e.g. communication/inspection strength
    target: Optional[Tuple[int, int]] = None
    goal_id: str = ""
    neural_evidence: Dict[str, float] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        d = dict(kind=self.kind, heading=round(self.heading, 4), speed=round(self.speed, 4),
                 duration=int(self.duration), intensity=round(self.intensity, 4),
                 target=list(self.target) if self.target else None,
                 goal_id=self.goal_id)
        d["neural_evidence"] = {k: round(v, 5) for k, v in self.neural_evidence.items()}
        return d


class AutonomyEngine:
    """Self-generated goal management + compositional action synthesis."""

    def __init__(self, seed: int = 42, goal_patience: int = 12,
                 novelty_radius: int = 3):
        self.seed = int(seed)
        self.goal_patience = int(goal_patience)
        self.novelty_radius = int(novelty_radius)
        self.goals: List[Goal] = []
        self.completed: List[Goal] = []
        self._goal_seq = 0
        self._visited: set = set()
        self._rng_counter = 0

    # ------------------------------------------------------------ goals
    def _new_goal(self, tick: int, source: GoalSource, kind: str,
                  priority: float, target=None, evidence=None) -> Goal:
        self._goal_seq += 1
        gid = f"g{self.seed:04d}-{self._goal_seq:04d}"
        g = Goal(gid, source.value, kind, target, round(float(priority), 4),
                 tick, evidence=dict(evidence or {}))
        self.goals.append(g)
        return g

    def generate_goals(self, tick: int, energy: float, health: float,
                       drives: Dict[str, float], prediction_error: float,
                       world_sense: Dict[str, Any], position: Tuple[int, int],
                       skills: Dict[str, float]) -> List[Goal]:
        """Self-generated tasks from needs, drives, errors, opportunities, memory."""
        generated: List[Goal] = []
        # 1. Unmet needs dominate
        if energy < 0.45:
            generated.append(self._new_goal(
                tick, GoalSource.NEED, "forage", priority=1.0 + (0.45 - energy),
                evidence={"energy": round(energy, 3)}))
        if health < 0.5:
            generated.append(self._new_goal(
                tick, GoalSource.NEED, "rest", priority=0.9 + (0.5 - health),
                evidence={"health": round(health, 3)}))
        # 2. Curiosity / novelty (unvisited neighborhood)
        nearby = self._nearby_unvisited(position)
        if drives.get("curiosity", 0.0) > 0.75 and nearby:
            generated.append(self._new_goal(
                tick, GoalSource.CURIOSITY, "explore", priority=0.3 + drives["curiosity"],
                target=nearby[0], evidence={"unvisited_nearby": len(nearby)}))
        # 3. Prediction error -> investigate
        if prediction_error > 0.4:
            generated.append(self._new_goal(
                tick, GoalSource.PREDICTION_ERROR, "investigate",
                priority=0.4 + prediction_error, target=position,
                evidence={"prediction_error": round(prediction_error, 3)}))
        # 4. Environmental opportunity
        food = float(world_sense.get("food_gradient", 0.0))
        if food > 1.0 and energy < 0.85:
            generated.append(self._new_goal(
                tick, GoalSource.OPPORTUNITY, "forage", priority=0.5 + min(1.0, food / 3.0),
                evidence={"food_gradient": round(food, 3)}))
        hazard = float(world_sense.get("hazard_gradient", 0.0))
        if hazard > 0.6:
            generated.append(self._new_goal(
                tick, GoalSource.NEED, "avoid", priority=0.8 + hazard,
                evidence={"hazard_gradient": round(hazard, 3)}))
        # 5. Social drive
        if drives.get("social", 0.0) > 0.9 and int(world_sense.get("nearby_organisms", 0)) > 0:
            generated.append(self._new_goal(
                tick, GoalSource.SOCIAL, "socialize", priority=0.35 + drives["social"],
                evidence={"nearby": int(world_sense.get("nearby_organisms", 0))}))
        return generated

    def _nearby_unvisited(self, position: Tuple[int, int]) -> List[Tuple[int, int]]:
        x, y = position
        r = self.novelty_radius
        cands = [(x + dx, y + dy) for dx in range(-r, r + 1) for dy in range(-r, r + 1)
                 if (dx, dy) != (0, 0)]
        return [c for c in cands if c not in self._visited]

    def update_goals(self, tick: int, energy_delta: float, novelty_gained: bool,
                     prediction_error: float) -> List[Goal]:
        """Self-evaluation: progress, achievement, abandonment, task switching."""
        finished: List[Goal] = []
        for g in self.goals:
            g.ticks_active += 1
            if g.kind == "forage":
                g.progress = min(1.0, g.progress + max(0.0, energy_delta) * 2.0)
            elif g.kind == "explore" and novelty_gained:
                g.progress = min(1.0, g.progress + 0.34)
            elif g.kind == "investigate":
                g.progress = min(1.0, g.progress + max(0.0, 0.5 - prediction_error))
            if g.progress >= 0.99:
                g.status = GoalStatus.ACHIEVED.value
            elif g.ticks_active > self.goal_patience and g.progress < 0.05:
                g.status = GoalStatus.ABANDONED.value
            if g.status != GoalStatus.ACTIVE.value:
                finished.append(g)
        if finished:
            self.completed.extend(finished)
            fin_ids = {g.goal_id for g in finished}
            self.goals = [g for g in self.goals if g.goal_id not in fin_ids]
        return finished

    def active_goal(self) -> Optional[Goal]:
        if not self.goals:
            return None
        return max(self.goals, key=lambda g: (g.priority, -g.created_tick))

    def note_position(self, position: Tuple[int, int]) -> bool:
        """Track visited cells for novelty. Returns True if novel."""
        if position in self._visited:
            return False
        self._visited.add(position)
        return True

    # ---------------------------------------------------------- actions
    def synthesize_action(self, tick: int, goal: Optional[Goal],
                          brain_out: Dict[str, Any], world_sense: Dict[str, Any],
                          energy: float, health: float,
                          exploration: float = 0.5) -> ActionCandidate:
        """Compose continuous control from the ACTIVE goal + neural state.

        Neural evidence (mission rule 13): motor_act asymmetry biases heading,
        drive levels scale speed/intensity, prediction error boosts inspection.
        """
        self._rng_counter += 1
        rng = np.random.RandomState(derive_subseed(self.seed, f"act:{tick}:{self._rng_counter}"))
        drives = brain_out.get("drives", {})
        assoc = brain_out.get("tool_associations", {})
        act_a = float(assoc.get("act_in_environment", 0.0))
        remember_a = float(assoc.get("remember", 0.0))
        pred_err = float(brain_out.get("prediction_error", 0.0))

        # heading: goal target pull + neural lateral bias + deterministic jitter
        base_heading = rng.uniform(0.0, 2.0 * math.pi)
        if goal is not None and goal.target is not None:
            dx, dy = goal.target[0] - world_sense["position"][0], goal.target[1] - world_sense["position"][1]
            if abs(dx) + abs(dy) > 0:
                base_heading = 0.4 * base_heading + 0.6 * math.atan2(dy, dx)
        neural_bias = (act_a - remember_a) * (math.pi / 4.0)
        heading = (base_heading + neural_bias) % (2.0 * math.pi)

        kind = "move"
        target = goal.target if goal is not None else None
        if goal is not None:
            if goal.kind == "rest":
                kind = "rest"
            elif goal.kind == "investigate":
                kind = "investigate"
            elif goal.kind == "socialize":
                kind = "communicate"
        elif health < 0.35:
            kind = "rest"

        curiosity = float(drives.get("curiosity", 0.5))
        energy_drive = float(drives.get("energy", 1.0))
        if kind == "rest":
            speed = 0.0
            duration = 3
            intensity = float(np.clip(1.0 - health, 0.0, 1.0))
        elif kind == "investigate":
            speed = float(np.clip(0.2 + pred_err, 0.0, 1.0))
            duration = 2
            intensity = float(np.clip(pred_err, 0.0, 1.0))
        elif kind == "communicate":
            speed = float(np.clip(0.1 + 0.2 * drives.get("social", 0.5), 0.0, 1.0))
            duration = 2
            intensity = float(np.clip(drives.get("social", 0.5), 0.0, 1.0))
        else:
            # hungry organisms sprint; satiated organisms wander curiously
            speed = float(np.clip(0.3 + (1.0 - energy) * 0.6 * energy_drive
                                  + 0.2 * curiosity * exploration, 0.05, 1.0))
            duration = 1 + int(rng.randint(0, 2))
            intensity = float(np.clip(act_a, 0.0, 1.0))
        return ActionCandidate(
            kind=kind, heading=float(heading), speed=float(np.clip(speed, 0.0, 1.0)),
            duration=int(duration), intensity=float(np.clip(intensity, 0.0, 1.0)),
            target=target, goal_id=goal.goal_id if goal is not None else "",
            neural_evidence={
                "motor_act": act_a, "motor_remember": remember_a,
                "curiosity": curiosity, "prediction_error": pred_err,
            })

    # -------------------------------------------------------- introspection
    def autonomy_summary(self) -> Dict[str, Any]:
        return {
            "active_goals": [g.to_dict() for g in self.goals],
            "completed_count": len(self.completed),
            "achieved": sum(1 for g in self.completed if g.status == GoalStatus.ACHIEVED.value),
            "abandoned": sum(1 for g in self.completed if g.status == GoalStatus.ABANDONED.value),
            "visited_cells": len(self._visited),
            "goals_generated": self._goal_seq,
            "sources": sorted({g.source for g in self.completed + self.goals}),
        }

    def snapshot(self) -> Dict[str, Any]:
        return {
            "seed": self.seed, "goal_patience": self.goal_patience,
            "novelty_radius": self.novelty_radius,
            "goals": [g.to_dict() for g in self.goals],
            "completed": [g.to_dict() for g in self.completed],
            "goal_seq": self._goal_seq,
            "visited": sorted(list(self._visited)),
            "rng_counter": self._rng_counter,
        }

    @classmethod
    def restore(cls, payload: Dict[str, Any]) -> "AutonomyEngine":
        eng = cls(payload["seed"], payload.get("goal_patience", 12),
                  payload.get("novelty_radius", 3))
        def _goal(d):
            t = d.get("target")
            return Goal(d["goal_id"], d["source"], d["kind"],
                        tuple(t) if t else None, d["priority"], d["created_tick"],
                        d.get("status", "ACTIVE"), d.get("progress", 0.0),
                        d.get("ticks_active", 0), d.get("evidence", {}))
        eng.goals = [_goal(d) for d in payload.get("goals", [])]
        eng.completed = [_goal(d) for d in payload.get("completed", [])]
        eng._goal_seq = int(payload.get("goal_seq", 0))
        eng._visited = {tuple(c) for c in payload.get("visited", [])}
        eng._rng_counter = int(payload.get("rng_counter", 0))
        return eng