OSCR

Neural Surrogate-Enhanced Metaheuristic Optimization for Distributed Quadrotor Swarm Control.

Code ↔ Paper

4 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 4 matches
  1. [1] § 5. Results and Discussion › 5.1. Experimental Protocol ↔ src/eval_closed_loop.py, lines 172–195 · score 0.72 · step overrun ratio, collision free rate, obstacle hard, violation, sum, closed loop
  2. [2] § 2. System Modeling and Problem Formulation › 2.3. Obstacles and Gap-Based Avoidance Model ↔ src/mpio_swarm_simv1.py, lines 313–354 · score 0.59 · interior gaps, boundary gaps, obstacle avoidance
  3. [3] § 5. Results and Discussion › 5.2. Closed-Loop Simulation Results ↔ src/eval_closed_loop.py, lines 172–195 · score 0.57 · step overrun ratio, collision free rate, closed loop, budget, formation, margin
  4. [4] § 3. Multi-Objective Optimization Formulation and Online Modified MPIO Solver ↔ src/base_mpio_solver.py, lines 398–487 · score 0.51 · MPIO solver, obstacle avoidance, vector, pigeon, optimize, UAV

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 488 lines · 22 KB · MIT · 2 matches

  1. import argparse
  2. import copy
  3. import json
  4. from pathlib import Path
  5. from typing import Dict, Iterable, List, Tuple
  6. import numpy as np
  7. import torch
  8. from base_mpio_solver import SimParams as BaseSimParams, run_episode as run_base_episode
  9. from mpio_swarm_simv1 import (
  10. SimParams as ModifiedSimParams,
  11. extract_features,
  12. neighbors_within,
  13. run_episode as run_modified_episode,
  14. )
  15. from dataset_gen import sample_scene, _init_swarm, N_RANGE
  16. TARGET_SCENE_TYPES = (
  17. "b_chicane_chain",
  18. "e_pillar_forest",
  19. "g_forest_dynamic_spheres",
  20. )
  21. METHODS = ("mlp", "base_mpio", "mpio")
  22. def _n_bucket(n_uav: int) -> str:
  23. if n_uav <= 5:
  24. return "3-5"
  25. if n_uav <= 8:
  26. return "6-8"
  27. return f"9-{N_RANGE[1]}"
  28. def _parse_csv_list(raw: str) -> List[str]:
  29. if not raw:
  30. return []
  31. return [item.strip() for item in raw.split(",") if item.strip()]
  32. def _to_jsonable(value):
  33. if isinstance(value, np.ndarray):
  34. return value.tolist()
  35. if isinstance(value, np.generic):
  36. return value.item()
  37. if isinstance(value, dict):
  38. return {str(k): _to_jsonable(v) for k, v in value.items()}
  39. if isinstance(value, (list, tuple)):
  40. return [_to_jsonable(v) for v in value]
  41. return value
  42. def _progress_path(out_path: Path) -> Path:
  43. return out_path.parent / f"{out_path.name}.progress.json"
  44. def _save_scene_json(scene_cfg: Dict[str, object], out_path: Path) -> None:
  45. out_path.parent.mkdir(parents=True, exist_ok=True)
  46. out_path.write_text(json.dumps(_to_jsonable(scene_cfg), indent=2), encoding="utf-8")
  47. def sample_scene_legacy_easy(rng: np.random.Generator) -> Dict[str, object]:
  48. n_uav = int(rng.integers(5, 7))
  49. ve_mag = float(rng.uniform(6.0, 12.0))
  50. try:
  51. from dataset_gen import _scene_legacy_easy
  52. except ImportError as exc:
  53. raise SystemExit(
  54. "legacy_easy scene is not available in dataset_gen.py; "
  55. "run without --legacy_easy_only or add _scene_legacy_easy."
  56. ) from exc
  57. obstacles, obstacles_vxy = _scene_legacy_easy(rng)
  58. init_p, init_vxy = _init_swarm(rng, n_uav, ve_mag)
  59. return {
  60. "scene_type": "legacy_easy",
  61. "N_uav": n_uav,
  62. "ve_mag": ve_mag,
  63. "init_P": init_p,
  64. "init_Vxy": init_vxy,
  65. "ve_xy": (ve_mag, 0.0),
  66. "obstacles": obstacles,
  67. "obstacles_vxy": obstacles_vxy,
  68. "obstacles_use_prediction": True,
  69. "square_enable": False,
  70. "formation_err_mean_thresh": 3.0,
  71. "formation_err_max_thresh": 10.0,
  72. "formation_over_limit": 0.20,
  73. }
  74. def _load_scene_cfg(path: Path) -> Dict[str, object]:
  75. data = json.loads(path.read_text(encoding="utf-8"))
  76. array_keys = {"init_P", "init_Vxy", "obstacles", "obstacles_vxy"}
  77. tuple_keys = {"ve_xy", "square_center_xy", "square_vxy", "obstacles_bounds"}
  78. scene_cfg: Dict[str, object] = {}
  79. for key, value in data.items():
  80. if value is None:
  81. scene_cfg[key] = None
  82. elif key in array_keys:
  83. scene_cfg[key] = np.asarray(value, dtype=float)
  84. elif key in tuple_keys:
  85. scene_cfg[key] = tuple(value)
  86. else:
  87. scene_cfg[key] = value
  88. scene_cfg.setdefault("scene_type", "custom")
  89. scene_cfg.setdefault("scene_level", 1)
  90. scene_cfg.setdefault("N_uav", 0)
  91. scene_cfg.setdefault("ve_mag", 0.0)
  92. scene_cfg.setdefault("he", 50.0)
  93. scene_cfg.setdefault("ve_xy", (0.0, 0.0))
  94. scene_cfg.setdefault("obstacles_use_prediction", False)
  95. scene_cfg.setdefault("square_enable", False)
  96. scene_cfg.setdefault("square_center_xy", (0.0, 0.0))
  97. scene_cfg.setdefault("square_side", 0.0)
  98. scene_cfg.setdefault("square_vxy", (0.0, 0.0))
  99. scene_cfg.setdefault("x_goal", 0.0)
  100. scene_cfg.setdefault("y_goal", 0.0)
  101. scene_cfg.setdefault("rng_seed", None)
  102. return scene_cfg
  103. def _new_method_stats(selected_methods: Iterable[str]) -> Dict[str, Dict[str, float]]:
  104. return {method: {} for method in selected_methods}
  105. def _accumulate(stats: Dict[str, float], results: Dict[str, object], formation_metrics: Dict[str, float]) -> None:
  106. stats["count"] = stats.get("count", 0) + 1
  107. stats["safe_ok"] = stats.get("safe_ok", 0) + int(results["safe_ok"])
  108. stats["reach_ok"] = stats.get("reach_ok", 0) + int(results["reach_ok"])
  109. stats["formation_ok"] = stats.get("formation_ok", 0) + int(results["formation_ok"])
  110. stats["formation_mean"] = stats.get("formation_mean", 0.0) + formation_metrics["mean_err"]
  111. stats["formation_max"] = stats.get("formation_max", 0.0) + formation_metrics["max_err"]
  112. cost_flags = results.get("cost_flags", {})
  113. stats["collision_obs"] = stats.get("collision_obs", 0) + int(cost_flags.get("c3_any", False))
  114. stats["collision_obs_hard"] = stats.get("collision_obs_hard", 0) + int(cost_flags.get("c3_hard_any", False))
  115. stats["collision_nbr"] = stats.get("collision_nbr", 0) + int(cost_flags.get("c4_any", False))
  116. hard_collision = bool(cost_flags.get("c3_hard_any", False)) or bool(cost_flags.get("c4_any", False))
  117. stats["collision_free"] = stats.get("collision_free", 0) + int(not hard_collision)
  118. latency = results.get("latency", {})
  119. stats["lat_step_mean_sum"] = stats.get("lat_step_mean_sum", 0.0) + float(latency.get("step_mean_ms", 0.0))
  120. stats["lat_step_std_sum"] = stats.get("lat_step_std_sum", 0.0) + float(latency.get("step_std_ms", 0.0))
  121. stats["lat_step_p95_sum"] = stats.get("lat_step_p95_sum", 0.0) + float(latency.get("step_p95_ms", 0.0))
  122. stats["lat_decision_mean_sum"] = stats.get("lat_decision_mean_sum", 0.0) + float(latency.get("decision_mean_ms", 0.0))
  123. stats["lat_decision_std_sum"] = stats.get("lat_decision_std_sum", 0.0) + float(latency.get("decision_std_ms", 0.0))
  124. stats["lat_decision_p95_sum"] = stats.get("lat_decision_p95_sum", 0.0) + float(latency.get("decision_p95_ms", 0.0))
  125. stats["lat_step_overrun_count"] = stats.get("lat_step_overrun_count", 0) + int(latency.get("step_overrun_count", 0))
  126. stats["lat_step_count"] = stats.get("lat_step_count", 0) + int(latency.get("step_count", 0))
  127. stats["lat_budget_ms"] = float(latency.get("budget_ms", stats.get("lat_budget_ms", 0.0)))
  128. def _episode_diag(results: Dict[str, object]) -> Dict[str, object]:
  129. collision = results.get("collision")
  130. latency = results.get("latency", {})
  131. return {
  132. "safe_ok": bool(results.get("safe_ok")),
  133. "reach_ok": bool(results.get("reach_ok")),
  134. "formation_ok": bool(results.get("formation_ok")),
  135. "collision_obs": bool(results.get("cost_flags", {}).get("c3_any", False)),
  136. "collision_obs_hard": bool(results.get("cost_flags", {}).get("c3_hard_any", False)),
  137. "collision_nbr": bool(results.get("cost_flags", {}).get("c4_any", False)),
  138. "latency_step_mean_ms": float(latency.get("step_mean_ms", 0.0)),
  139. "latency_step_p95_ms": float(latency.get("step_p95_ms", 0.0)),
  140. "latency_step_overrun_ratio": float(latency.get("step_overrun_ratio", 0.0)),
  141. "failure_step": collision.get("step") if isinstance(collision, dict) else None,
  142. "failure_type": collision.get("type") if isinstance(collision, dict) else None,
  143. "min_clearance": results.get("min_clearance"),
  144. }
  145. def _finalize(stats: Dict[str, float]) -> Dict[str, float]:
  146. count = max(int(stats.get("count", 0)), 1)
  147. lat_step_count = max(int(stats.get("lat_step_count", 0)), 1)
  148. return {
  149. "count": int(stats.get("count", 0)),
  150. "collision_free_rate": float(stats.get("collision_free", 0) / count),
  151. "safe_ok_rate": float(stats.get("safe_ok", 0) / count),
  152. "reach_ok_rate": float(stats.get("reach_ok", 0) / count),
  153. "formation_ok_rate": float(stats.get("formation_ok", 0) / count),
  154. "formation_mean": float(stats.get("formation_mean", 0.0) / count),
  155. "formation_max": float(stats.get("formation_max", 0.0) / count),
  156. "collision_obs_rate": float(stats.get("collision_obs", 0) / count),
  157. "obstacle_margin_violation_rate": float(stats.get("collision_obs", 0) / count),
  158. "obstacle_hard_collision_rate": float(stats.get("collision_obs_hard", 0) / count),
  159. "collision_nbr_rate": float(stats.get("collision_nbr", 0) / count),
  160. "latency_budget_ms": float(stats.get("lat_budget_ms", 0.0)),
  161. "latency_step_mean_ms": float(stats.get("lat_step_mean_sum", 0.0) / count),
  162. "latency_step_std_ms": float(stats.get("lat_step_std_sum", 0.0) / count),
  163. "latency_step_p95_ms": float(stats.get("lat_step_p95_sum", 0.0) / count),
  164. "latency_decision_mean_ms": float(stats.get("lat_decision_mean_sum", 0.0) / count),
  165. "latency_decision_std_ms": float(stats.get("lat_decision_std_sum", 0.0) / count),
  166. "latency_decision_p95_ms": float(stats.get("lat_decision_p95_sum", 0.0) / count),
  167. "latency_step_overrun_ratio": float(stats.get("lat_step_overrun_count", 0) / lat_step_count),
  168. }
  169. def _build_final_summary(summary_raw: Dict[str, object], selected_methods: Tuple[str, ...]) -> Dict[str, object]:
  170. summary = copy.deepcopy(summary_raw)
  171. for scene_key, stats in summary["scene_stats"].items():
  172. for method in selected_methods:
  173. summary["scene_stats"][scene_key][method] = _finalize(stats[method])
  174. for bucket_key, stats in summary["bucket_stats"].items():
  175. for method in selected_methods:
  176. summary["bucket_stats"][bucket_key][method] = _finalize(stats[method])
  177. for method in selected_methods:
  178. summary["overall"][method] = _finalize(summary["overall"][method])
  179. return summary
  180. def _write_progress(
  181. progress_path: Path,
  182. summary_raw: Dict[str, object],
  183. rng: np.random.Generator,
  184. valid_eps: int,
  185. target_scene_counts: Dict[str, int],
  186. scene_ptr: int,
  187. ) -> None:
  188. payload = {
  189. "summary_raw": summary_raw,
  190. "rng_state": _to_jsonable(rng.bit_generator.state),
  191. "valid_eps": int(valid_eps),
  192. "target_scene_counts": {k: int(v) for k, v in target_scene_counts.items()},
  193. "scene_ptr": int(scene_ptr),
  194. }
  195. progress_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
  196. def _make_base_params() -> BaseSimParams:
  197. sp = BaseSimParams()
  198. sp.dt = 0.5
  199. sp.sim_time = 59.5
  200. setattr(sp, "safe_clearance_m", 0.5)
  201. setattr(sp, "safe_soft_band", 1.0)
  202. return sp
  203. def _make_modified_params() -> ModifiedSimParams:
  204. sp = ModifiedSimParams()
  205. setattr(sp, "safe_clearance_m", 0.5)
  206. return sp
  207. def main() -> None:
  208. parser = argparse.ArgumentParser()
  209. parser.add_argument("--model_dir", type=str, default="mlp_out")
  210. parser.add_argument("--num_episodes", type=int, default=50)
  211. parser.add_argument("--episodes_per_scene", type=int, default=50)
  212. parser.add_argument("--seed", type=int, default=2024)
  213. parser.add_argument("--out_path", type=str, default="results.json")
  214. parser.add_argument("--legacy_easy_only", action="store_true")
  215. parser.add_argument("--scene_dir", type=str, default="", help="use scene jsons from this dir instead of sample_scene")
  216. parser.add_argument("--methods", type=str, default="mlp,base_mpio,mpio", help="comma-separated subset of: mlp,base_mpio,mpio")
  217. parser.add_argument("--scene_types", type=str, default="", help="comma-separated scene types to run; default uses protocol scene set")
  218. parser.add_argument("--resume", action="store_true", help="resume from out_path.progress.json")
  219. parser.add_argument("--save_scene_dir", type=str, default="", help="save accepted eval scenes as ep_XXXXXX.json for later replay")
  220. args = parser.parse_args()
  221. selected_methods = tuple(_parse_csv_list(args.methods))
  222. if not selected_methods:
  223. raise SystemExit("no methods selected")
  224. invalid_methods = [m for m in selected_methods if m not in METHODS]
  225. if invalid_methods:
  226. raise SystemExit(f"unknown methods: {invalid_methods}; valid={METHODS}")
  227. requested_scene_types = tuple(_parse_csv_list(args.scene_types))
  228. invalid_scene_types = [s for s in requested_scene_types if s not in TARGET_SCENE_TYPES]
  229. if invalid_scene_types:
  230. raise SystemExit(f"unknown scene_types: {invalid_scene_types}; valid={TARGET_SCENE_TYPES}")
  231. model = None
  232. mean = std = None
  233. if "mlp" in selected_methods:
  234. model_dir = Path(args.model_dir)
  235. scaler = np.load(model_dir / "scaler.npz")
  236. mean = scaler["mean"]
  237. std = scaler["std"]
  238. ckpt = torch.load(model_dir / "model.pt", map_location="cpu")
  239. input_dim = int(ckpt.get("input_dim", mean.shape[0]))
  240. if mean.shape[0] != input_dim:
  241. raise SystemExit("scaler dim != model dim")
  242. model = torch.nn.Sequential(
  243. torch.nn.Linear(input_dim, 128),
  244. torch.nn.ReLU(),
  245. torch.nn.Linear(128, 64),
  246. torch.nn.ReLU(),
  247. torch.nn.Linear(64, 2),
  248. torch.nn.Sigmoid(),
  249. )
  250. state = ckpt["model_state"]
  251. if any(k.startswith("net.") for k in state.keys()):
  252. state = {k.replace("net.", "", 1): v for k, v in state.items()}
  253. model.load_state_dict(state, strict=True)
  254. model.eval()
  255. rng = np.random.default_rng(args.seed)
  256. def mlp_policy(i, P, V_xy, psi, lamb, obs_state, obstacles_plan, sp,
  257. ff_raw, fa_raw, fc_raw, vf_z, vo_raw, step_k, t):
  258. if model is None or mean is None or std is None:
  259. raise RuntimeError("mlp policy requested but model is not loaded")
  260. nbr_idx = neighbors_within(P, i, sp.R1_comm)
  261. neighbors = np.array([
  262. [P[j, 0], P[j, 1], V_xy[j, 0], V_xy[j, 1]] for j in nbr_idx
  263. ], dtype=float)
  264. obstacles = np.hstack([obstacles_plan, obs_state[:, 3:5]])
  265. feat = extract_features(
  266. pos_i=P[i, :],
  267. vel_i=V_xy[i, :],
  268. psi_i=psi[i],
  269. lamb_i=lamb[i],
  270. neighbors=neighbors,
  271. obstacles=obstacles,
  272. ve_xy=sp.ve_xy,
  273. )
  274. x = (feat - mean) / std
  275. with torch.no_grad():
  276. w = model(torch.from_numpy(x.astype(np.float32))).numpy()
  277. return np.clip(w.astype(float), 0.0, 1.0)
  278. balanced_mode = not args.scene_dir and not args.legacy_easy_only
  279. target_scene_types = list(requested_scene_types) if requested_scene_types else (
  280. list(TARGET_SCENE_TYPES) if balanced_mode else []
  281. )
  282. target_total_episodes = (
  283. len(target_scene_types) * args.episodes_per_scene
  284. if balanced_mode else args.num_episodes
  285. )
  286. summary = {
  287. "episodes": target_total_episodes,
  288. "valid_episodes": 0,
  289. "scene_sample_attempts": 0,
  290. "invalid_scene_skips": 0,
  291. "balanced_scene_eval": bool(balanced_mode),
  292. "episodes_per_scene": int(args.episodes_per_scene) if balanced_mode else None,
  293. "target_scene_types": target_scene_types,
  294. "selected_methods": list(selected_methods),
  295. "obstacle_metric_definition": "collision_obs_rate is evaluated on inflated boundary: dist_to_center < r_obs + safe_clearance_m",
  296. "latency_protocol": "step-level decision latency (sum over UAVs per sim step); overrun if latency > dt",
  297. "scene_stats": {},
  298. "bucket_stats": {},
  299. "overall": {method: {} for method in selected_methods},
  300. "episodes_detail": [],
  301. }
  302. scene_paths: List[Path] = []
  303. if args.scene_dir:
  304. scene_dir = Path(args.scene_dir)
  305. if not scene_dir.exists():
  306. raise SystemExit(f"scene_dir not found: {scene_dir}")
  307. scene_paths = sorted(scene_dir.glob("ep_*.json"))
  308. if requested_scene_types:
  309. scene_paths = [p for p in scene_paths if _load_scene_cfg(p).get("scene_type") in requested_scene_types]
  310. if not scene_paths:
  311. raise SystemExit(f"no scene jsons in {scene_dir} after scene_type filter")
  312. target_total_episodes = len(scene_paths) if args.num_episodes <= 0 else min(args.num_episodes, len(scene_paths))
  313. summary["episodes"] = target_total_episodes
  314. summary["balanced_scene_eval"] = False
  315. summary["episodes_per_scene"] = None
  316. summary["target_scene_types"] = list(requested_scene_types)
  317. sample_scene_fn = sample_scene_legacy_easy if args.legacy_easy_only else sample_scene
  318. target_scene_counts = {scene_type: 0 for scene_type in target_scene_types}
  319. valid_eps = 0
  320. scene_ptr = 0
  321. progress_path = _progress_path(Path(args.out_path))
  322. save_scene_dir = Path(args.save_scene_dir) if args.save_scene_dir else None
  323. if save_scene_dir is not None:
  324. save_scene_dir.mkdir(parents=True, exist_ok=True)
  325. if args.resume:
  326. if not progress_path.exists():
  327. raise SystemExit(f"resume requested but progress file not found: {progress_path}")
  328. payload = json.loads(progress_path.read_text(encoding="utf-8"))
  329. summary = payload["summary_raw"]
  330. valid_eps = int(payload.get("valid_eps", summary.get("valid_episodes", 0)))
  331. summary["valid_episodes"] = valid_eps
  332. target_scene_counts = {k: int(v) for k, v in payload.get("target_scene_counts", target_scene_counts).items()}
  333. scene_ptr = int(payload.get("scene_ptr", 0))
  334. rng.bit_generator.state = payload["rng_state"]
  335. print(f"[eval] resumed progress: {valid_eps}/{summary['episodes']} from {progress_path}", flush=True)
  336. max_attempts = max(target_total_episodes * 50, 1000)
  337. while valid_eps < target_total_episodes:
  338. summary["scene_sample_attempts"] += 1
  339. if summary["scene_sample_attempts"] > max_attempts:
  340. raise SystemExit(
  341. f"failed to collect {target_total_episodes} valid episodes after {summary['scene_sample_attempts']} attempts; "
  342. f"invalid_scene_skips={summary['invalid_scene_skips']}"
  343. )
  344. if scene_paths:
  345. if scene_ptr >= len(scene_paths):
  346. break
  347. scene_cfg = _load_scene_cfg(scene_paths[scene_ptr])
  348. scene_ptr += 1
  349. if scene_cfg.get("rng_seed") is None:
  350. scene_cfg["rng_seed"] = int(rng.integers(1, 1_000_000_000))
  351. elif balanced_mode:
  352. remaining_types = [
  353. scene_type for scene_type, count in target_scene_counts.items()
  354. if count < args.episodes_per_scene
  355. ]
  356. if not remaining_types:
  357. break
  358. scene_type = remaining_types[0]
  359. max_scene_tries = 500
  360. scene_cfg = {}
  361. for _ in range(max_scene_tries):
  362. scene_cfg = sample_scene(rng)
  363. if not scene_cfg:
  364. continue
  365. if scene_cfg.get("scene_type") != scene_type:
  366. continue
  367. break
  368. if not scene_cfg:
  369. raise SystemExit(f"failed to sample enough scenes for type={scene_type} after {max_scene_tries} tries")
  370. scene_cfg["rng_seed"] = int(rng.integers(1, 1_000_000_000))
  371. else:
  372. scene_cfg = sample_scene_fn(rng)
  373. if not scene_cfg or "scene_type" not in scene_cfg:
  374. summary["invalid_scene_skips"] += 1
  375. continue
  376. if requested_scene_types and scene_cfg["scene_type"] not in requested_scene_types:
  377. continue
  378. scene_cfg["rng_seed"] = int(rng.integers(1, 1_000_000_000))
  379. n_uav = int(scene_cfg.get("N_uav", 0))
  380. if not (N_RANGE[0] <= n_uav <= N_RANGE[1]):
  381. summary["invalid_scene_skips"] += 1
  382. continue
  383. scene_cfg["verbose_uav1"] = False
  384. scene_cfg["csv_path"] = ""
  385. results_by_method = {}
  386. if "mlp" in selected_methods:
  387. results_by_method["mlp"] = run_modified_episode(_make_modified_params(), scene_cfg, collect=False, policy_fn=mlp_policy)
  388. if "mpio" in selected_methods:
  389. results_by_method["mpio"] = run_modified_episode(_make_modified_params(), scene_cfg, collect=False, policy_fn=None)
  390. if "base_mpio" in selected_methods:
  391. results_by_method["base_mpio"] = run_base_episode(_make_base_params(), scene_cfg, collect=False)
  392. scene_key = scene_cfg["scene_type"]
  393. if balanced_mode and scene_key in target_scene_counts:
  394. if target_scene_counts[scene_key] >= args.episodes_per_scene:
  395. continue
  396. target_scene_counts[scene_key] += 1
  397. bucket_key = _n_bucket(scene_cfg["N_uav"])
  398. if save_scene_dir is not None:
  399. scene_path = save_scene_dir / f"ep_{valid_eps:06d}.json"
  400. _save_scene_json(scene_cfg, scene_path)
  401. summary["scene_stats"].setdefault(scene_key, _new_method_stats(selected_methods))
  402. summary["bucket_stats"].setdefault(bucket_key, _new_method_stats(selected_methods))
  403. ep_diag = {
  404. "scene_type": scene_key,
  405. "N_uav": int(scene_cfg["N_uav"]),
  406. "rng_seed": int(scene_cfg["rng_seed"]),
  407. }
  408. for method, result in results_by_method.items():
  409. ep_diag[method] = _episode_diag(result)
  410. _accumulate(summary["scene_stats"][scene_key][method], result, result["formation_metrics"])
  411. _accumulate(summary["bucket_stats"][bucket_key][method], result, result["formation_metrics"])
  412. _accumulate(summary["overall"][method], result, result["formation_metrics"])
  413. summary["episodes_detail"].append(ep_diag)
  414. valid_eps += 1
  415. summary["valid_episodes"] = valid_eps
  416. if balanced_mode:
  417. summary["scene_target_counts"] = {k: int(args.episodes_per_scene) for k in target_scene_types}
  418. summary["scene_actual_counts"] = {k: int(v) for k, v in target_scene_counts.items()}
  419. progress_scene = ""
  420. if balanced_mode and scene_key in target_scene_counts:
  421. progress_scene = f" [{scene_key} {target_scene_counts[scene_key]}/{args.episodes_per_scene}]"
  422. print(f"[eval] progress {valid_eps}/{target_total_episodes}{progress_scene}", flush=True)
  423. _write_progress(progress_path, summary, rng, valid_eps, target_scene_counts, scene_ptr)
  424. final_summary = _build_final_summary(summary, selected_methods)
  425. Path(args.out_path).write_text(json.dumps(final_summary, indent=2), encoding="utf-8")
  426. print(f"[eval] wrote {args.out_path}", flush=True)
  427. if __name__ == "__main__":
  428. main()

