chathurvitha commited on
Commit
8be5cf6
·
unverified ·
2 Parent(s): c47b19aa7cb63d

Merge branch 'main' into chathu01

Browse files
agents/base_agent.py CHANGED
@@ -31,19 +31,25 @@ class BaseAgent(ABC):
31
  def propose(self) -> List[Proposal]:
32
  """Return one Proposal per available sensor this agent controls."""
33
 
34
- def update(self, reward: float, conflict_history: List[ConflictRecord]) -> None:
 
 
 
35
  self.episode_reward += reward
36
  for record in conflict_history:
37
- ctype = record.conflict_type
38
- if self.agent_id in record.agents_involved:
39
- if record.resolution == "command_override":
40
- self.losses[ctype] = self.losses.get(ctype, 0) + 1
 
 
 
 
 
 
 
41
  else:
42
- # Won if we're not the one overridden — heuristic: first agent listed wins
43
- if record.agents_involved and record.agents_involved[0] == self.agent_id:
44
- self.wins[ctype] = self.wins.get(ctype, 0) + 1
45
- else:
46
- self.losses[ctype] = self.losses.get(ctype, 0) + 1
47
  self.conflict_history.extend(conflict_history)
48
  self.conflict_history = self.conflict_history[-10:]
49
 
 
31
  def propose(self) -> List[Proposal]:
32
  """Return one Proposal per available sensor this agent controls."""
33
 
34
+ def update(self, reward: float, conflict_history: List) -> None:
35
+ """Accept ConflictRecord objects from interaction/conflict.py.
36
+ Uses involved_agents (not agents_involved) and has no resolution field.
37
+ """
38
  self.episode_reward += reward
39
  for record in conflict_history:
40
+ # Support both interaction.conflict.ConflictRecord (dataclass) and
41
+ # env.models.ConflictRecord (Pydantic) by reading whichever field exists.
42
+ involved = (
43
+ getattr(record, "involved_agents", None)
44
+ or getattr(record, "agents_involved", [])
45
+ )
46
+ ctype = str(getattr(record, "conflict_type", "unknown"))
47
+ if self.agent_id in involved:
48
+ # Heuristic: first listed agent is the "winner" of the conflict
49
+ if involved and involved[0] == self.agent_id:
50
+ self.wins[ctype] = self.wins.get(ctype, 0) + 1
51
  else:
52
+ self.losses[ctype] = self.losses.get(ctype, 0) + 1
 
 
 
 
53
  self.conflict_history.extend(conflict_history)
54
  self.conflict_history = self.conflict_history[-10:]
55
 
agents/command.py CHANGED
@@ -108,21 +108,37 @@ class CommandAgent(BaseAgent):
108
  })
109
  return proposals
110
 
111
- def issue_override(self, conflict: ConflictRecord) -> Optional[Proposal]:
112
- """Called by resolver when conflict persists. Returns a binding override proposal."""
113
- target_id = conflict.target_id
 
 
 
 
 
 
 
 
 
 
 
 
114
  target = next((t for t in self._active_targets() if t.id == target_id), None)
115
  if target is None:
116
  return None
117
 
 
 
 
 
 
 
118
  used_sensors = {p.sensor_id for p in self._all_proposals}
119
  free_sensor = next(
120
  (s for s in self._available_sensors() if s.id not in used_sensors), None
121
  )
122
 
123
  if free_sensor is None:
124
- # Pick the proposal from the first involved agent to re-assign
125
- involved = conflict.agents_involved
126
  override_prop = next(
127
  (p for p in self._all_proposals
128
  if p.agent_id in involved and p.target_id == target_id),
 
108
  })
109
  return proposals
110
 
111
+ def issue_override(self, conflict) -> Optional[Proposal]:
112
+ """Called by resolver when conflict persists. Returns a binding override proposal.
113
+ Works with interaction.conflict.ConflictRecord (involved_targets/involved_agents)
114
+ and env.models.ConflictRecord (target_id/agents_involved).
115
+ """
116
+ # Resolve target_id from whichever schema is present
117
+ involved_targets = getattr(conflict, "involved_targets", None)
118
+ target_id = (
119
+ involved_targets[0]
120
+ if involved_targets
121
+ else getattr(conflict, "target_id", None)
122
+ )
123
+ if target_id is None:
124
+ return None
125
+
126
  target = next((t for t in self._active_targets() if t.id == target_id), None)
127
  if target is None:
128
  return None
129
 
130
+ # Resolve involved agents from whichever schema is present
131
+ involved = (
132
+ getattr(conflict, "involved_agents", None)
133
+ or getattr(conflict, "agents_involved", [])
134
+ )
135
+
136
  used_sensors = {p.sensor_id for p in self._all_proposals}
137
  free_sensor = next(
138
  (s for s in self._available_sensors() if s.id not in used_sensors), None
139
  )
140
 
141
  if free_sensor is None:
 
 
