File size: 14,313 Bytes
ca7e89f 3771eaf ca7e89f 6fd6de7 ca7e89f a7cb63d 3771eaf 6fd6de7 ca7e89f 6fd6de7 ca7e89f 3771eaf ca7e89f f6ad663 ca7e89f f6ad663 ca7e89f 6fd6de7 ca7e89f 6fd6de7 1ea0cac a7cb63d 3771eaf 1ea0cac a7cb63d 3771eaf ca7e89f 6fd6de7 ca7e89f a7cb63d ca7e89f 3771eaf cfa1b2b a7cb63d 3771eaf a7cb63d ca7e89f 3771eaf ca7e89f 6fd6de7 ca7e89f 3771eaf ca7e89f 3771eaf ca7e89f 6fd6de7 f6ad663 6fd6de7 f114961 6fd6de7 f114961 6fd6de7 ca7e89f 6fd6de7 ca7e89f 6fd6de7 ca7e89f 6fd6de7 ca7e89f 6fd6de7 ca7e89f 6fd6de7 ca7e89f 6fd6de7 f114961 ca7e89f 6fd6de7 ca7e89f 0a0a4fc f114961 ca7e89f a7cb63d ca7e89f a7cb63d ca7e89f 6fd6de7 ca7e89f 6fd6de7 ca7e89f | 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 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | """
Arya-X Multi-Agent Environment — unified serving + training env.
mode='multi': partial obs + noise (training). mode='single': full obs (server/inference).
"""
import random
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass
from env.models import Sensor, Target, Action
from env.dynamics import initialize_sensors, spawn_targets, spawn_targets_stochastic, apply_correlated_failures
from env.world_model import add_observation_noise, apply_mask, get_priority_mapping
from interaction import NegotiationLayer
from interaction.reward import RewardEngine
# ── Agent types ───────────────────────────────────────────────────────────────
AGENT_TYPES = ["satellite", "drone", "radar", "command"]
PRIORITY_REWARD = {3: 2.0, 2: 1.0, 1: 0.5}
IDLE_PENALTY = -2.0
CONFLICT_PENALTY = -0.5
# Stable per-agent salts for partial obs noise
_AGENT_SALT = {"satellite": 1001, "drone": 2002, "radar": 3003, "command": 4004}
@dataclass
class AgentObservation:
agent_id: str
sensors: List[dict]
targets: List[dict]
timestep: int
agent_type: str
conflict_history: List[dict] = None
def __post_init__(self):
if self.conflict_history is None:
self.conflict_history = []
def to_dict(self) -> dict:
return {
"agent_id": self.agent_id,
"agent_type": self.agent_type,
"sensors": self.sensors,
"targets": self.targets,
"timestep": self.timestep,
"conflict_history": self.conflict_history,
}
@dataclass
class Proposal:
agent_id: str
sensor_id: str
target_id: str
def _cmd_override(tied: list[dict]) -> dict:
"""Command agent override: pick proposal with highest capability_score."""
return max(tied, key=lambda p: p.get("capability_score", 0.0))
# ── Multi-agent environment ───────────────────────────────────────────────────
class AryaXEnv:
def __init__(self, max_steps: int = 10, seed: int = 42,
density_factor: float = 1.0, failure_prob: float = 0.0,
conflict_injection: bool = False,
min_targets: int = 0, max_targets: int = 0,
mode: str = 'multi'):
self.max_steps = max_steps
self.seed = seed
self.density_factor = density_factor
self.failure_prob = failure_prob
self.conflict_injection = conflict_injection
self.min_targets = min_targets
self.max_targets = max_targets
# mode='multi': partial obs + noise (training)
# mode='single': full obs, no noise (server/inference backwards compat)
self.mode = mode
self.episode_number = 0
self._rng = random.Random(seed)
self.sensors: List[Sensor] = []
self.targets: List[Target] = []
self.current_step: int = 0
self._assignments: List[dict] = []
self._missed: List[str] = []
self.initial_sensor_count: int = 0
self.negotiation = NegotiationLayer()
self.reward_engine = RewardEngine()
self._agent_rewards: Dict[str, float] = {a: 0.0 for a in AGENT_TYPES}
def _clamp_targets(self, targets: List[Target]) -> List[Target]:
"""Enforce min/max target count from curriculum targets_per_step_range."""
if self.max_targets > 0 and len(targets) > self.max_targets:
targets = targets[:self.max_targets]
if self.min_targets > 0 and len(targets) < self.min_targets:
# Pad with copies of the highest-priority target
base = targets[-1] if targets else Target(id="T_pad_1", priority=1, active=True)
while len(targets) < self.min_targets:
pad = Target(
id=f"{base.id}_pad{len(targets)}",
priority=base.priority,
active=True,
type=base.type,
)
targets.append(pad)
return targets
def reset(self) -> Dict[str, AgentObservation]:
self._rng = random.Random(self.seed)
# Scale sensor count with density: easy=3, medium=4, hard=5
num_sensors = max(3, min(5, int(self.density_factor * 0.9) + 2))
self.sensors = initialize_sensors(self.seed, num_sensors=num_sensors)
if self.failure_prob > 0.0:
self.sensors = apply_correlated_failures(self.sensors, self._rng.randint(0, 9999), self.failure_prob)
self.targets = self._clamp_targets(
spawn_targets_stochastic(step=0, seed=self.seed, density_factor=self.density_factor)
)
self.current_step = 0
self._assignments = []
self._missed = []
self.initial_sensor_count = len(self.sensors)
self._agent_rewards = {a: 0.0 for a in AGENT_TYPES}
self.episode_number += 1
self.negotiation.reset()
self.reward_engine.reset()
return self._build_observations()
def state(self) -> Dict[str, AgentObservation]:
return self._build_observations()
def _build_observations(self) -> Dict[str, AgentObservation]:
if self.mode == 'single':
# Full obs — no masking or noise (server/inference backwards compat)
sensors_dict = [s.model_dump() for s in self.sensors]
targets_dict = [t.model_dump() for t in self.targets]
return {
agent: AgentObservation(
agent_id=agent, agent_type=agent,
sensors=sensors_dict, targets=targets_dict,
timestep=self.current_step,
)
for agent in AGENT_TYPES
}
# mode='multi': partial obs + noise per agent
priority_map = get_priority_mapping(self.episode_number)
obs = {}
for agent in AGENT_TYPES:
agent_sensors = [s for s in self.sensors if s.type == agent or agent == "command"]
masked_targets = apply_mask(self.targets, agent, agent_sensors)
rng = random.Random(self.seed + self.current_step + _AGENT_SALT.get(agent, 0))
noisy_targets = add_observation_noise(masked_targets, agent, rng)
drifted = [Target(id=t.id, priority=priority_map.get(t.priority, t.priority),
active=t.active, type=t.type) for t in noisy_targets]
obs[agent] = AgentObservation(
agent_id=agent, agent_type=agent,
sensors=[s.model_dump() for s in agent_sensors],
targets=[t.model_dump() for t in drifted],
timestep=self.current_step,
)
return obs
def step_multiagent(
self,
proposals: List[Proposal],
) -> Tuple[Dict[str, AgentObservation], Dict[str, float], bool, dict]:
# Build world_state for interaction/ pipeline
sensors_dict = [s.model_dump() for s in self.sensors]
targets_dict = [t.model_dump() for t in self.targets]
CAPABILITY_MATRIX = {
("satellite","strategic"):0.95, ("satellite","kinetic"):0.40, ("satellite","airspace"):0.60,
("drone","kinetic"):0.95, ("drone","strategic"):0.30, ("drone","airspace"):0.50,
("radar","airspace"):0.95, ("radar","kinetic"):0.65, ("radar","strategic"):0.45,
}
proposals_raw = []
for p in proposals:
sensor_type = next((getattr(s, "type", "unknown") for s in self.sensors if s.id == p.sensor_id), "unknown")
target_type = next((getattr(t, "type", "strategic") for t in self.targets if t.id == p.target_id), "strategic")
cap_score = CAPABILITY_MATRIX.get((sensor_type, target_type), 0.5)
proposals_raw.append({
"agent_id": p.agent_id,
"sensor_id": p.sensor_id,
"target_id": p.target_id,
"capability_score": cap_score
})
assigned_sids = {p.sensor_id for p in proposals}
idle_sensors = [s["id"] for s in sensors_dict if s["id"] not in assigned_sids]
world_state = {
"sensors": sensors_dict,
"targets": targets_dict,
"idle_sensors": idle_sensors,
"step": self.current_step,
"proposals": proposals_raw,
}
# Run full negotiation pipeline (interaction/)
neg_result = self.negotiation.negotiate(proposals_raw, world_state, _cmd_override)
# Detect conflicts on raw proposals (before _validate_proposals drops duplicates)
# so conflict log and rate reflect actual agent behaviour, not post-validation state
conflicts_raw = self.negotiation.detector.detect(proposals_raw, world_state)
if self.negotiation._history:
self.negotiation._history[-1].step_metrics["num_conflicts"] = len(conflicts_raw)
final_assignments = neg_result.final_assignments # list[{sensor_id, target_id}]
conflicts = conflicts_raw # use pre-validation conflicts for accurate reporting
# Build agent_id lookup from proposals
sensor_to_agent = {p.sensor_id: p.agent_id for p in proposals}
handled_ids: set = set()
used_sensor_ids: set = set()
agent_assignments: Dict[str, List[dict]] = {a: [] for a in AGENT_TYPES}
for a in final_assignments:
sid, tid = a["sensor_id"], a["target_id"]
if sid in used_sensor_ids or tid in handled_ids:
continue
sensor_ok = any(s.id == sid and s.available for s in self.sensors)
target_ok = any(t.id == tid and t.active for t in self.targets)
if not sensor_ok or not target_ok:
continue
for s in self.sensors:
if s.id == sid:
s.available = False
for t in self.targets:
if t.id == tid:
t.active = False
handled_ids.add(tid)
used_sensor_ids.add(sid)
agent_id = sensor_to_agent.get(sid, "command")
self._assignments.append({"sensor": sid, "target": tid, "agent": agent_id})
agent_assignments[agent_id].append({"sensor_id": sid, "target_id": tid})
# Per-agent rewards — direct priority-based calculation
step_rewards: Dict[str, float] = {a: 0.0 for a in AGENT_TYPES}
# Use pre-step target map (targets were deactivated in the loop above)
pre_step_target_map = {t["id"]: t for t in targets_dict}
n_assigned = len(used_sensor_ids)
recent = self._assignments[-n_assigned:] if n_assigned else []
for a in recent:
agent_id = a["agent"]
tgt = pre_step_target_map.get(a["target"])
if tgt and agent_id in step_rewards:
step_rewards[agent_id] += PRIORITY_REWARD.get(tgt["priority"], 0.0)
# Conflict penalties
for c in conflicts:
from interaction.conflict import ConflictType
if c.conflict_type in (ConflictType.REDUNDANT_COVERAGE, ConflictType.OVER_ASSIGNMENT):
for aid in c.involved_agents:
if aid in step_rewards:
step_rewards[aid] += CONFLICT_PENALTY
# Idle penalty — sensor available but P3 target uncovered
covered_tids = {a["target"] for a in recent}
uncovered_p3 = [t for t in targets_dict if t.get("active") and t["priority"] == 3 and t["id"] not in covered_tids]
if uncovered_p3:
for s in sensors_dict:
if s["id"] not in used_sensor_ids:
agent_id = sensor_to_agent.get(s["id"])
if agent_id and agent_id in step_rewards:
step_rewards[agent_id] += IDLE_PENALTY
for a in AGENT_TYPES:
self._agent_rewards[a] += step_rewards.get(a, 0.0)
# print(f"[DEBUG] step={self.current_step} step_rewards={step_rewards} cumulative={self._agent_rewards}")
self._missed.extend(
t.id for t in self.targets
if t.active and t.priority == 3 and t.id not in handled_ids
)
self.current_step += 1
self.targets = self._clamp_targets(spawn_targets_stochastic(
step=self.current_step, seed=self.seed, density_factor=self.density_factor
))
if self.conflict_injection:
# Guarantee at least one P3 target to exercise the conflict pipeline
from env.dynamics import spawn_targets as _spawn_det
injected = _spawn_det(step=self.current_step, seed=self.seed, conflict_injection=True)
p3_ids = {t.id for t in self.targets if t.priority == 3}
for t in injected:
if t.priority == 3 and t.id not in p3_ids:
self.targets.append(t)
break
for s in self.sensors:
s.available = True
if self.failure_prob > 0.0:
rng = random.Random(self.seed + self.current_step)
self.sensors = apply_correlated_failures(self.sensors, rng.randint(0, 9999), self.failure_prob)
done = self.current_step >= self.max_steps
conflict_rate = self.negotiation.get_conflict_rate()
info = {
"assignments": list(self._assignments),
"missed_targets": list(self._missed),
"step_count": self.current_step,
"conflicts": [
{
"type": str(c.conflict_type),
"agents": c.involved_agents,
"sensor_id": c.involved_sensors[0] if c.involved_sensors else None,
"target_id": c.involved_targets[0] if c.involved_targets else None,
}
for c in conflicts
],
"conflict_rate": round(conflict_rate, 4),
"agent_rewards": dict(self._agent_rewards),
"step_rewards": dict(step_rewards),
}
return self._build_observations(), step_rewards, done, info
|