eval_closed_loop.py at commit 7f3212e, under MIT · at the source

Overview

Authors: Jinze Li1, Zeling Wen1, Zhaoke Ning1,2
  1. School of Aeronautics and Astronautics, Sichuan University, Chengdu 610207, China; (J.L.); (Z.W.)
  2. Multi-Source Information Intelligent Fusion Key Laboratory of Sichuan Province, Chengdu 610207, China
Institutions: Sichuan University (China)
Journal: Sensors (Basel, Switzerland), volume 26, issue 11, article 3398
Dates: received 17 April 2026; accepted 18 May 2026; published online 27 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3390/s26113398 · PMID 42280917 · PMCID PMC13259504 · OpenAlex W7162530465
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: cognitive (subfield)
Methods: Spectral & time-frequency, Machine learning
Keywords: UAV swarms, multi-objective optimization, neural surrogate, distributed control, real-time decision making
Topic: Distributed Control Multi-Agent Systems (Computer Networks and Communications, Computer Science), according to OpenAlex
Citations: not cited yet (Europe PMC); 33 references in the paper

Abstract

Real-time cooperative control of quadrotor swarms in cluttered environments requires balancing formation maintenance, obstacle avoidance, inter-UAV safety, and per-step computational cost. This paper proposes a multilayer perceptron (MLP) surrogate for high-level objective-weight selection in a modified multi-objective pigeon-inspired optimization (modified MPIO) distributed controller. The proposed MLP surrogate learns the state-to-weight mapping of the online search and directly predicts the two-dimensional objective-weight vector, while the original flocking, gap-based obstacle-avoidance, and command generation rules are retained unchanged. The surrogate is trained from teacher-generated weight labels using randomized scenes, DAgger-based state aggregation, and risk-weighted supervision. On a fixed closed-loop benchmark, the proposed controller increases the true collision free rate from 48.00% to 86.89% and the safe success rate from 38.67% to 74.22% relative to modified MPIO, while reducing the mean per-step decision latency for the whole swarm from 8494.70 ms to 0.92 ms. The improvement is most pronounced in safety-related and runtime metrics, while the formation-related gain is comparatively modest. Ablation results show that the final benchmark performance is not explained by DAgger or risk weighting alone, and that the medium-sized surrogate provides the best safety-latency tradeoff among the tested network architectures. A qualitative AirSim case study further indicates that the same high-level surrogate controller can be executed in a higher-fidelity asynchronous multirotor simulator.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 4 matches between paragraphs and lines of code.