142
  override_prop = next(
143
  (p for p in self._all_proposals
144
  if p.agent_id in involved and p.target_id == target_id),
curriculum.py CHANGED
@@ -110,6 +110,7 @@ class CurriculumEngine:
110
  Called after each episode with the episode's coordination and efficiency scores.
111
  Returns {"phase": int, "difficulty": float, "action": str}.
112
  """
 
113
  self._score_history.append(coordination_score)
114
  self._eff_history.append(efficiency_score)
115
 
@@ -181,10 +182,9 @@ class CurriculumEngine:
181
  # ── Episode lifecycle ────────────────────────────────────────────
182
  def reset_episode(self) -> None:
183
  """
184
- Increment episode counter and check for self-play trigger.
185
- Call at the start of each episode.
186
  """
187
- self.episode_count += 1
188
  if self.should_trigger_self_play():
189
  self._self_play_triggers_count += 1
190
  logger.info(
 
110
  Called after each episode with the episode's coordination and efficiency scores.
111
  Returns {"phase": int, "difficulty": float, "action": str}.
112
  """
113
+ self.episode_count += 1
114
  self._score_history.append(coordination_score)
115
  self._eff_history.append(efficiency_score)
116
 
 
182
  # ── Episode lifecycle ────────────────────────────────────────────
183
  def reset_episode(self) -> None:
184
  """
185
+ Called at the START of each episode to check for self-play triggers.
186
+ Does NOT increment episode_count — that is done by update() at episode end.
187
  """
 
188
  if self.should_trigger_self_play():
189
  self._self_play_triggers_count += 1
190
  logger.info(
env/dynamics.py CHANGED
@@ -4,6 +4,7 @@ from typing import List
4
  from env.models import Sensor, Target
5
 
6
  SENSOR_TYPES = ["satellite", "drone", "radar"]
 
7
 
8
 
9
  def initialize_sensors(seed: int = 42) -> List[Sensor]:
@@ -34,7 +35,12 @@ def spawn_targets(
34
  rng = random.Random((seed * 6364136223846793005 + step) & 0xFFFFFFFFFFFFFFFF)
35
  count = rng.randint(2, 4)
36
  targets = [
37
- Target(id=f"T{step}_{i+1}", priority=rng.randint(1, 3), active=True)
 
 
 
 
 
38
  for i in range(count)
39
  ]
40
 
@@ -47,12 +53,17 @@ def spawn_targets(
47
  if injected:
48
  # Elevate existing target to P3
49
  targets = [
50
- Target(id=t.id, priority=3 if t.id == overlap_id else t.priority, active=t.active)
 
 
 
 
 
51
  for t in targets
52
  ]
53
  else:
54
  # Prepend new P3 target in the overlap zone
55
- targets = [Target(id=overlap_id, priority=3, active=True)] + targets
56
 
57
  return targets
58
 
@@ -69,7 +80,12 @@ def spawn_targets_stochastic(step: int, seed: int = 42, density_factor: float =
69
  p *= rng.random()
70
  count = max(1, k - 1)
71
  return [
72
- Target(id=f"T{step}_{i+1}", priority=rng.randint(1, 3), active=True)
 
 
 
 
 
73
  for i in range(count)
74
  ]
75
 
 
4
  from env.models import Sensor, Target
5
 
6
  SENSOR_TYPES = ["satellite", "drone", "radar"]
7
+ TARGET_TYPES = ["strategic", "kinetic", "airspace"]
8
 
9
 
10
  def initialize_sensors(seed: int = 42) -> List[Sensor]:
 
35
  rng = random.Random((seed * 6364136223846793005 + step) & 0xFFFFFFFFFFFFFFFF)
36
  count = rng.randint(2, 4)
37
  targets = [
38
+ Target(
39
+ id=f"T{step}_{i+1}",
40
+ priority=rng.randint(1, 3),
41
+ active=True,
42
+ type=rng.choice(TARGET_TYPES),
43
+ )
44
  for i in range(count)
45
  ]
46
 
 
53
  if injected:
54
  # Elevate existing target to P3
55
  targets = [
56
+ Target(
57
+ id=t.id,
58
+ priority=3 if t.id == overlap_id else t.priority,
59
+ active=t.active,
60
+ type=t.type,
61
+ )
62
  for t in targets
63
  ]
64
  else:
65
  # Prepend new P3 target in the overlap zone
66
+ targets = [Target(id=overlap_id, priority=3, active=True, type=rng.choice(TARGET_TYPES))] + targets
67
 
68
  return targets
69
 
 
80
  p *= rng.random()
81
  count = max(1, k - 1)
82
  return [
83
+ Target(
84
+ id=f"T{step}_{i+1}",
85
+ priority=rng.randint(1, 3),
86
+ active=True,
87
+ type=rng.choice(TARGET_TYPES),
88
+ )
89
  for i in range(count)
90
  ]
91
 
env/environment.py CHANGED
@@ -126,6 +126,9 @@ PRIORITY_REWARD = {3: 2.0, 2: 1.0, 1: 0.5}
126
  MISSED_HIGH_PENALTY = -2.0
127
  IDLE_PENALTY = -2.0
128
 
 
 
 
129
 
130
  class AryaXEnv:
131
  def __init__(self, max_steps: int = 20, seed: int = 42, density_factor: float = 2.5,
@@ -222,7 +225,7 @@ class AryaXEnv:
222
  for agent_type in AGENT_TYPES:
223
  agent_sensors = [s for s in self.sensors if s.type == agent_type or agent_type == "command"]
224
  masked_targets = apply_mask(self.targets, agent_type, agent_sensors)
225
- rng = random.Random(self.seed + self.current_step + hash(agent_type))
226
  noisy_targets = add_observation_noise(masked_targets, agent_type, rng)
227
  # Apply schema drift to noisy targets
228
  drifted = [Target(id=t.id, priority=priority_map.get(t.priority, t.priority), active=t.active)
 
126
  MISSED_HIGH_PENALTY = -2.0
127
  IDLE_PENALTY = -2.0
128
 
129
+ # Stable per-agent salts — avoids hash() which is randomised per Python process
130
+ _AGENT_SALT = {"satellite": 1001, "drone": 2002, "radar": 3003, "command": 4004}
131
+
132
 
133
  class AryaXEnv:
134
  def __init__(self, max_steps: int = 20, seed: int = 42, density_factor: float = 2.5,
 
225
  for agent_type in AGENT_TYPES:
226
  agent_sensors = [s for s in self.sensors if s.type == agent_type or agent_type == "command"]
227
  masked_targets = apply_mask(self.targets, agent_type, agent_sensors)
228
+ rng = random.Random(self.seed + self.current_step + _AGENT_SALT.get(agent_type, 0))
229
  noisy_targets = add_observation_noise(masked_targets, agent_type, rng)
230
  # Apply schema drift to noisy targets
231
  drifted = [Target(id=t.id, priority=priority_map.get(t.priority, t.priority), active=t.active)
env/models.py CHANGED
@@ -13,6 +13,7 @@ class Target(BaseModel):
13
  id: str
14
  priority: int # 1=low, 2=medium, 3=high
15
  active: bool
 
16
 
17
 
18
  class Observation(BaseModel):
 
13
  id: str
14
  priority: int # 1=low, 2=medium, 3=high
15
  active: bool
16
+ type: str = "strategic" # "strategic" | "kinetic" | "airspace"
17
 
18
 
19
  class Observation(BaseModel):
env/multiagent.py CHANGED
@@ -8,7 +8,7 @@ import random
8
  from typing import Dict, List, Optional, Tuple, Any
9
  from dataclasses import dataclass
10
  from env.models import Sensor, Target, Action
11
- from env.dynamics import initialize_sensors, spawn_targets
12
  from interaction import NegotiationLayer
13
  from interaction.reward import RewardEngine
14
 
@@ -53,11 +53,17 @@ def _cmd_override(tied: list[dict]) -> dict:
53
  # ── Multi-agent environment ───────────────────────────────────────────────────
54
  class AryaXEnv:
55
  def __init__(self, max_steps: int = 10, seed: int = 42,
56
- density_factor: float = 1.0, failure_prob: float = 0.0):
57
- self.max_steps = max_steps
58
- self.seed = seed
59
- self.density_factor = density_factor
60
- self.failure_prob = failure_prob
 
 
 
 
 
 
61
  self.sensors: List[Sensor] = []
62
  self.targets: List[Target] = []
63
  self.current_step: int = 0
@@ -68,9 +74,31 @@ class AryaXEnv:
68
  self.reward_engine = RewardEngine()
69
  self._agent_rewards: Dict[str, float] = {a: 0.0 for a in AGENT_TYPES}
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def reset(self) -> Dict[str, AgentObservation]:
72
  self.sensors = initialize_sensors(self.seed)
73
- self.targets = spawn_targets(step=0, seed=self.seed)
 
 
 
 
 
74
  self.current_step = 0
75
  self._assignments = []
76
  self._missed = []
@@ -174,9 +202,23 @@ class AryaXEnv:
174
  )
175
 
176
  self.current_step += 1
177
- self.targets = spawn_targets(step=self.current_step, seed=self.seed)
 
 
 
 
 
 
 
 
 
 
 
178
  for s in self.sensors:
179
  s.available = True
 
 
 
180
 
181
  done = self.current_step >= self.max_steps
182
  conflict_rate = self.negotiation.get_conflict_rate()
 
8
  from typing import Dict, List, Optional, Tuple, Any
9
  from dataclasses import dataclass
10
  from env.models import Sensor, Target, Action
11
+ from env.dynamics import initialize_sensors, spawn_targets, spawn_targets_stochastic, apply_correlated_failures
12
  from interaction import NegotiationLayer
13
  from interaction.reward import RewardEngine
14
 
 
53
  # ── Multi-agent environment ───────────────────────────────────────────────────
54
  class AryaXEnv:
55
  def __init__(self, max_steps: int = 10, seed: int = 42,
56
+ density_factor: float = 1.0, failure_prob: float = 0.0,
57
+ conflict_injection: bool = False,
58
+ min_targets: int = 0, max_targets: int = 0):
59
+ self.max_steps = max_steps
60
+ self.seed = seed
61
+ self.density_factor = density_factor
62
+ self.failure_prob = failure_prob
63
+ self.conflict_injection = conflict_injection
64
+ # When non-zero, clamp spawned target count to [min_targets, max_targets]
65
+ self.min_targets = min_targets
66
+ self.max_targets = max_targets
67
  self.sensors: List[Sensor] = []
68
  self.targets: List[Target] = []
69
  self.current_step: int = 0
 
74
  self.reward_engine = RewardEngine()
75
  self._agent_rewards: Dict[str, float] = {a: 0.0 for a in AGENT_TYPES}
76
 
77
+ def _clamp_targets(self, targets: List[Target]) -> List[Target]:
78
+ """Enforce min/max target count from curriculum targets_per_step_range."""
79
+ if self.max_targets > 0 and len(targets) > self.max_targets:
80
+ targets = targets[:self.max_targets]
81
+ if self.min_targets > 0 and len(targets) < self.min_targets:
82
+ # Pad with copies of the highest-priority target
83
+ base = targets[-1] if targets else Target(id="T_pad_1", priority=1, active=True)
84
+ while len(targets) < self.min_targets:
85
+ pad = Target(
86
+ id=f"{base.id}_pad{len(targets)}",
87
+ priority=base.priority,
88
+ active=True,
89
+ type=base.type,
90
+ )
91
+ targets.append(pad)
92
+ return targets
93
+
94
  def reset(self) -> Dict[str, AgentObservation]:
95
  self.sensors = initialize_sensors(self.seed)
96
+ if self.failure_prob > 0.0:
97
+ rng = random.Random(self.seed)
98
+ self.sensors = apply_correlated_failures(self.sensors, rng.randint(0, 9999), self.failure_prob)
99
+ self.targets = self._clamp_targets(
100
+ spawn_targets_stochastic(step=0, seed=self.seed, density_factor=self.density_factor)
101
+ )
102
  self.current_step = 0
103
  self._assignments = []
104
  self._missed = []
 
202
  )
203
 
204
  self.current_step += 1
205
+ self.targets = self._clamp_targets(spawn_targets_stochastic(
206
+ step=self.current_step, seed=self.seed, density_factor=self.density_factor
207
+ ))
208
+ if self.conflict_injection:
209
+ # Guarantee at least one P3 target to exercise the conflict pipeline
210
+ from env.dynamics import spawn_targets as _spawn_det
211
+ injected = _spawn_det(step=self.current_step, seed=self.seed, conflict_injection=True)
212
+ p3_ids = {t.id for t in self.targets if t.priority == 3}
213
+ for t in injected:
214
+ if t.priority == 3 and t.id not in p3_ids:
215
+ self.targets.append(t)
216
+ break
217
  for s in self.sensors:
218
  s.available = True
219
+ if self.failure_prob > 0.0:
220
+ rng = random.Random(self.seed + self.current_step)
221
+ self.sensors = apply_correlated_failures(self.sensors, rng.randint(0, 9999), self.failure_prob)
222
 
223
  done = self.current_step >= self.max_steps
224
  conflict_rate = self.negotiation.get_conflict_rate()
env/reward.py DELETED
@@ -1,4 +0,0 @@
1
- """
2
- NOTE: This file is deprecated.
3
- Reward logic is implemented directly in environment.py (step_batch).
4
- """
 
 
 
 
 
pyproject.toml CHANGED
@@ -19,6 +19,16 @@ dependencies = [
19
  "python-dotenv>=1.0.1",
20
  ]
21
 
 
 
 
 
 
 
 
 
 
 
22
  [project.scripts]
23
  server = "server.app:main"
24
 
 
19
  "python-dotenv>=1.0.1",
20
  ]
21
 
22
+ [project.optional-dependencies]
23
+ training = [
24
+ "trl>=0.8.0",
25
+ "transformers>=4.40.0",
26
+ "peft>=0.10.0",
27
+ "torch",
28
+ "accelerate",
29
+ "bitsandbytes",
30
+ ]
31
+
32
  [project.scripts]
33
  server = "server.app:main"
34
 
requirements.txt CHANGED
@@ -29,6 +29,18 @@ ffmpy==1.0.0
29
  filelock==3.25.2
30
  Flask==3.0.3
31
  Flask-Cors==4.0.1
 
 
 
 
 
 
 
 
 
 
 
 
32
  fsspec==2026.3.0
33
  gradio==6.11.0
34
  gradio_client==2.4.0
 
29
  filelock==3.25.2
30
  Flask==3.0.3
31
  Flask-Cors==4.0.1
32
+
33
+ # ── Optional: training stack (GRPO/TRL) ──────────────────────────────────────
34
+ # These are NOT installed by default. Install manually to enable trainer.py:
35
+ # pip install trl>=0.8.0 transformers>=4.40.0 peft>=0.10.0 torch accelerate bitsandbytes
36
+ # On Colab with GPU: also install unsloth (see train_colab.py)
37
+ # trl
38
+ # transformers
39
+ # peft
40
+ # torch
41
+ # accelerate
42
+ # bitsandbytes
43
+ # unsloth
44
  fsspec==2026.3.0
45
  gradio==6.11.0
46
  gradio_client==2.4.0
server.py CHANGED
@@ -12,8 +12,6 @@ from flask import Flask, request, jsonify, render_template
12
  from env import SentinelEnv
13
  from env.models import Action
14
  from env.multiagent import AryaXEnv, Proposal, AGENT_TYPES
15
- from interaction import NegotiationLayer
16
- from interaction.reward import RewardEngine
17
 
18
  app = Flask(__name__)
19
 
@@ -23,10 +21,8 @@ obs = None
23
  _target_positions: dict = {}
24
 
25
  # ── Multi-agent env ───────────────────────────────────────────────────────────
26
- mx_env = AryaXEnv(max_steps=10, seed=42)
27
- mx_obs = None # Dict[str, AgentObservation] | None
28
- _negotiation = NegotiationLayer()
29
- _reward_eng = RewardEngine()
30
 
31
  # ── LLM client (optional) ─────────────────────────────────────────────────────
32
  _llm_client = None
@@ -198,26 +194,32 @@ def _build_multi_prompt(agent_id: str, agent_obs) -> str:
198
  f" - id={t['id']} priority={t['priority']} active={t['active']}"
199
  for t in agent_obs.targets if t["active"]
200
  )
201
- return f"""You are the {agent_id} agent in a multi-agent ISR system.
202
- Propose assignments for sensors you are responsible for.
203
- Priority 3=HIGH, 2=MED, 1=LOW. Cover HIGH threats first.
 
 
 
 
 
204
 
205
  Timestep: {agent_obs.timestep}
206
- Your agent type: {agent_id}
207
 
208
- Available Sensors:
209
  {sensors}
210
 
211
  Active Threats:
212
  {targets}
213
 
214
- Respond ONLY with a JSON array:
215
- [{{"sensor_id": "S1", "target_id": "T0_1"}}]
216
  """
217
 
218
 
219
- def _greedy_proposals(agent_id: str, agent_obs, used_sensors: set) -> list[Proposal]:
220
  """Greedy proposals for one agent — only claim sensors matching agent type."""
 
 
221
  my_sensors = [
222
  s for s in agent_obs.sensors
223
  if s["available"] and s["id"] not in used_sensors
@@ -227,7 +229,7 @@ def _greedy_proposals(agent_id: str, agent_obs, used_sensors: set) -> list[Propo
227
  [t for t in agent_obs.targets if t["active"]],
228
  key=lambda t: -t["priority"]
229
  )
230
- proposals, used_targets = [], set()
231
  for sensor in my_sensors:
232
  for target in targets:
233
  if target["id"] not in used_targets:
@@ -282,8 +284,10 @@ def _lora_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
282
 
283
  def _get_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
284
  """Build proposals from all agents. Returns (proposals, source)."""
 
285
  proposals: list[Proposal] = []
286
  used_sensors: set = set()
 
287
 
288
  if _has_adapters:
289
  return _lora_multi_proposals(agent_obs_map)
@@ -293,6 +297,9 @@ def _get_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
293
  for agent_id in AGENT_TYPES:
294
  agent_obs = agent_obs_map[agent_id]
295
  prompt = _build_multi_prompt(agent_id, agent_obs)
 
 
 
296
  response = _llm_client.chat.completions.create(
297
  model=MODEL_NAME,
298
  messages=[{"role": "user", "content": prompt}],
@@ -300,27 +307,38 @@ def _get_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
300
  temperature=0.0
301
  )
302
  raw = response.choices[0].message.content.strip()
 
303
  start, end = raw.find("["), raw.rfind("]") + 1
 
 
 
304
  items = json.loads(raw[start:end])
305
- valid_sensors = {s["id"] for s in agent_obs.sensors if s["available"]}
 
 
 
306
  valid_targets = {t["id"] for t in agent_obs.targets if t["active"]}
307
  for item in items:
308
  sid, tid = item.get("sensor_id"), item.get("target_id")
309
- if sid in valid_sensors and tid in valid_targets:
310
  proposals.append(Proposal(agent_id=agent_id, sensor_id=sid, target_id=tid))
311
  used_sensors.add(sid)
312
  if proposals:
 
313
  return proposals, "llm"
 
314
  except Exception as e:
315
- print(f"[LLM multi] Error: {e}. Falling back to greedy.")
316
 
317
- # Greedy fallback — agents claim sensors by type
318
  used_sensors = set()
 
319
  for agent_id in AGENT_TYPES:
320
  agent_obs = agent_obs_map[agent_id]
321
- for p in _greedy_proposals(agent_id, agent_obs, used_sensors):
322
  proposals.append(p)
323
  used_sensors.add(p.sensor_id)
 
324
 
325
  return proposals, "greedy"
326
 
@@ -449,8 +467,6 @@ def reset_multi():
449
  mx_env.seed = seed
450
  mx_env.max_steps = max_steps
451
  mx_obs = mx_env.reset()
452
- _negotiation.reset()
453
- _reward_eng.reset()
454
  return jsonify({
455
  "seed": seed,
456
  "max_steps": max_steps,
@@ -479,113 +495,40 @@ def step_multi():
479
  if p.get("agent_id") and p.get("sensor_id") and p.get("target_id")
480
  ]
481
 
482
- # Build world_state from satellite observation
483
- sat_obs = mx_obs.get("satellite") or next(iter(mx_obs.values()))
484
- sensors_dict = sat_obs.sensors
485
- targets_dict = sat_obs.targets
486
- proposals_raw = [
487
- {"agent_id": p.agent_id, "sensor_id": p.sensor_id,
488
- "target_id": p.target_id, "capability_score": 0.5}
489
- for p in proposals
490
- ]
491
- assigned_sids = {p.sensor_id for p in proposals}
492
- idle_sensors = [s["id"] for s in sensors_dict if s["id"] not in assigned_sids]
493
- world_state = {
494
- "sensors": sensors_dict,
495
- "targets": targets_dict,
496
- "idle_sensors": idle_sensors,
497
- "step": sat_obs.timestep,
498
- "proposals": proposals_raw,
499
- }
500
-
501
- # Run NegotiationLayer (interaction/ package)
502
- neg_result = _negotiation.negotiate(
503
- proposals_raw, world_state, lambda tied: tied[0]
504
- )
505
-
506
- # Step env with resolved proposals
507
  mx_obs, step_rewards, done, info = mx_env.step_multiagent(proposals)
508
 
509
- # Compute per-agent rewards via RewardEngine
510
- per_agent_rewards = _reward_eng.compute_step_reward(
511
- neg_result.final_assignments, neg_result, world_state, list(AGENT_TYPES)
512
- )
513
-
514
- # Build conflict list from neg_result
515
- conflicts = [
516
- {
517
- "type": str(c.conflict_type),
518
- "agents": c.involved_agents,
519
- "sensor_id": c.involved_sensors[0] if c.involved_sensors else None,
520
- "target_id": c.involved_targets[0] if c.involved_targets else None,
521
- }
522
- for c in neg_result.conflicts_detected
523
- ]
524
- conflict_rate = _negotiation.get_conflict_rate()
525
 
526
  return jsonify({
527
  "observations": {k: v.to_dict() for k, v in mx_obs.items()},
528
  "step_rewards": step_rewards,
529
- "per_agent_rewards": per_agent_rewards,
530
  "done": done,
531
  "info": info,
532
  "conflict_rate": round(conflict_rate, 4),
533
  "conflicts": conflicts,
534
- "override_invoked": neg_result.override_invoked,
535
  })
536
 
537
 
538
  @app.post("/auto_multi")
539
  def auto_multi():
540
  global mx_obs
 
541
  if mx_obs is None:
542
  return jsonify({"error": "Call /reset_multi first"}), 400
543
 
544
  proposals, source = _get_multi_proposals(mx_obs)
545
 
546
- # Build world_state from satellite observation
547
- sat_obs = mx_obs.get("satellite") or next(iter(mx_obs.values()))
548
- sensors_dict = sat_obs.sensors
549
- targets_dict = sat_obs.targets
550
- proposals_raw = [
551
- {"agent_id": p.agent_id, "sensor_id": p.sensor_id,
552
- "target_id": p.target_id, "capability_score": 0.5}
553
- for p in proposals
554
- ]
555
- assigned_sids = {p.sensor_id for p in proposals}
556
- idle_sensors = [s["id"] for s in sensors_dict if s["id"] not in assigned_sids]
557
- world_state = {
558
- "sensors": sensors_dict,
559
- "targets": targets_dict,
560
- "idle_sensors": idle_sensors,
561
- "step": sat_obs.timestep,
562
- "proposals": proposals_raw,
563
- }
564
-
565
- # Run NegotiationLayer (interaction/ package)
566
- neg_result = _negotiation.negotiate(
567
- proposals_raw, world_state, lambda tied: tied[0]
568
- )
569
-
570
- # Step env with resolved proposals (neg_result.final_assignments already validated)
571
  mx_obs, step_rewards, done, info = mx_env.step_multiagent(proposals)
572
 
573
- # Compute per-agent rewards via RewardEngine
574
- per_agent_rewards = _reward_eng.compute_step_reward(
575
- neg_result.final_assignments, neg_result, world_state, list(AGENT_TYPES)
576
- )
577
-
578
- # Build conflict list from neg_result
579
- conflicts = [
580
- {
581
- "type": str(c.conflict_type),
582
- "agents": c.involved_agents,
583
- "sensor_id": c.involved_sensors[0] if c.involved_sensors else None,
584
- "target_id": c.involved_targets[0] if c.involved_targets else None,
585
- }
586
- for c in neg_result.conflicts_detected
587
- ]
588
- conflict_rate = _negotiation.get_conflict_rate()
589
 
590
  return jsonify({
591
  "proposals": [{"agent_id": p.agent_id, "sensor_id": p.sensor_id,
@@ -593,12 +536,12 @@ def auto_multi():
593
  "agent": source,
594
  "observations": {k: v.to_dict() for k, v in mx_obs.items()},
595
  "step_rewards": step_rewards,
596
- "per_agent_rewards": per_agent_rewards,
597
  "done": done,
598
  "info": info,
599
  "conflict_rate": round(conflict_rate, 4),
600
  "conflicts": conflicts,
601
- "override_invoked": neg_result.override_invoked,
602
  })
603
 
604
 
@@ -610,5 +553,7 @@ def ui():
610
 
611
 
612
  if __name__ == "__main__":
 
 
613
  port = int(os.environ.get("PORT", 7860))
614
- app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False)
 
12
  from env import SentinelEnv
13
  from env.models import Action
14
  from env.multiagent import AryaXEnv, Proposal, AGENT_TYPES
 
 
15
 
16
  app = Flask(__name__)
17
 
 
21
  _target_positions: dict = {}
22
 
23
  # ── Multi-agent env ───────────────────────────────────────────────────────────
24
+ mx_env = AryaXEnv(max_steps=10, seed=42)
25
+ mx_obs = None # Dict[str, AgentObservation] | None
 
 
26
 
27
  # ── LLM client (optional) ─────────────────────────────────────────────────────
28
  _llm_client = None
 
194
  f" - id={t['id']} priority={t['priority']} active={t['active']}"
195
  for t in agent_obs.targets if t["active"]
196
  )
197
+ if not my_sensors:
198
+ return ""
199
+ sensor_ids = [s['id'] for s in my_sensors]
200
+ target_ids = [t['id'] for t in agent_obs.targets if t['active']]
201
+ return f"""You are the {agent_id} agent. Assign ONLY your sensors to threats.
202
+ Your sensors: {sensor_ids}
203
+ Active threat IDs: {target_ids}
204
+ Priority 3=HIGH, 2=MED, 1=LOW. Cover HIGH first. Each sensor to a DIFFERENT target.
205
 
206
  Timestep: {agent_obs.timestep}
 
207
 
208
+ Your Sensors:
209
  {sensors}
210
 
211
  Active Threats:
212
  {targets}
213
 
214
+ Respond ONLY with a JSON array using ONLY the sensor IDs and target IDs listed above:
215
+ [{{"sensor_id": "{sensor_ids[0]}", "target_id": "{target_ids[0] if target_ids else 'T0_1'}"}}]
216
  """
217
 
218
 
219
+ def _greedy_proposals(agent_id: str, agent_obs, used_sensors: set, used_targets: set = None) -> list[Proposal]:
220
  """Greedy proposals for one agent — only claim sensors matching agent type."""
221
+ if used_targets is None:
222
+ used_targets = set()
223
  my_sensors = [
224
  s for s in agent_obs.sensors
225
  if s["available"] and s["id"] not in used_sensors
 
229
  [t for t in agent_obs.targets if t["active"]],
230
  key=lambda t: -t["priority"]
231
  )
232
+ proposals = []
233
  for sensor in my_sensors:
234
  for target in targets:
235
  if target["id"] not in used_targets:
 
284
 
285
  def _get_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]:
286
  """Build proposals from all agents. Returns (proposals, source)."""
287
+ import sys
288
  proposals: list[Proposal] = []
289
  used_sensors: set = set()
290
+ print(f"[_get_multi_proposals] called, llm_client={_llm_client is not None}", flush=True)
291
 
292
  if _has_adapters:
293
  return _lora_multi_proposals(agent_obs_map)
 
297
  for agent_id in AGENT_TYPES:
298
  agent_obs = agent_obs_map[agent_id]
299
  prompt = _build_multi_prompt(agent_id, agent_obs)
300
+ if not prompt:
301
+ continue
302
+ print(f"[LLM multi] calling LLM for {agent_id}", flush=True)
303
  response = _llm_client.chat.completions.create(
304
  model=MODEL_NAME,
305
  messages=[{"role": "user", "content": prompt}],
 
307
  temperature=0.0
308
  )
309
  raw = response.choices[0].message.content.strip()
310
+ print(f"[LLM multi] {agent_id} response: {raw[:200]}", flush=True)
311
  start, end = raw.find("["), raw.rfind("]") + 1
312
+ if start == -1 or end == 0:
313
+ print(f"[LLM multi] {agent_id} no JSON array found", flush=True)
314
+ continue
315
  items = json.loads(raw[start:end])
316
+ valid_sensors = {
317
+ s["id"] for s in agent_obs.sensors
318
+ if s["available"] and (agent_id == "command" or s["type"] == agent_id)
319
+ }
320
  valid_targets = {t["id"] for t in agent_obs.targets if t["active"]}
321
  for item in items:
322
  sid, tid = item.get("sensor_id"), item.get("target_id")
323
+ if sid in valid_sensors and tid in valid_targets and sid not in used_sensors:
324
  proposals.append(Proposal(agent_id=agent_id, sensor_id=sid, target_id=tid))
325
  used_sensors.add(sid)
326
  if proposals:
327
+ print(f"[LLM multi] Generated {len(proposals)} proposals", flush=True)
328
  return proposals, "llm"
329
+ print("[LLM multi] No valid proposals, falling back to greedy", flush=True)
330
  except Exception as e:
331
+ print(f"[LLM multi] Error: {e}. Falling back to greedy.", flush=True)
332
 
333
+ # Greedy fallback — partition both sensors and targets across agents
334
  used_sensors = set()
335
+ used_targets: set = set()
336
  for agent_id in AGENT_TYPES:
337
  agent_obs = agent_obs_map[agent_id]
338
+ for p in _greedy_proposals(agent_id, agent_obs, used_sensors, used_targets):
339
  proposals.append(p)
340
  used_sensors.add(p.sensor_id)
341
+ used_targets.add(p.target_id)
342
 
343
  return proposals, "greedy"
344
 
 
467
  mx_env.seed = seed
468
  mx_env.max_steps = max_steps
469
  mx_obs = mx_env.reset()
 
 
470
  return jsonify({
471
  "seed": seed,
472
  "max_steps": max_steps,
 
495
  if p.get("agent_id") and p.get("sensor_id") and p.get("target_id")
496
  ]
497
 
498
+ # Step env with proposals — env runs negotiation internally
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  mx_obs, step_rewards, done, info = mx_env.step_multiagent(proposals)
500
 
501
+ # Extract conflict info from env's internal negotiation layer
502
+ conflict_rate = mx_env.negotiation.get_conflict_rate()
503
+ conflicts = info.get("conflicts", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
  return jsonify({
506
  "observations": {k: v.to_dict() for k, v in mx_obs.items()},
507
  "step_rewards": step_rewards,
508
+ "per_agent_rewards": dict(mx_env._agent_rewards),
509
  "done": done,
510
  "info": info,
511
  "conflict_rate": round(conflict_rate, 4),
512
  "conflicts": conflicts,
513
+ "override_invoked": False,
514
  })
515
 
516
 
517
  @app.post("/auto_multi")
518
  def auto_multi():
519
  global mx_obs
520
+ print(f"[auto_multi] called, mx_obs is None: {mx_obs is None}", flush=True)
521
  if mx_obs is None:
522
  return jsonify({"error": "Call /reset_multi first"}), 400
523
 
524
  proposals, source = _get_multi_proposals(mx_obs)
525
 
526
+ # Step env with proposals — env runs negotiation internally
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  mx_obs, step_rewards, done, info = mx_env.step_multiagent(proposals)
528
 
529
+ # Extract conflict info from env's internal negotiation layer
530
+ conflict_rate = mx_env.negotiation.get_conflict_rate()
531
+ conflicts = info.get("conflicts", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
532
 
533
  return jsonify({
534
  "proposals": [{"agent_id": p.agent_id, "sensor_id": p.sensor_id,
 
536
  "agent": source,
537
  "observations": {k: v.to_dict() for k, v in mx_obs.items()},
538
  "step_rewards": step_rewards,
539
+ "per_agent_rewards": dict(mx_env._agent_rewards),
540
  "done": done,
541
  "info": info,
542
  "conflict_rate": round(conflict_rate, 4),
543
  "conflicts": conflicts,
544
+ "override_invoked": False,
545
  })
546
 
547
 
 
553
 
554
 
555
  if __name__ == "__main__":
556
+ import logging
557
+ logging.basicConfig(level=logging.INFO)
558
  port = int(os.environ.get("PORT", 7860))
559
+ app.run(host="0.0.0.0", port=port, debug=True, use_reloader=False)
tasks/easy_task.py CHANGED
@@ -28,3 +28,8 @@ def get_easy_task():
28
  def get_easy_env():
29
  from env import SentinelEnv
30
  return SentinelEnv(max_steps=EASY_TASK_CONFIG["max_steps"], seed=42, config=EASY_TASK_CONFIG)
 
 
 
 
 
 
28
  def get_easy_env():
29
  from env import SentinelEnv
30
  return SentinelEnv(max_steps=EASY_TASK_CONFIG["max_steps"], seed=42, config=EASY_TASK_CONFIG)
31
+
32
+
33
+ def get_easy_multi_env():
34
+ from env.multiagent import AryaXEnv
35
+ return AryaXEnv(max_steps=EASY_TASK_CONFIG["max_steps"], seed=42, density_factor=1.5)
tasks/grader.py CHANGED
@@ -127,100 +127,79 @@ def run_deterministic_eval(
127
  """
128
  try:
129
  from env.environment import SentinelEnv
130
- from tasks.easy_task import get_easy_env
131
- from tasks.medium_task import get_medium_env
132
- from tasks.hard_task import get_hard_env
 
133
  except ImportError as exc:
134
  raise ImportError(f"env/ or tasks/ package not found: {exc}") from exc
135
 
136
- TASKS = [
137
- ("easy", get_easy_env, 42),
138
- ("medium", get_medium_env, 7),
139
- ("hard", get_hard_env, 13),
140
- ]
 
 
 
 
 
 
 
141
 
142
  results: dict = {}
143
  all_scores: list[float] = []
144
  all_conflict_rates: list[float] = []
145
 
146
  for task_name, get_env_fn, seed in TASKS:
147
- env: SentinelEnv = get_env_fn()
148
- obs = env.reset()
149
-
150
  if mode == "multi":
151
- # Multi-agent mode — uses NegotiationLayer + agents list
152
  try:
153
  from interaction import NegotiationLayer
154
  from interaction.reward import RewardEngine
155
  except ImportError as exc:
156
  raise ImportError(f"interaction/ package not found: {exc}") from exc
157
 
 
 
158
  nl = NegotiationLayer()
159
  reward_eng = RewardEngine()
160
- agent_ids = ["SAT", "UAV", "RDR", "CMD"]
161
  total_rewards: dict[str, float] = {aid: 0.0 for aid in agent_ids}
162
  done = False
163
 
164
  rng = __import__("random").Random(seed)
165
 
166
  while not done:
167
- sensors_list = [
168
- {"id": s.id, "type": s.type, "range": s.range, "available": s.available}
169
- for s in obs.sensors
170
- ]
171
- targets_list = [
172
- {"id": t.id, "priority": t.priority, "active": t.active, "type": "strategic"}
173
- for t in obs.targets
174
- ]
175
-
176
  # Greedy proposals per agent
 
177
  proposals = []
178
- available = [s for s in sensors_list if s.get("available", True)]
179
- active_t = sorted(
180
- [t for t in targets_list if t.get("active", True)],
181
- key=lambda t: -t.get("priority", 0),
182
- )
183
  used_sensors: set[str] = set()
184
  used_targets: set[str] = set()
185
- for i, aid in enumerate(agent_ids):
186
- if i < len(available) and active_t:
187
- sensor = available[i % len(available)]
188
- target = active_t[i % len(active_t)]
189
- if sensor["id"] not in used_sensors:
190
- proposals.append({
191
- "agent_id": aid,
192
- "sensor_id": sensor["id"],
193
- "target_id": target["id"],
194
- "priority_estimate": target.get("priority", 1),
195
- "confidence": 0.8,
196
- "capability_score": 0.7,
197
- })
198
- used_sensors.add(sensor["id"])
199
-
200
- assigned_sids = {p["sensor_id"] for p in proposals}
201
- idle_sensors = [s["id"] for s in sensors_list if s["id"] not in assigned_sids]
202
-
203
- world_state = {
204
- "sensors": sensors_list,
205
- "targets": targets_list,
206
- "idle_sensors": idle_sensors,
207
- "step": env.current_step,
208
- "proposals": proposals,
209
- }
210
-
211
- def _cmd_fn(tied):
212
- return max(tied, key=lambda p: p.get("capability_score", 0.0))
213
-
214
- neg_result = nl.negotiate(proposals, world_state, _cmd_fn)
215
- env_actions = [
216
- {"sensor_id": a["sensor_id"], "target_id": a["target_id"]}
217
- for a in neg_result.final_assignments
218
- ]
219
- obs, _, done, _ = env.step_batch(env_actions)
220
-
221
- step_r = reward_eng.compute_step_reward(
222
- neg_result.final_assignments, neg_result, world_state, agent_ids
223
- )
224
  for aid, r in step_r.items():
225
  total_rewards[aid] += r
226
 
@@ -228,7 +207,7 @@ def run_deterministic_eval(
228
  per_agent_rewards=total_rewards,
229
  step_count=env.max_steps,
230
  num_sensors=env.initial_sensor_count,
231
- negotiation_layer=nl,
232
  num_agents=4,
233
  )
234
  all_scores.append(task_result["overall_score"])
@@ -236,6 +215,8 @@ def run_deterministic_eval(
236
 
237
  else:
238
  # Single-agent mode — uses greedy baseline policy
 
 
239
  try:
240
  from agent.policy import select_action
241
  except ImportError:
 
127
  """
128
  try:
129
  from env.environment import SentinelEnv
130
+ from env.multiagent import AryaXEnv
131
+ from tasks.easy_task import get_easy_env, get_easy_multi_env
132
+ from tasks.medium_task import get_medium_env, get_medium_multi_env
133
+ from tasks.hard_task import get_hard_env, get_hard_multi_env
134
  except ImportError as exc:
135
  raise ImportError(f"env/ or tasks/ package not found: {exc}") from exc
136
 
137
+ if mode == "multi":
138
+ TASKS = [
139
+ ("easy", get_easy_multi_env, 42),
140
+ ("medium", get_medium_multi_env, 7),
141
+ ("hard", get_hard_multi_env, 13),
142
+ ]
143
+ else:
144
+ TASKS = [
145
+ ("easy", get_easy_env, 42),
146
+ ("medium", get_medium_env, 7),
147
+ ("hard", get_hard_env, 13),
148
+ ]
149
 
150
  results: dict = {}
151
  all_scores: list[float] = []
152
  all_conflict_rates: list[float] = []
153
 
154
  for task_name, get_env_fn, seed in TASKS:
 
 
 
155
  if mode == "multi":
156
+ # Multi-agent mode — uses AryaXEnv + NegotiationLayer
157
  try:
158
  from interaction import NegotiationLayer
159
  from interaction.reward import RewardEngine
160
  except ImportError as exc:
161
  raise ImportError(f"interaction/ package not found: {exc}") from exc
162
 
163
+ env: AryaXEnv = get_env_fn()
164
+ obs_map = env.reset()
165
  nl = NegotiationLayer()
166
  reward_eng = RewardEngine()
167
+ agent_ids = ["satellite", "drone", "radar", "command"]
168
  total_rewards: dict[str, float] = {aid: 0.0 for aid in agent_ids}
169
  done = False
170
 
171
  rng = __import__("random").Random(seed)
172
 
173
  while not done:
 
 
 
 
 
 
 
 
 
174
  # Greedy proposals per agent
175
+ from env.multiagent import Proposal
176
  proposals = []
 
 
 
 
 
177
  used_sensors: set[str] = set()
178
  used_targets: set[str] = set()
179
+ for agent_id in agent_ids:
180
+ agent_obs = obs_map[agent_id]
181
+ my_sensors = [
182
+ s for s in agent_obs.sensors
183
+ if s["available"] and s["id"] not in used_sensors
184
+ and (agent_id == "command" or s["type"] == agent_id)
185
+ ]
186
+ targets = sorted(
187
+ [t for t in agent_obs.targets if t["active"]],
188
+ key=lambda t: -t["priority"]
189
+ )
190
+ for sensor in my_sensors:
191
+ for target in targets:
192
+ if target["id"] not in used_targets:
193
+ proposals.append(Proposal(
194
+ agent_id=agent_id,
195
+ sensor_id=sensor["id"],
196
+ target_id=target["id"]
197
+ ))
198
+ used_sensors.add(sensor["id"])
199
+ used_targets.add(target["id"])
200
+ break
201
+
202
+ obs_map, step_r, done, info = env.step_multiagent(proposals)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  for aid, r in step_r.items():
204
  total_rewards[aid] += r
205
 
 
207
  per_agent_rewards=total_rewards,
208
  step_count=env.max_steps,
209
  num_sensors=env.initial_sensor_count,
210
+ negotiation_layer=env.negotiation,
211
  num_agents=4,
212
  )
213
  all_scores.append(task_result["overall_score"])
 
215
 
216
  else:
217
  # Single-agent mode — uses greedy baseline policy
218
+ env: SentinelEnv = get_env_fn()
219
+ obs = env.reset()
220
  try:
221
  from agent.policy import select_action
222
  except ImportError:
tasks/hard_task.py CHANGED
@@ -61,3 +61,8 @@ def get_hard_task():
61
  def get_hard_env():
62
  from env import SentinelEnv
63
  return SentinelEnv(max_steps=HARD_TASK_CONFIG["max_steps"], seed=13, config=HARD_TASK_CONFIG)
 
 
 
 
 
 
61
  def get_hard_env():
62
  from env import SentinelEnv
63
  return SentinelEnv(max_steps=HARD_TASK_CONFIG["max_steps"], seed=13, config=HARD_TASK_CONFIG)
64
+
65
+
66
+ def get_hard_multi_env():
67
+ from env.multiagent import AryaXEnv
68
+ return AryaXEnv(max_steps=HARD_TASK_CONFIG["max_steps"], seed=13, density_factor=4.0, failure_prob=0.15, conflict_injection=True)
tasks/medium_task.py CHANGED
@@ -45,3 +45,8 @@ def get_medium_task():
45
  def get_medium_env():
46
  from env import SentinelEnv
47
  return SentinelEnv(max_steps=MEDIUM_TASK_CONFIG["max_steps"], seed=7, config=MEDIUM_TASK_CONFIG)
 
 
 
 
 
 
45
  def get_medium_env():
46
  from env import SentinelEnv
47
  return SentinelEnv(max_steps=MEDIUM_TASK_CONFIG["max_steps"], seed=7, config=MEDIUM_TASK_CONFIG)
48
+
49
+
50
+ def get_medium_multi_env():
51
+ from env.multiagent import AryaXEnv
52
+ return AryaXEnv(max_steps=MEDIUM_TASK_CONFIG["max_steps"], seed=7, density_factor=2.5, conflict_injection=True)
test_integration.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ARYA-X Full Integration Test
3
+ Tests every module connection described in the spec.
4
+ """
5
+ import sys
6
+ import traceback
7
+
8
+ PASS = []
9
+ FAIL = []
10
+
11
+ def check(name, fn):
12
+ try:
13
+ fn()
14
+ PASS.append(name)
15
+ print(f" PASS {name}")
16
+ except Exception as e:
17
+ FAIL.append((name, str(e)))
18
+ print(f" FAIL {name}")
19
+ print(f" {e}")
20
+
21
+ # ── 1. Core imports ────────────────────────────────────────────────────────────
22
+ check("env/__init__ exports SentinelEnv", lambda: __import__("env").SentinelEnv)
23
+ check("env.models all 6 models", lambda: [
24
+ getattr(__import__("env.models", fromlist=["Sensor"]), x)
25
+ for x in ["Sensor","Target","Observation","Action","Proposal","AgentObservation","ConflictRecord","EpisodeMetrics"]
26
+ ])
27
+ check("env.dynamics all 4 functions", lambda: [
28
+ getattr(__import__("env.dynamics", fromlist=["initialize_sensors"]), x)
29
+ for x in ["initialize_sensors","spawn_targets","spawn_targets_stochastic","apply_correlated_failures"]
30
+ ])
31
+ check("env.world_model all 3 functions", lambda: [
32
+ getattr(__import__("env.world_model", fromlist=["add_observation_noise"]), x)
33
+ for x in ["add_observation_noise","apply_mask","get_priority_mapping"]
34
+ ])
35
+ check("interaction package all 4 classes", lambda: [
36
+ getattr(__import__("interaction", fromlist=["NegotiationLayer"]), x)
37
+ for x in ["NegotiationLayer","ConflictDetector","ConflictResolver","RewardEngine"]
38
+ ])
39
+ check("agents package all 4 agents", lambda: [
40
+ getattr(__import__("agents", fromlist=["SatelliteAgent"]), x)
41
+ for x in ["SatelliteAgent","DroneAgent","RadarAgent","CommandAgent"]
42
+ ])
43
+ check("curriculum.CurriculumEngine", lambda: __import__("curriculum").CurriculumEngine)
44
+ check("trainer.ARYAXTrainer importable", lambda: __import__("trainer").ARYAXTrainer)
45
+ check("tasks.grader all 3 functions", lambda: [
46
+ getattr(__import__("tasks.grader", fromlist=["grade_episode"]), x)
47
+ for x in ["grade_episode","grade_multi_agent_episode","run_deterministic_eval"]
48
+ ])
49
+
50
+ # ── 2. env/environment.py — both AryaXEnv classes ─────────────────────────────
51
+ check("env.environment.SentinelEnv exists", lambda: __import__("env.environment", fromlist=["SentinelEnv"]).SentinelEnv)
52
+ check("env.environment.AryaXEnv (rich) exists", lambda: __import__("env.environment", fromlist=["AryaXEnv"]).AryaXEnv)
53
+ check("env.multiagent.AryaXEnv (server) exists", lambda: __import__("env.multiagent", fromlist=["AryaXEnv"]).AryaXEnv)
54
+
55
+ # ── 3. SentinelEnv full episode ────────────────────────────────────────────────
56
+ def _sentinel_episode():
57
+ from env.environment import SentinelEnv
58
+ env = SentinelEnv(max_steps=3, seed=42)
59
+ obs = env.reset()
60
+ assert obs.sensors and obs.targets
61
+ from env.models import Action
62
+ obs, r, done, info = env.step(Action(sensor_id=obs.sensors[0].id, target_id=obs.targets[0].id))
63
+ assert "step_count" in info
64
+ check("SentinelEnv reset/step/info", _sentinel_episode)
65
+
66
+ # ── 4. env.environment.AryaXEnv (rich) — partial obs + world model ────────────
67
+ def _rich_aryax():
68
+ from env.environment import AryaXEnv
69
+ env = AryaXEnv(max_steps=3, seed=42, density_factor=2.5, failure_prob=0.1)
70
+ obs_map = env.reset()
71
+ assert set(obs_map.keys()) == {"satellite","drone","radar","command"}
72
+ # Each agent gets different target counts (partial obs)
73
+ sat_t = len(obs_map["satellite"].targets)
74
+ drone_t = len(obs_map["drone"].targets)
75
+ # satellite sees all, drone sees subset — may differ
76
+ assert sat_t >= 0 and drone_t >= 0
77
+ check("env.environment.AryaXEnv partial obs + world model", _rich_aryax)
78
+
79
+ # ── 5. env.multiagent.AryaXEnv (server) — interaction/ pipeline ───────────────
80
+ def _server_aryax():
81
+ from env.multiagent import AryaXEnv, Proposal
82
+ env = AryaXEnv(max_steps=3, seed=42)
83
+ obs = env.reset()
84
+ proposals = [
85
+ Proposal(agent_id="satellite", sensor_id=obs["satellite"].sensors[0]["id"],
86
+ target_id=obs["satellite"].targets[0]["id"]),
87
+ ]
88
+ obs2, rewards, done, info = env.step_multiagent(proposals)
89
+ assert "conflict_rate" in info
90
+ assert "step_rewards" in info
91
+ assert set(rewards.keys()) == {"satellite","drone","radar","command"}
92
+ check("env.multiagent.AryaXEnv step_multiagent + conflict_rate", _server_aryax)
93
+
94
+ # ── 6. interaction/ pipeline end-to-end ───────────────────────────────────────
95
+ def _interaction_pipeline():
96
+ from interaction import NegotiationLayer
97
+ from interaction.reward import RewardEngine
98
+ nl = NegotiationLayer()
99
+ re = RewardEngine()
100
+ proposals = [
101
+ {"agent_id":"satellite","sensor_id":"S1","target_id":"T0_1","capability_score":0.9},
102
+ {"agent_id":"drone", "sensor_id":"S2","target_id":"T0_1","capability_score":0.5}, # conflict
103
+ ]
104
+ world_state = {
105
+ "sensors": [{"id":"S1","type":"satellite","range":500,"available":True},
106
+ {"id":"S2","type":"drone","range":100,"available":True}],
107
+ "targets": [{"id":"T0_1","priority":3,"active":True,"type":"strategic"},
108
+ {"id":"T0_2","priority":1,"active":True,"type":"airspace"}],
109
+ "idle_sensors": [],
110
+ "step": 0,
111
+ "proposals": proposals,
112
+ }
113
+ result = nl.negotiate(proposals, world_state, lambda tied: max(tied, key=lambda p: p.get("capability_score",0)))
114
+ assert len(result.conflicts_detected) > 0, "Expected conflict"
115
+ assert nl.get_conflict_rate() > 0
116
+ rewards = re.compute_step_reward(result.final_assignments, result, world_state, ["satellite","drone","radar","command"])
117
+ assert isinstance(rewards, dict)
118
+ check("interaction/ NegotiationLayer + RewardEngine pipeline", _interaction_pipeline)
119
+
120
+ # ── 7. agents/ — observe/propose interface ────────────────────────────────────
121
+ def _agents_propose():
122
+ from agents import SatelliteAgent, DroneAgent, RadarAgent, CommandAgent
123
+ from env.models import AgentObservation, Sensor, Target
124
+ sensors = [Sensor(id="S1", type="satellite", range=500, available=True)]
125
+ targets = [Target(id="T0_1", priority=3, active=True),
126
+ Target(id="T0_2", priority=2, active=True)]
127
+ obs = AgentObservation(agent_id="satellite", agent_type="satellite",
128
+ sensors=sensors, targets=targets, timestep=0)
129
+ sat = SatelliteAgent()
130
+ sat.observe(obs)
131
+ props = sat.propose()
132
+ assert isinstance(props, list)
133
+
134
+ drone_obs = AgentObservation(agent_id="drone", agent_type="drone",
135
+ sensors=[Sensor(id="S2",type="drone",range=100,available=True)],
136
+ targets=targets, timestep=0)
137
+ d = DroneAgent()
138
+ d.observe(drone_obs)
139
+ assert isinstance(d.propose(), list)
140
+
141
+ radar_obs = AgentObservation(agent_id="radar", agent_type="radar",
142
+ sensors=[Sensor(id="S3",type="radar",range=300,available=True)],
143
+ targets=targets, timestep=0)
144
+ r = RadarAgent()
145
+ r.observe(radar_obs)
146
+ assert isinstance(r.propose(), list)
147
+
148
+ cmd_obs = AgentObservation(agent_id="command", agent_type="command",
149
+ sensors=sensors, targets=targets, timestep=0)
150
+ c = CommandAgent()
151
+ c.observe(cmd_obs)
152
+ assert isinstance(c.propose(), list)
153
+ check("agents/ all 4 observe()+propose() callable", _agents_propose)
154
+
155
+ # ── 8. CommandAgent.observe() accepts proposals kwarg ─────────────────────────
156
+ def _cmd_proposals_kwarg():
157
+ from agents import CommandAgent
158
+ from env.models import AgentObservation, Sensor, Target, Proposal
159
+ sensors = [Sensor(id="S1",type="satellite",range=500,available=True)]
160
+ targets = [Target(id="T0_1",priority=3,active=True)]
161
+ obs = AgentObservation(agent_id="command",agent_type="command",
162
+ sensors=sensors,targets=targets,timestep=0)
163
+ existing = [Proposal(sensor_id="S1",target_id="T0_1",agent_id="satellite",
164
+ priority_estimate=3,confidence=0.9)]
165
+ c = CommandAgent()
166
+ c.observe(obs, proposals=existing)
167
+ props = c.propose()
168
+ assert isinstance(props, list)
169
+ check("CommandAgent.observe(obs, proposals=...) signature", _cmd_proposals_kwarg)
170
+
171
+ # ── 9. DroneAgent theory-of-mind (loss tracking) ──────────────────────────────
172
+ def _drone_tom():
173
+ from agents import DroneAgent
174
+ from env.models import AgentObservation, Sensor, Target, ConflictRecord
175
+ sensors = [Sensor(id="S2",type="drone",range=100,available=True)]
176
+ targets = [Target(id="T0_2",priority=2,active=True)]
177
+ obs = AgentObservation(agent_id="drone",agent_type="drone",
178
+ sensors=sensors,targets=targets,timestep=0)
179
+ d = DroneAgent()
180
+ d.observe(obs)
181
+ # Simulate 3 losses to radar on redundant_coverage
182
+ d.losses["redundant_coverage"] = 3
183
+ props = d.propose()
184
+ # Boundary target (even index) should have reduced confidence
185
+ boundary_props = [p for p in props if d._is_boundary_target(p.target_id)]
186
+ if boundary_props:
187
+ assert boundary_props[0].confidence <= 0.5
188
+ check("DroneAgent theory-of-mind confidence reduction", _drone_tom)
189
+
190
+ # ── 10. CurriculumEngine → AryaXEnv config connection ─────────────────────────
191
+ def _curriculum_to_env():
192
+ from curriculum import CurriculumEngine
193
+ from env.multiagent import AryaXEnv
194
+ ce = CurriculumEngine()
195
+ cfg = ce.get_scenario_config()
196
+ assert "max_steps" in cfg
197
+ assert "sensor_failure_prob" in cfg
198
+ env = AryaXEnv(
199
+ max_steps=cfg["max_steps"],
200
+ seed=42,
201
+ density_factor=1.5 + ce.difficulty_level * 2.5,
202
+ failure_prob=cfg["sensor_failure_prob"],
203
+ )
204
+ obs = env.reset()
205
+ assert obs is not None
206
+ check("CurriculumEngine config -> AryaXEnv constructor", _curriculum_to_env)
207
+
208
+ # ── 11. grader.grade_multi_agent_episode ──────────────────────────────────────
209
+ def _grader_multi():
210
+ from tasks.grader import grade_multi_agent_episode
211
+ from interaction import NegotiationLayer
212
+ nl = NegotiationLayer()
213
+ result = grade_multi_agent_episode(
214
+ per_agent_rewards={"SAT":10.0,"UAV":5.0,"RDR":3.0,"CMD":2.0},
215
+ step_count=20,
216
+ num_sensors=4,
217
+ negotiation_layer=nl,
218
+ num_agents=4,
219
+ )
220
+ assert "overall_score" in result
221
+ assert "coordination_score" in result
222
+ assert "conflict_rate" in result
223
+ check("grader.grade_multi_agent_episode", _grader_multi)
224
+
225
+ # ── 12. server.py imports cleanly ─────────────────────────────────────────────
226
+ check("server.py imports without error", lambda: __import__("server"))
227
+
228
+ # ── 13. inference.py imports + run_multi_task callable ────────────────────────
229
+ def _inference_multi():
230
+ import inference
231
+ assert callable(inference.run_multi_task)
232
+ assert callable(inference._greedy_multi_proposals)
233
+ check("inference.py run_multi_task callable", _inference_multi)
234
+
235
+ # ── 14. trainer.py ARYAXTrainer instantiates (no model) ──────────────────────
236
+ def _trainer_init():
237
+ from trainer import ARYAXTrainer
238
+ # Should not raise even without GPU/model
239
+ t = ARYAXTrainer.__new__(ARYAXTrainer)
240
+ t.curriculum = __import__("curriculum").CurriculumEngine()
241
+ from interaction import NegotiationLayer
242
+ from interaction.reward import RewardEngine
243
+ t.negotiation = NegotiationLayer()
244
+ t.reward_engine = RewardEngine()
245
+ t._grpo_trainer = None
246
+ assert t._grpo_trainer is None
247
+ check("ARYAXTrainer attributes accessible", _trainer_init)
248
+
249
+ # ── 15. tasks/ easy/medium/hard envs return SentinelEnv ───────────────────────
250
+ def _task_envs():
251
+ from tasks.easy_task import get_easy_env
252
+ from tasks.medium_task import get_medium_env
253
+ from tasks.hard_task import get_hard_env
254
+ from env.environment import SentinelEnv
255
+ for fn in [get_easy_env, get_medium_env, get_hard_env]:
256
+ e = fn()
257
+ assert isinstance(e, SentinelEnv)
258
+ obs = e.reset()
259
+ assert obs.sensors
260
+ check("tasks/ easy/medium/hard envs", _task_envs)
261
+
262
+ # ── 16. env.multiagent NegotiationLayer is interaction/ (not duplicate) ────────
263
+ def _no_duplicate_nl():
264
+ from env.multiagent import NegotiationLayer as MaNL
265
+ from interaction import NegotiationLayer as INL
266
+ assert MaNL is INL, "env.multiagent.NegotiationLayer must BE interaction.NegotiationLayer"
267
+ check("env.multiagent.NegotiationLayer IS interaction.NegotiationLayer (no duplicate)", _no_duplicate_nl)
268
+
269
+ # ── Summary ────────────────────────────────────────────────────────────────────
270
+ print(f"\n{'='*55}")
271
+ print(f" PASSED: {len(PASS)}/{len(PASS)+len(FAIL)}")
272
+ if FAIL:
273
+ print(f" FAILED: {len(FAIL)}")
274
+ for name, err in FAIL:
275
+ print(f" - {name}")
276
+ print(f" {err}")
277
+ print(f"{'='*55}")
278
+ sys.exit(0 if not FAIL else 1)
train_colab.py CHANGED
@@ -252,14 +252,11 @@ for ep in range(1, NUM_EPISODES + 1):
252
  if grpo_trainer is not None and proposals:
253
  queries = [json.dumps({"sensors": env_state["sensors"], "step": ep}) for _ in proposals]
254
  responses = [json.dumps(p) for p in proposals]
255
- try:
256
- grpo_trainer.step(
257
- queries=queries,
258
- completions=responses,
259
- rewards=[ep_reward / max(len(proposals), 1)] * len(proposals),
260
- )
261
- except Exception as _e:
262
- pass # graceful degradation if TRL API differs
263
 
264
  if ep % 5 == 0 or ep == 1:
265
  print(
 
252
  if grpo_trainer is not None and proposals:
253
  queries = [json.dumps({"sensors": env_state["sensors"], "step": ep}) for _ in proposals]
254
  responses = [json.dumps(p) for p in proposals]
255
+ grpo_trainer.step(
256
+ queries=queries,
257
+ responses=responses,
258
+ rewards=[ep_reward / max(len(proposals), 1)] * len(proposals),
259
+ )
 
 
 
260
 
261
  if ep % 5 == 0 or ep == 1:
262
  print(
trainer.py CHANGED
@@ -295,17 +295,24 @@ class ARYAXTrainer:
295
 
296
  for episode in range(1, self.num_episodes + 1):
297
  # ── Phase 1: Episode rollout ──────────────────────────
 
 
298
  self.curriculum.reset_episode()
299
  cfg = self.curriculum.get_scenario_config()
300
 
301
  ep_seed = cfg.get("seed_override") or rng.randint(0, 10000)
302
  conflict_injection = cfg.get("conflict_injection", False)
 
 
303
 
304
  env = AryaXEnv(
305
  max_steps=cfg["max_steps"],
306
  seed=ep_seed,
307
  density_factor=1.5 + self.curriculum.difficulty_level * 2.5,
308
  failure_prob=cfg["sensor_failure_prob"],
 
 
 
309
  )
310
  # ── PARTIAL OBSERVABILITY: use AryaXEnv.reset() → per-agent obs ──
311
  obs_map = env.reset() # Dict[agent_id → AgentObservation (dataclass)]
@@ -332,11 +339,11 @@ class ARYAXTrainer:
332
  targets_list_by_agent: dict[str, list] = {}
333
 
334
  # First pass: satellite, drone, radar observe & propose
 
335
  for canonical_id in ["satellite", "drone", "radar"]:
336
  agent = self._agents[canonical_id]
337
- agent_obs = obs_map[canonical_id] # AgentObservation dataclass
338
 
339
- # Build models.AgentObservation for the agent's observe() method
340
  from env.models import AgentObservation as ModelAgentObs, Sensor as ModelSensor, Target as ModelTarget
341
  model_obs = ModelAgentObs(
342
  agent_id=canonical_id,
@@ -347,7 +354,13 @@ class ARYAXTrainer:
347
  conflict_history=[],
348
  )
349
  agent.observe(model_obs)
350
- proposals_obj = agent.propose()
 
 
 
 
 
 
351
  all_proposals_obj.extend(proposals_obj)
352
 
353
  sensors_list_by_agent[canonical_id] = agent_obs.sensors
@@ -418,14 +431,16 @@ class ARYAXTrainer:
418
  for canonical_id in AGENT_TYPES:
419
  episode_rewards[canonical_id] += step_rewards.get(canonical_id, 0.0)
420
 
421
- # Update agent conflict history
422
- for conflict in neg_result.conflicts_detected:
423
- for canonical_id in AGENT_TYPES:
424
- if canonical_id in (conflict.involved_agents or []):
425
- self._agents[canonical_id].update(
426
- step_rewards.get(canonical_id, 0.0),
427
- []
428
- )
 
 
429
 
430
  # Store GRPO experiences (keyed by short AGENT_IDS for backward compat)
431
  for short_id in AGENT_IDS:
@@ -492,6 +507,8 @@ class ARYAXTrainer:
492
  "difficulty": round(curr_update["difficulty"], 3),
493
  "phase": curr_update["phase"],
494
  "conflict_injection": conflict_injection,
 
 
495
  })
496
  # Flush metrics every 10 episodes (avoid losing data on crash)
497
  if episode % 10 == 0:
@@ -586,7 +603,7 @@ class ARYAXTrainer:
586
  try:
587
  self._grpo_trainer.step(
588
  queries=list(queries),
589
- completions=list(responses),
590
  rewards=advantages,
591
  )
592
  print(
@@ -606,7 +623,7 @@ class ARYAXTrainer:
606
  try:
607
  from tasks.grader import run_deterministic_eval
608
  # Pass None since we are using greedy agents, not LLM agents
609
- result = run_deterministic_eval(agents=None, verbose=True, mode="single")
610
  conflict_rate = self.negotiation.get_conflict_rate()
611
  coordination_score = 1.0 - conflict_rate
612
  result["conflict_rate"] = conflict_rate
 
295
 
296
  for episode in range(1, self.num_episodes + 1):
297
  # ── Phase 1: Episode rollout ──────────────────────────
298
+ # reset_episode() only checks self-play triggers; episode_count
299
+ # is incremented once by update() at episode end.
300
  self.curriculum.reset_episode()
301
  cfg = self.curriculum.get_scenario_config()
302
 
303
  ep_seed = cfg.get("seed_override") or rng.randint(0, 10000)
304
  conflict_injection = cfg.get("conflict_injection", False)
305
+ targets_range = cfg.get("targets_per_step_range", (0, 0))
306
+ freeze_agent = cfg.get("freeze_agent")
307
 
308
  env = AryaXEnv(
309
  max_steps=cfg["max_steps"],
310
  seed=ep_seed,
311
  density_factor=1.5 + self.curriculum.difficulty_level * 2.5,
312
  failure_prob=cfg["sensor_failure_prob"],
313
+ conflict_injection=cfg["conflict_injection"],
314
+ min_targets=targets_range[0],
315
+ max_targets=targets_range[1],
316
  )
317
  # ── PARTIAL OBSERVABILITY: use AryaXEnv.reset() → per-agent obs ──
318
  obs_map = env.reset() # Dict[agent_id → AgentObservation (dataclass)]
 
339
  targets_list_by_agent: dict[str, list] = {}
340
 
341
  # First pass: satellite, drone, radar observe & propose
342
+ # Frozen agent (self-play curriculum) skips propose() entirely
343
  for canonical_id in ["satellite", "drone", "radar"]:
344
  agent = self._agents[canonical_id]
345
+ agent_obs = obs_map[canonical_id]
346
 
 
347
  from env.models import AgentObservation as ModelAgentObs, Sensor as ModelSensor, Target as ModelTarget
348
  model_obs = ModelAgentObs(
349
  agent_id=canonical_id,
 
354
  conflict_history=[],
355
  )
356
  agent.observe(model_obs)
357
+ # freeze_agent uses short-code (SAT/UAV/RDR); map to canonical
358
+ _freeze_map = {"SAT": "satellite", "UAV": "drone", "RDR": "radar"}
359
+ frozen_canonical = _freeze_map.get(freeze_agent)
360
+ if frozen_canonical == canonical_id:
361
+ proposals_obj = [] # frozen — contributes nothing this step
362
+ else:
363
+ proposals_obj = agent.propose()
364
  all_proposals_obj.extend(proposals_obj)
365
 
366
  sensors_list_by_agent[canonical_id] = agent_obs.sensors
 
431
  for canonical_id in AGENT_TYPES:
432
  episode_rewards[canonical_id] += step_rewards.get(canonical_id, 0.0)
433
 
434
+ # Update agent conflict history with real conflict records
435
+ for canonical_id in AGENT_TYPES:
436
+ agent_conflicts = [
437
+ c for c in neg_result.conflicts_detected
438
+ if canonical_id in (getattr(c, "involved_agents", []) or [])
439
+ ]
440
+ self._agents[canonical_id].update(
441
+ step_rewards.get(canonical_id, 0.0),
442
+ agent_conflicts
443
+ )
444
 
445
  # Store GRPO experiences (keyed by short AGENT_IDS for backward compat)
446
  for short_id in AGENT_IDS:
 
507
  "difficulty": round(curr_update["difficulty"], 3),
508
  "phase": curr_update["phase"],
509
  "conflict_injection": conflict_injection,
510
+ "targets_range": list(targets_range),
511
+ "freeze_agent": freeze_agent,
512
  })
513
  # Flush metrics every 10 episodes (avoid losing data on crash)
514
  if episode % 10 == 0:
 
603
  try:
604
  self._grpo_trainer.step(
605
  queries=list(queries),
606
+ responses=list(responses),
607
  rewards=advantages,
608
  )
609
  print(
 
623
  try:
624
  from tasks.grader import run_deterministic_eval
625
  # Pass None since we are using greedy agents, not LLM agents
626
+ result = run_deterministic_eval(agents=None, verbose=True, mode="multi")
627
  conflict_rate = self.negotiation.get_conflict_rate()
628
  coordination_score = 1.0 - conflict_rate
629
  result["conflict_rate"] = conflict_rate