cliche71/quadrotor-swarm-neural-surrogate

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 7f3212e243f2375c2b727b222a329a6325aad774, 19 April 2026
Languages: Python (8)
Size: 30 files, 8 scripts
Software Heritage: not archived
Found in: the text, “1. Introduction”
Holds: README, license file, environment (requirements-airsim.txt, requirements.txt)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (7 files), PyTorch (4 files), Matplotlib (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
10 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 8 scripts, each with its path and the digest of its content;
  • 4 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data Availability Statement

The datasets generated and analyzed during the current study are not publicly archived. They are available from the first author upon reasonable request. The source code supporting the implementation is available in the public repository cited in the manuscript.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 3 authors, 5 keywords, 30 references.

Cite

This paper

Li, J., Wen, Z., & Ning, Z. (2026). Neural Surrogate-Enhanced Metaheuristic Optimization for Distributed Quadrotor Swarm Control. Sensors (Basel, Switzerland), 26(11), 3398. https://doi.org/10.3390/s26113398

BibTeX

@article{li2026neural,
author = {Li, Jinze and Wen, Zeling and Ning, Zhaoke},
title = {{Neural Surrogate-Enhanced Metaheuristic Optimization for Distributed Quadrotor Swarm Control}},
journal = {Sensors (Basel, Switzerland)},
year = {2026},
month = may,
volume = {26},
number = {11},
pages = {3398},
publisher = {Multidisciplinary Digital Publishing Institute (MDPI)},
issn = {1424-8220},
doi = {10.3390/s26113398},
url = {https://doi.org/10.3390/s26113398},
pmid = {42280917},
pmcid = {PMC13259504}
}

RIS

TY - JOUR
AU - Li, Jinze
AU - Wen, Zeling
AU - Ning, Zhaoke
TI - Neural Surrogate-Enhanced Metaheuristic Optimization for Distributed Quadrotor Swarm Control
T2 - Sensors (Basel, Switzerland)
J2 - Sensors (Basel)
PY - 2026
DA - 2026/05/27
VL - 26
IS - 11
SP - 3398
SN - 1424-8220
PB - Multidisciplinary Digital Publishing Institute (MDPI)
DO - 10.3390/s26113398
UR - https://doi.org/10.3390/s26113398
LA - en
ER -

CSL-JSON

{
"id": "10.3390/s26113398",
"type": "article-journal",
"title": "Neural Surrogate-Enhanced Metaheuristic Optimization for Distributed Quadrotor Swarm Control",
"container-title": "Sensors (Basel, Switzerland)",
"author": [
{
"family": "Li",
"given": "Jinze"
},
{
"family": "Wen",
"given": "Zeling"
},
{
"family": "Ning",
"given": "Zhaoke"
}
],
"container-title-short": "Sensors (Basel)",
"volume": "26",
"issue": "11",
"page": "3398",
"DOI": "10.3390/s26113398",
"PMID": "42280917",
"PMCID": "PMC13259504",
"ISSN": "1424-8220",
"publisher": "Multidisciplinary Digital Publishing Institute (MDPI)",
"URL": "https://doi.org/10.3390/s26113398",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
27
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.3390/biomimetics11040288 [code]
BioLAMR: A Biomimetically Inspired Large Language Model Adaptation Framework for Automatic Modulation Recognition.
Journal: Biomimetics (Basel, Switzerland)
In common: PyTorch, Matplotlib, NumPy, 1 reference
[2] doi:10.3389/fneur.2026.1831912
Efficient EEG channel-and-frequency-band selection for epileptic seizure classification using multi-objective optimization.
Journal: Frontiers in neurology
In common: 1 reference

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.