mhsnar commited on
Commit
28deadc
·
verified ·
1 Parent(s): 0f27045

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. applications/multi_robot/README.md +32 -0
  2. applications/multi_robot/robotarium/README.md +114 -0
  3. applications/multi_robot/robotarium/batch.py +231 -0
  4. applications/multi_robot/robotarium/evaluate.py +208 -0
  5. applications/multi_robot/robotarium/figures.py +261 -0
  6. applications/multi_robot/robotarium/instances.py +185 -0
  7. applications/multi_robot/robotarium/projector.py +535 -0
  8. applications/multi_robot/robotarium/robotarium_baselines/.gitignore +6 -0
  9. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/.gitignore +43 -0
  10. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/LICENSE +28 -0
  11. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/README.md +209 -0
  12. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/analysis_config.yaml +63 -0
  13. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/analyze_mmd.yaml +59 -0
  14. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_conveyor.yaml +79 -0
  15. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_dropregion.yaml +79 -0
  16. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_empty.yaml +81 -0
  17. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_highway.yaml +80 -0
  18. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_conveyor.yaml +90 -0
  19. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_dropregion.yaml +76 -0
  20. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_empty.yaml +77 -0
  21. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_highway.yaml +76 -0
  22. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/LICENSE +21 -0
  23. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/MANIFEST.in +2 -0
  24. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/Makefile +14 -0
  25. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/README.md +111 -0
  26. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/basic/launch_test.py +75 -0
  27. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/basic/test.py +47 -0
  28. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/configs/config00.yaml +7 -0
  29. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/configs/config01.yaml +7 -0
  30. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/launch_test.py +65 -0
  31. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/test.py +49 -0
  32. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/multi_exp_same_gpu/launch_test.py +60 -0
  33. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/multi_exp_same_gpu/test.py +54 -0
  34. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/start_seed/launch_test.py +98 -0
  35. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/start_seed/test.py +47 -0
  36. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/__init__.py +8 -0
  37. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/decorators.py +49 -0
  38. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/exceptions.py +10 -0
  39. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/launcher.py +495 -0
  40. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/utils.py +117 -0
  41. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/requirements.txt +5 -0
  42. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/setup.py +35 -0
  43. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/LICENSE +24 -0
  44. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/README.md +54 -0
  45. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/__init__.py +0 -0
  46. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/__init__.py +0 -0
  47. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/base.py +226 -0
  48. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/chomp.py +169 -0
  49. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/costs/__init__.py +0 -0
  50. applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/costs/cost_functions.py +745 -0
applications/multi_robot/README.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-robot motion planning
2
+
3
+ Reproduces the multi-robot results of *Continuous-Time Constrained Motion Planning with Global
4
+ Convergence Guarantees*, in two experiments:
5
+
6
+ | folder | experiment | paper |
7
+ |---|---|---|
8
+ | [`robotarium/`](robotarium/README.md) | the Robotarium arena: 5 maps x N in {3,6,9,12}, single-plan success, the hardware runs; GlobMP behind a diffusion planner's projector | Sec. 4.2, App. `sec:steering` |
9
+ | [`multi_robot_extension/`](multi_robot_extension/README.md) | the simulated suite: 8 maps x N = 3..21 x 3 velocity caps, the eight suite tables and figures | App. `app:exp-multirobot` |
10
+
11
+ Each folder has the same layout and runs on its own:
12
+
13
+ ```
14
+ <experiment>/
15
+ *.py GlobMP's side of the experiment (instances, evaluate and figures are shared, identical copies)
16
+ run/ the entry scripts
17
+ data/ instances and the result files the paper's tables and figures read
18
+ *_baselines/ the baselines and the benchmark they run in: config/, data_prep/, train/, run/, eval/, tools/,
19
+ checkpoints/ and one folder per original repository (same layout as steering/xhand_baselines/)
20
+ ```
21
+
22
+ GlobMP itself is the `globmp` package at the repository root; nothing here changes it.
23
+
24
+ ## Environments
25
+
26
+ * GlobMP side: Python >= 3.10 with numpy, scipy, torch (CPU is enough), matplotlib, and the `globmp` package
27
+ on the path: `export PYTHONPATH=<repo root>` (or `pip install -e <repo root>`). The runs behind the paper
28
+ used one thread per job (`OMP_NUM_THREADS=1`, `run_grid.py --threads 1`) on pinned cores.
29
+ * Benchmark side (only for the benchmark's predicate, the instance export and the Robotarium sampler): the
30
+ co-authors' reproduction of the DiRecT/MMD multi-robot benchmark (`<experiment>_baselines/` in each folder) with its `direct` conda env. Two quirks:
31
+ `PYTHONPATH` must put `multi_robot/` and `multi_robot/deps/torch_robotics` first, and `PYTHONNOUSERSITE`
32
+ must not be set. `EnvEmpty2D` has no checkpoint; `EnvEmptyNoWait2D` is used.
applications/multi_robot/robotarium/README.md ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-robot: Robotarium arena (Sec. 4.2)
2
+
3
+ Single-plan success of GlobMP on the Robotarium arena (5 maps x N in {3,6,9,12}) and the baselines it is compared
4
+ with, plus GlobMP behind a diffusion planner's projector interface (App. `sec:steering`).
5
+
6
+ | paper item | what | produced by |
7
+ |---|---|---|
8
+ | Sec. 4.2, Fig. `robotarium_sr_one_compact.pdf` | single-plan success on the Robotarium arena, 5 maps x N in {3,6,9,12} | `run/run_cell.sh` (GlobMP as the sampler's projector), `run/score.py`, `figures.py robotarium` |
9
+ | Sec. 4.2, Fig. `robotarium_gallery.pdf` | frames of the real Robotarium runs | not reproducible from this package (see [Hardware gallery](#hardware-gallery)) |
10
+ | App. `sec:steering` (Steering a generative planner) | GlobMP behind a diffusion planner's projector interface | `projector.py`, `batch.py` (this is the configuration of the Robotarium runs) |
11
+
12
+ ## Files
13
+
14
+ ```
15
+ projector.py GlobMP as DiRecT's projector (BaseProjector.project): steering calls, final certified solve,
16
+ robot-by-robot certification with wall, timing and detour repairs
17
+ batch.py all candidate teams of one projector call as one batched problem (CPU or GPU)
18
+ instances.py map names, obstacle scales; load_robotarium; disc covers of boxes (shared, identical copy)
19
+ evaluate.py exact continuous-time robot-robot check and obstacle checks (shared, identical copy)
20
+ figures.py the multi-robot figures from result files (`figures.py robotarium`) (shared, identical copy)
21
+ run/
22
+ direct_hook.py the three-line wiring of projector.py into the benchmark's DiRecT fork
23
+ run_cell.sh one Robotarium cell: diffusion sampler + GlobMP projector, the paper's configuration
24
+ score.py select (first certified candidate per instance), bench (the comparison's predicate),
25
+ exact (offline re-check without the benchmark)
26
+ data/robotarium/ instances.npz (10 per cell), maps.json, geometry.json (boxes in metres)
27
+ data/results/ robotarium_cells.json (baselines), robotarium_gpa_sr_one.json, robotarium_gpa_sr_one_detail.json
28
+ robotarium_baselines/ the baselines (DiRecT, CD, Final Proj., PCD-SHD, PCD-LB, MMD-CBS) on this arena: the forks, data
29
+ preparation, training, runners, scorer, models, the N = 12 upload scripts (robotarium_baselines/README.md)
30
+ ```
31
+
32
+ `instances.py`, `evaluate.py` and `figures.py` are shared by both experiments; each folder keeps an identical copy,
33
+ so either folder runs on its own.
34
+
35
+ ## Environments
36
+
37
+ * GlobMP side: Python >= 3.10 with numpy, scipy, torch (CPU is enough), matplotlib, and the `globmp` package
38
+ on the path: `export PYTHONPATH=<repo root>` (or `pip install -e <repo root>`). The runs behind the paper
39
+ used one thread per job on pinned cores.
40
+ * Benchmark side (only for the benchmark's predicate, the instance export and the Robotarium sampler): the
41
+ co-authors' reproduction of the DiRecT/MMD multi-robot benchmark (in `robotarium_baselines/`) with its `direct` conda env. Two quirks:
42
+ `PYTHONPATH` must put `multi_robot/` and `multi_robot/deps/torch_robotics` first, and `PYTHONNOUSERSITE`
43
+ must not be set. `EnvEmpty2D` has no checkpoint; `EnvEmptyNoWait2D` is used.
44
+
45
+ The baselines' code is in `robotarium_baselines/` (`robotarium_baselines/README.md`). Their records
46
+ (`data/results/robotarium_cells.json`, a copy of `robotarium_baselines/eval/robotarium_cells.json`) were written by its
47
+ Robotarium scorer (`robotarium_baselines/eval/original/`; `robotarium_baselines/eval/score_cells.py` is the scorer for
48
+ new runs and reproduces them). The MMD-CBS cells are from the 2026-10-15 re-run with the isotropic normalizer
49
+ (`robotarium_baselines/eval/mmd_cbs_rerun.md`).
50
+
51
+ The trained single-robot models of the paper's setting (v2) are in `robotarium_baselines/checkpoints/<EnvClass>-RobotPlanarDisk/`
52
+ (`args.yaml` and the final EMA weights `checkpoints/ema_model_current_state_dict.pth`), byte-identical to the models
53
+ GlobMP's runs loaded. Conveyor's is the wide-channel model retrained for `RBT_WIDE=1`; the other four are shared with
54
+ the earlier v1 setting. Point the baselines' `MODELS_ROOT` / `--models-root`, and the `data_trained_models/` of the
55
+ fork behind `DIRECT_MR`, at this folder. The planners rebuild their normalizer from the training demonstrations at
56
+ start-up; those (about 4 GB) are not included and are regenerated by `robotarium_baselines/data_prep/generate_data.sh`.
57
+
58
+ GlobMP is scored with the same predicates: robot pairs at least 2r apart at the 64 waypoints and the
59
+ benchmark's `task.get_trajs_collision_and_free` at its default interpolation (k = 0).
60
+
61
+ ## Robotarium (Sec. 4.2)
62
+
63
+ The Robotarium numbers come from GlobMP used as the projector of the comparison's own diffusion sampler
64
+ (DiRecT with the single-robot models trained for the arena, r = 0.13 m, isotropic normalizer), on the
65
+ comparison's 10 instances per cell. At each of the 25 projection calls of an instance the sampler's batch of
66
+ 64 candidate teams is the reference of a tracking cost; intermediate calls steer (short restoration), the final
67
+ call solves every candidate team, and the returned plan is the first candidate GlobMP certifies (its
68
+ piecewise-linear motion is exact-checked against walls, arena, speed and 2r). If no candidate certifies with
69
+ seed 18, fresh diffusion batches are drawn (seeds 19, 20, ...) and the first certified plan is returned.
70
+
71
+ ```bash
72
+ export DIRECT_MR=$PWD/robotarium_baselines/DiRecT # the baselines' DiRecT fork, with run/direct_hook.py wired in
73
+ export DIRECT_PY=<conda>/envs/direct/bin/python OUT=$PWD/rbt_out
74
+ run/run_cell.sh crossroom 12 0 # map N gpu: 10 instances x 64 candidates
75
+ SEED=19 EXP_TAG=_s19i5 NINITS=1 INST_NPZ=<single-instance npz> run/run_cell.sh crossroom 12 0 # a retry
76
+ python run/score.py select --results $DIRECT_MR/results --certs $OUT --out ret
77
+ (cd $DIRECT_MR && <Robotarium env vars of run_cell.sh> $DIRECT_PY <here>/run/score.py bench crossroom 12 ret/returned_crossroom_N12.npz)
78
+ python run/score.py exact --returned ret --sr-json data/results/robotarium_gpa_sr_one.json
79
+ python figures.py robotarium --cells data/results/robotarium_cells.json --gpa data/results/robotarium_gpa_sr_one.json --out robotarium_sr_one_compact.pdf
80
+ ```
81
+
82
+ The cells were run on one A100 each with 40 CPU workers for the robot-by-robot certification; Conveyor N = 9
83
+ and N = 12 were split into five jobs of two instances (`score.py` SPLITS), and the retried instances are listed
84
+ in `score.py` RETRIES (Conveyor N = 12: instances 1, 3, 4, 5, 8, 9; Cross-Room N = 12: instance 5). Conveyor
85
+ N = 12 instances 3 and 4 never certified (seeds 18 to 24), which is the 0.8 in the figure. Wall time per cell
86
+ ranged from minutes (Empty, small N) to several hours (Conveyor N = 12). The comparison's predicate does not
87
+ require the goal to be reached exactly: the sampler pins only the start, and the returned plans end on average
88
+ 4 to 5 cm (at most 22 cm) from their goals; the same holds for every method in the comparison.
89
+
90
+ ### Hardware gallery
91
+
92
+ `robotarium_gallery.pdf` shows frames of the overhead videos of the real runs (12-robot ring instance,
93
+ Cross-Room and Conveyor) with tracked paths; it is drawn by the co-authors' `fig_robotarium_gallery.py` from the
94
+ videos and tracks, which are not part of this package. The GlobMP plans executed there ("best picks") were
95
+ produced on 2026-09-24 by the projector above, on the antipodal ring instances of an earlier benchmark
96
+ version, with a selection loop over steering settings (keep-right, circulation `GPA_CIRC_MODE=reflect`,
97
+ `GPA_SHIFT_SMOOTH`, intermediate budget) that kept the first certified plan passing a review (channel use, path
98
+ length at most 1.3 times the straight lines, a clean replay in the Robotarium simulator). The submission
99
+ scripts were generated by the co-authors' uploader. `projector.py` supports those settings; the loop and the
100
+ uploader are not included.
101
+
102
+ ## Validation of this port (2026-10-12)
103
+
104
+ Robotarium: `projector.py` + `batch.py` against the original projector, on identical inputs (Cross-Room N = 3
105
+ and N = 6, a steering call and a final call, including the robot-by-robot certification): outputs agree to
106
+ 3e-15 in iteration-limited runs (each code differs from its own repeat by 1e-14) and to 6e-4 (normalized units)
107
+ in full runs, with identical certified teams. The final-call certificate screen of `projector.py` on the saved
108
+ 64 x 10 candidates of Empty N = 3, Cross-Room N = 6 and Drop-Region N = 12 reproduces the original certificate
109
+ logs on 30 of 30 instances (625, 267 and 314 certified candidates; same first certified candidate).
110
+ `score.py exact` on the 20 saved returned-plan files gives the published single-plan success on all 20 cells
111
+ (Conveyor N = 12: 0.8, instances 3 and 4). An end-to-end rerun of the diffusion sampler was not done here (it
112
+ needs the benchmark's Robotarium fork and a GPU).
113
+
114
+ Figures from the files in `data/results/`: `figures.py robotarium` matches the paper's PDF.
applications/multi_robot/robotarium/batch.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """All candidate teams of one projector call solved as ONE batched problem with a leading team axis G.
2
+
3
+ Same algorithm as globmp.Problem in 'orthant' mode: Algorithm 1 (restoration, Barzilai-Borwein steps with a
4
+ nonmonotone backtracking reference) and a few blocks of Algorithm 2 (gradient projection), with per-team
5
+ masks instead of a Python loop over teams. The chart (N, pseudoinverse), the cost Hessian and the constraint
6
+ layout are shared by all teams; endpoints, tracking reference and step sizes are per team. Runs on CPU or GPU.
7
+
8
+ Constraint layout (the same for every team): pairwise separation 2r, a per-robot speed bound, the workspace
9
+ box (hard) and, per robot, every obstacle disc within `reach` of that robot's reference in ANY team.
10
+
11
+ Used by projector.py (the GlobMP projector behind the diffusion sampler of the Robotarium runs)."""
12
+ import math
13
+ import time
14
+
15
+ import numpy as np
16
+ import torch
17
+
18
+ from globmp import Problem, spline as bern
19
+ from globmp import problem as _problem
20
+
21
+ DT = torch.float64
22
+
23
+
24
+ class TeamBatch:
25
+ def __init__(self, W, n_agents, d, M, delta, robot_r, obstacles, ws_bounds, dx_max, De=30, reach=0.25,
26
+ device='cuda'):
27
+ """W: (G, L+1, n) references (sampler frame). obstacles: list of (center (2,), radius)."""
28
+ self.dev = torch.device(device)
29
+ G, L1, n = W.shape; L = L1 - 1
30
+ self.G, self.n, self.na, self.d, self.M, self.dl = G, n, n // 2, d, M, float(delta)
31
+ tk = dict(dtype=DT, device=self.dev)
32
+ # shared chart from one CPU problem (cached per (d, ell, ell_j, M) inside globmp)
33
+ q = Problem(n, d, 4, 0, M, W[0, 0], W[0, -1], ell_j=2, delta=delta, mode='orthant', De=De)
34
+ Api, N = _problem._CHART_CACHE[('chart', d, 0, 2, M)]
35
+ self.N = N.to(**tk); self.P = M * (d + 1)
36
+ self.NpT = torch.linalg.pinv(N).T.to(**tk) # for the initial fit
37
+ self.u = bern.ones_u(d).to(**tk); self.uu = torch.outer(self.u, self.u)
38
+ self.D1 = q.D1.to(**tk); self.E1 = q.E1.to(**tk); self.Eo = q.Eo.to(**tk)
39
+ C_, idx_ = bern._S_coeffs(d); self.SC = C_.to(**tk); self.Sidx = idx_.to(self.dev)
40
+ C1_, idx1_ = bern._S_coeffs(d - 1); self.SC1 = C1_.to(**tk); self.Sidx1 = idx1_.to(self.dev)
41
+ # per-team endpoints -> Gamma0 = start (x) Api[:,0] + goal (x) Api[:,1]
42
+ Wt = torch.as_tensor(W, **tk)
43
+ A0 = Api[0].to(**tk); A1 = Api[1].to(**tk)
44
+ self.Gamma0 = Wt[:, 0, :, None] * A0 + Wt[:, -1, :, None] * A1 # (G, n, P)
45
+ # tracking cost against the piecewise-linear reference (linear in W; exact Gauss-Legendre)
46
+ Tm, Qm = self._tracking_maps(L, M, d)
47
+ Gk = torch.block_diag(*[bern.cost_gram(d, 0)] * M).to(**tk)
48
+ self.H = self.N.T @ Gk @ self.N; self.H = 0.5 * (self.H + self.H.T)
49
+ Rd = torch.einsum('lc,gln->gnc', Tm.to(**tk), Wt) # (G, n, P)
50
+ cref = torch.einsum('lk,gln,gkn->g', Qm.to(**tk), Wt, Wt)
51
+ A = self.Gamma0
52
+ self.G0 = torch.einsum('gnp,pq,qf->gnf', A, Gk, self.N) - torch.einsum('gnp,pf->gnf', Rd * M, self.N)
53
+ self.c0 = torch.einsum('pq,gip,giq->g', Gk, A, A) - 2.0 * (Rd * M * A).sum((1, 2)) + cref * M
54
+ ev = torch.linalg.eigvalsh(self.H); self.beta = 2 * float(ev.max())
55
+ # constraint layout
56
+ na = self.na; r2 = 2.0 * robot_r
57
+ pairs = [(a, b) for a in range(na) for b in range(a + 1, na)]
58
+ self.sepI = torch.tensor([[2 * a, 2 * a + 1] for a, _ in pairs], device=self.dev)
59
+ self.sepJ = torch.tensor([[2 * b, 2 * b + 1] for _, b in pairs], device=self.dev)
60
+ self.sepR = r2
61
+ self.spdI = torch.tensor([[2 * a, 2 * a + 1] for a in range(na)], device=self.dev)
62
+ self.vmax = float(dx_max) * L / M
63
+ rows, cen, rad = [], [], []
64
+ if obstacles:
65
+ Cc = np.array([c for c, _ in obstacles], float); Rc = np.array([r for _, r in obstacles], float)
66
+ for a in range(na):
67
+ Pa = W[:, :, 2 * a:2 * a + 2].reshape(-1, 2)
68
+ dmin = np.linalg.norm(Pa[:, None] - Cc[None], axis=-1).min(0)
69
+ for k in np.where(dmin <= Rc + robot_r + delta + reach)[0]:
70
+ rows.append([2 * a, 2 * a + 1]); cen.append(Cc[k]); rad.append(Rc[k] + robot_r)
71
+ self.nsph = len(rows)
72
+ if rows:
73
+ self.sphI = torch.tensor(rows, device=self.dev); self.sphC = torch.as_tensor(np.array(cen), **tk)
74
+ self.sphR = torch.as_tensor(np.array(rad), **tk)
75
+ lo, hi = np.asarray(ws_bounds[0], float), np.asarray(ws_bounds[1], float)
76
+ self.lo = torch.as_tensor(np.tile(lo + robot_r - delta + 1e-6, na), **tk)
77
+ self.hi = torch.as_tensor(np.tile(hi - robot_r + delta - 1e-6, na), **tk)
78
+ # tolerances per block, in pi_all order
79
+ dl = self.dl; tv = []
80
+ if self.nsph: tv.append(((2 * self.sphR * dl + dl * dl) ** 2)[:, None].expand(-1, M).reshape(-1))
81
+ tv.append(torch.full((len(pairs) * M,), (2 * r2 * dl + dl * dl) ** 2, **tk))
82
+ tv.append(torch.full((na * M,), 1e-12, **tk)) # speed
83
+ tv.append(torch.full((2 * M * n,), 1e-12, **tk)) # box: hard
84
+ self.tv = torch.cat(tv)
85
+ # initial chart coordinates: least squares to the M+1 waypoints, linear inside each segment
86
+ wp = Wt[:, np.round(np.linspace(0, L, M + 1)).astype(int), :] # (G, M+1, n)
87
+ ss = torch.linspace(0, 1, d + 1, **tk)
88
+ tgt = wp[:, :-1, :, None] * (1 - ss) + wp[:, 1:, :, None] * ss # (G, M, n, d+1)
89
+ Gm = tgt.permute(0, 2, 1, 3).reshape(G, n, -1)
90
+ self.Z0 = (Gm - self.Gamma0) @ self.NpT # (G, n, d_f)
91
+
92
+ @staticmethod
93
+ def _tracking_maps(L, M, d):
94
+ """Rd(W) = sum_l Tm[l,c] W[l,:] and cref(W) = W^T Qm W, both exact (piecewise-linear reference)."""
95
+ nq = int(math.ceil((d + 2) / 2)); xg, wg = np.polynomial.legendre.leggauss(nq)
96
+ xg = 0.5 * (xg + 1.0); wg = 0.5 * wg
97
+ Tm = np.zeros((L + 1, M * (d + 1))); Qm = np.zeros((L + 1, L + 1))
98
+ tb = np.linspace(0, 1, L + 1)
99
+ for a in range(M):
100
+ t0, t1 = a / M, (a + 1) / M
101
+ cuts = np.unique(np.r_[t0, t1, tb[(tb > t0) & (tb < t1)]])
102
+ for c0, c1 in zip(cuts[:-1], cuts[1:]):
103
+ h = c1 - c0; tq = c0 + h * xg; uq = (tq - t0) / (t1 - t0)
104
+ bu = bern.bernstein_basis_eval(d, torch.as_tensor(uq, dtype=DT)).numpy() # (d+1, nq)
105
+ i = np.clip(np.floor(tq * L).astype(int), 0, L - 1); f = tq * L - i
106
+ for qq in range(nq):
107
+ w_ = wg[qq] * h
108
+ for (li, cf) in ((i[qq], 1 - f[qq]), (i[qq] + 1, f[qq])):
109
+ Tm[li, a * (d + 1):(a + 1) * (d + 1)] += w_ * cf * bu[:, qq]
110
+ Qm[i[qq], i[qq]] += w_ * (1 - f[qq]) ** 2; Qm[i[qq] + 1, i[qq] + 1] += w_ * f[qq] ** 2
111
+ Qm[i[qq], i[qq] + 1] += w_ * (1 - f[qq]) * f[qq]; Qm[i[qq] + 1, i[qq]] += w_ * (1 - f[qq]) * f[qq]
112
+ return torch.as_tensor(Tm, dtype=DT), torch.as_tensor(Qm, dtype=DT)
113
+
114
+ # ---- blocks
115
+ def _S(self, Q, d1=False):
116
+ C, idx = (self.SC1, self.Sidx1) if d1 else (self.SC, self.Sidx)
117
+ W = (Q * C).reshape(*Q.shape[:-2], -1); dd = (self.d - 1) if d1 else self.d
118
+ out = torch.zeros(*Q.shape[:-2], 2 * dd + 1, dtype=DT, device=self.dev)
119
+ out.index_add_(-1, idx, W)
120
+ return out
121
+
122
+ def _elev(self, c):
123
+ Mx = 0.5 * (c[..., :, None] * self.u + self.u[:, None] * c[..., None, :])
124
+ return self._S(Mx)
125
+
126
+ def segs(self, Z):
127
+ Gm = self.Gamma0 + Z @ self.N.T # (G, n, P)
128
+ return Gm.view(self.G, self.n, self.M, self.d + 1).permute(0, 2, 1, 3) # (G, M, n, d+1)
129
+
130
+ def pi_all(self, Z):
131
+ S = self.segs(Z); dl = self.dl; out = []
132
+ if self.nsph:
133
+ Gc = S[:, :, self.sphI, :] - self.sphC[None, None, :, :, None] * self.u # (G,M,K,2,d+1)
134
+ Mm = torch.einsum('gmkip,gmkiq->gmkpq', Gc, Gc) - ((self.sphR + dl) ** 2)[None, None, :, None, None] * self.uu
135
+ out.append(self._S(Mm).permute(0, 2, 1, 3).reshape(self.G, -1, 2 * self.d + 1))
136
+ Gc = S[:, :, self.sepI, :] - S[:, :, self.sepJ, :] # (G,M,Pp,2,d+1)
137
+ Mm = torch.einsum('gmkip,gmkiq->gmkpq', Gc, Gc) - (self.sepR + dl) ** 2 * self.uu
138
+ out.append(self._S(Mm).permute(0, 2, 1, 3).reshape(self.G, -1, 2 * self.d + 1))
139
+ Gd = S[:, :, self.spdI, :] @ self.D1 # (G,M,na,2,d)
140
+ Mm = torch.einsum('gmkip,gmkiq->gmkpq', Gd, Gd)
141
+ pe = self._S(Mm, d1=True) @ self.E1.T # (G,M,na,2d+1)
142
+ out.append((self.vmax ** 2 - pe).permute(0, 2, 1, 3).reshape(self.G, -1, 2 * self.d + 1))
143
+ lo = self.lo + dl; hi = self.hi - dl
144
+ pl = self._elev(S - lo[None, None, :, None] * self.u).reshape(self.G, -1, 2 * self.d + 1)
145
+ ph = self._elev(hi[None, None, :, None] * self.u - S).reshape(self.G, -1, 2 * self.d + 1)
146
+ out += [pl, ph]
147
+ return torch.cat(out, 1) # (G, B, 2d+1)
148
+
149
+ def per_block(self, Z):
150
+ pe = self.pi_all(Z) @ self.Eo.T
151
+ return (torch.clamp(-pe, min=0) ** 2).sum(-1) # (G, B)
152
+
153
+ def cost(self, Z):
154
+ return self.c0 + 2 * (Z * self.G0).sum((1, 2)) + torch.einsum('pq,gip,giq->g', self.H, Z, Z)
155
+
156
+ # ---- Algorithm 1, batched
157
+ def project(self, Z, cap=3000, deadline=None, active=None):
158
+ G = self.G; tv = self.tv
159
+ eta = torch.full((G,), 1e-3, dtype=DT, device=self.dev)
160
+ Zp = None; gp = None; hist = []
161
+ live = torch.ones(G, dtype=torch.bool, device=self.dev) if active is None else active.clone()
162
+ ok = torch.zeros(G, dtype=torch.bool, device=self.dev)
163
+ for it in range(cap):
164
+ with torch.no_grad():
165
+ d2 = self.per_block(Z); act = d2 > tv
166
+ done = ~act.any(1); ok |= done & live; live &= ~done
167
+ if not bool(live.any()) or (deadline and time.time() > deadline):
168
+ break
169
+ Zg = Z.detach().requires_grad_(True)
170
+ with torch.enable_grad():
171
+ per = self.per_block(Zg); f = (act.to(DT) * per).sum(1)
172
+ (g,) = torch.autograd.grad(f.sum(), [Zg])
173
+ f0 = f.detach()
174
+ if Zp is not None:
175
+ s_ = (Z - Zp).reshape(G, -1); y_ = (g - gp).reshape(G, -1); sy = (s_ * y_).sum(1)
176
+ eta = torch.where(sy > 1e-300, (s_ * s_).sum(1) / sy.clamp_min(1e-300),
177
+ torch.minimum(eta * 1.3, 2 * f0 / (g * g).sum((1, 2)).clamp_min(1e-300)))
178
+ else:
179
+ eta = torch.minimum(eta, 2 * f0 / (g * g).sum((1, 2)).clamp_min(1e-300))
180
+ hist.append(f0); fref = torch.stack(hist[-10:]).max(0).values if Zp is not None else f0 * (1 + 1e-12) + 1e-300
181
+ Zt = Z; acc = ~live
182
+ for _ in range(40):
183
+ cand = (Z - eta[:, None, None] * g).detach()
184
+ with torch.no_grad():
185
+ ft = (act.to(DT) * self.per_block(cand)).sum(1)
186
+ good = (ft < fref) & ~acc
187
+ Zt = torch.where(good[:, None, None], cand, Zt); acc |= good
188
+ if bool(acc.all()): break
189
+ eta = torch.where(acc, eta, eta * 0.5)
190
+ Zp = torch.where(live[:, None, None], Z, Zp if Zp is not None else Z)
191
+ gp = torch.where(live[:, None, None], g, gp if gp is not None else g)
192
+ Z = torch.where(live[:, None, None], Zt, Z)
193
+ with torch.no_grad():
194
+ ok |= ~(self.per_block(Z) > tv).any(1)
195
+ return Z.detach(), ok
196
+
197
+ # ---- Algorithm 2, batched (a few blocks)
198
+ def solve(self, Z, ok, blocks=4, steps=3, deadline=None):
199
+ Zp = Z.clone(); tc = torch.zeros(self.G, dtype=DT, device=self.dev)
200
+ for _ in range(blocks):
201
+ if deadline and time.time() > deadline: break
202
+ tc = tc + 1
203
+ y = Z + ((tc - 1) / (tc + 2))[:, None, None] * (Z - Zp)
204
+ for _ in range(steps):
205
+ y = (y - (1 / self.beta) * 2 * (self.G0 + y @ self.H)).detach()
206
+ cand, okc = self.project(y, cap=600, deadline=deadline, active=ok)
207
+ better = ok & okc & (self.cost(cand) <= self.cost(Z))
208
+ Zp = torch.where(better[:, None, None], Z, Zp); Z = torch.where(better[:, None, None], cand, Z)
209
+ tc = torch.where(better, tc, torch.zeros_like(tc))
210
+ return Z
211
+
212
+ def curve(self, Z, L):
213
+ tt = torch.linspace(0, 1, L + 1, dtype=DT, device=self.dev)
214
+ seg = torch.clamp((tt * self.M).floor().long(), 0, self.M - 1)
215
+ bu = bern.bernstein_basis_eval(self.d, (tt * self.M - seg).cpu()).to(device=self.dev, dtype=DT)
216
+ S = self.segs(Z) # (G, M, n, d+1)
217
+ return torch.einsum('ginp,pi->gin', S[:, seg], bu) # (G, L+1, n)
218
+
219
+
220
+ def solve_teams(W, n_agents, robot_r, obstacles, ws_bounds, dx_max, d=5, M=21, delta=0.005, budget=20.0,
221
+ cap=3000, alg2_blocks=4, partial=False, device='cuda'):
222
+ """W: (G, L+1, n) numpy. Returns ((G, L+1, n) numpy, ok (G,) bool). With partial=True, teams that did not
223
+ restore return their partially restored curve (intermediate steering calls)."""
224
+ L = W.shape[1] - 1; M = min(M, L)
225
+ tb = TeamBatch(W, n_agents, d, M, delta, robot_r, obstacles, ws_bounds, dx_max, device=device)
226
+ dl = time.time() + budget
227
+ Z, ok = tb.project(tb.Z0, cap=cap, deadline=dl)
228
+ if alg2_blocks > 0 and bool(ok.any()):
229
+ Z = tb.solve(Z, ok, blocks=alg2_blocks, deadline=dl)
230
+ out = tb.curve(Z, L).cpu().numpy()
231
+ return out, ok.cpu().numpy()
applications/multi_robot/robotarium/evaluate.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Scoring of waypoint plans.
2
+
3
+ A plan is executed (and scored by the benchmark) as the straight lines between its waypoints. Three checks:
4
+
5
+ * robot-robot, exact in continuous time: for two robots moving linearly over one interval the minimum
6
+ distance has a closed form (pair_min_dist_exact), so the check is over every t, not a dense sample.
7
+ * obstacles with the BENCHMARK'S OWN predicate, `task.get_trajs_collision_and_free(num_interpolation=k)`;
8
+ the tables use k = 0 (the 64 waypoints only, the benchmark's default). Needs the benchmark code.
9
+ * obstacles and arena, exact for discs and axis-aligned boxes (exact_obstacle_clearance), with no benchmark
10
+ code; used to re-check the Robotarium plans offline.
11
+
12
+ The paper's success numbers use the benchmark's own definitions (suite_metrics, robotarium/run/score.py);
13
+ nothing here re-defines success.
14
+
15
+ python evaluate.py suite <map> --results-dir DIR --trained-models DIR # benchmark env: SR CS Coll DA Time
16
+ python evaluate.py robotarium <map> <N> <plan.npz> [--inst K] # any env: exact offline check
17
+ """
18
+ import argparse
19
+ import glob
20
+ import json
21
+ import os
22
+ import sys
23
+
24
+ import numpy as np
25
+
26
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
+
28
+
29
+ # ---- robot-robot, exact
30
+ def pair_min_dist_exact(A, B):
31
+ """A, B: (..., H, 2) waypoints. Exact minimum distance over the piecewise-linear motion."""
32
+ P = A[..., :-1, :] - B[..., :-1, :]
33
+ D = (A[..., 1:, :] - A[..., :-1, :]) - (B[..., 1:, :] - B[..., :-1, :])
34
+ dd = (D * D).sum(-1)
35
+ s = np.clip(np.where(dd > 1e-18, -(P * D).sum(-1) / np.maximum(dd, 1e-18), 0.0), 0.0, 1.0)
36
+ Q = P + s[..., None] * D
37
+ return np.sqrt((Q * Q).sum(-1)).min(-1)
38
+
39
+
40
+ def eval_trajs(pos, radius):
41
+ """pos: (S, I, A, H, 2). Per-configuration robot-robot success at the waypoints and in continuous time."""
42
+ S, I, A, H, _ = pos.shape
43
+ need = 2.0 * radius
44
+ wp_ok = np.ones((S, I), bool); ct_ok = np.ones((S, I), bool)
45
+ wp_min = np.full((S, I), np.inf); ct_min = np.full((S, I), np.inf)
46
+ for a in range(A):
47
+ for b in range(a + 1, A):
48
+ Pa, Pb = pos[:, :, a], pos[:, :, b]
49
+ dw = np.linalg.norm(Pa - Pb, axis=-1).min(-1)
50
+ dc = pair_min_dist_exact(Pa, Pb)
51
+ wp_min = np.minimum(wp_min, dw); ct_min = np.minimum(ct_min, dc)
52
+ wp_ok &= dw >= need; ct_ok &= dc >= need
53
+ return dict(wp_success=float(wp_ok.any(0).mean()), ct_success=float(ct_ok.any(0).mean()),
54
+ wp_sample_rate=float(wp_ok.mean()), ct_sample_rate=float(ct_ok.mean()),
55
+ hidden_collision_rate=float((wp_ok & ~ct_ok).sum() / max(1, wp_ok.sum())),
56
+ wp_min=float(wp_min.min()), ct_min=float(ct_min.min()), need=float(need),
57
+ ct_ok=ct_ok, n_samples=S, n_inits=I, n_agents=A, horizon=H)
58
+
59
+
60
+ # ---- obstacles, exact for discs and boxes
61
+ def _seg_box_dist(P0, P1, c, h, iters=100):
62
+ """Distance from each segment P0[k]->P1[k] to the box |x - c| <= h (componentwise). The distance to a
63
+ convex set along a segment is convex in the segment parameter, so ternary search converges to the exact
64
+ minimum ((2/3)^100 of the segment after 100 iterations)."""
65
+ def dist(s):
66
+ X = P0 + s[:, None] * (P1 - P0)
67
+ q = np.maximum(np.abs(X - c) - h, 0.0)
68
+ return np.sqrt((q * q).sum(-1))
69
+ lo = np.zeros(len(P0)); hi = np.ones(len(P0))
70
+ for _ in range(iters):
71
+ m1 = lo + (hi - lo) / 3; m2 = hi - (hi - lo) / 3
72
+ left = dist(m1) <= dist(m2)
73
+ hi = np.where(left, m2, hi); lo = np.where(left, lo, m1)
74
+ return np.minimum.reduce([dist(lo), dist(hi), dist(np.zeros(len(P0))), dist(np.ones(len(P0)))])
75
+
76
+
77
+ def exact_obstacle_clearance(path, discs=(), boxes=(), ws=None):
78
+ """path: (H, 2) waypoints of one robot. Minimum centre-to-obstacle distance of the piecewise-linear
79
+ motion (discs: distance minus radius; boxes: distance to the box), and the minimum distance to the arena
80
+ edges. Returns (obstacle clearance, arena clearance)."""
81
+ P0, P1 = path[:-1], path[1:]
82
+ best = np.inf
83
+ for c, r in discs:
84
+ D = P1 - P0; w = np.asarray(c, float) - P0
85
+ t = np.clip((w * D).sum(-1) / np.maximum((D * D).sum(-1), 1e-18), 0, 1)
86
+ best = min(best, float((np.linalg.norm(w - t[:, None] * D, axis=-1) - r).min()))
87
+ for c, h in boxes:
88
+ best = min(best, float(_seg_box_dist(P0, P1, np.asarray(c, float), np.asarray(h, float)).min()))
89
+ edge = np.inf
90
+ if ws is not None:
91
+ edge = float(np.minimum(path - ws[0], ws[1] - path).min()) # a segment stays inside iff its ends do
92
+ return best, edge
93
+
94
+
95
+ def robotarium_exact(plans, mp, N, inst=None, data_dir=None):
96
+ """Exact offline check of returned Robotarium plans (I, N, H, 2), metres: robot-robot separation 2r over
97
+ every segment, robot body (r = 0.13) clear of every box, inside the arena. Returns per-instance dict."""
98
+ from instances import load_robotarium
99
+ G = load_robotarium(mp, N, data_dir)
100
+ r = G['robot_r']; boxes = list(zip(G['box_c'], G['box_h']))
101
+ T = np.asarray(plans, float)
102
+ out = []
103
+ for i in range(T.shape[0]):
104
+ rr = min([float(pair_min_dist_exact(T[i, a], T[i, b])) for a in range(N) for b in range(a + 1, N)] or [np.inf])
105
+ oc, ed = np.inf, np.inf
106
+ for a in range(N):
107
+ o, e = exact_obstacle_clearance(T[i, a], boxes=boxes, ws=G['ws'])
108
+ oc = min(oc, o); ed = min(ed, e)
109
+ k = i if inst is None else inst
110
+ ge = float(np.linalg.norm(T[i, :, -1] - G['goals'][k], axis=-1).max())
111
+ se = float(np.linalg.norm(T[i, :, 0] - G['starts'][k], axis=-1).max())
112
+ out.append(dict(inst=k, rr_min=rr, obs_clear=oc - r, arena_clear=ed - r, goal_err=ge, start_err=se,
113
+ free=bool(rr >= 2 * r and oc >= r and ed >= r)))
114
+ return out
115
+
116
+
117
+ # ---- the benchmark's own predicate (benchmark environment only)
118
+ def build_task(env_model_id, trained_models_dir, device='cpu', extra_objects=False):
119
+ import torch
120
+ from mmd.utils.loading import load_params_from_yaml
121
+ from mmd.trainer import get_dataset
122
+ args = load_params_from_yaml(os.path.join(trained_models_dir, env_model_id, 'args.yaml'))
123
+ trainset, _, _, _ = get_dataset(dataset_class='TrajectoryDataset', use_extra_objects=extra_objects,
124
+ obstacle_cutoff_margin=0.05, **args,
125
+ tensor_args=dict(device=device, dtype=torch.float32))
126
+ return trainset.dataset.task
127
+
128
+
129
+ def obstacle_rates(task, pos, k, device='cpu'):
130
+ """pos: (S, I, A, H, 2) -> per-(sample, configuration) mask of obstacle-free plans at interpolation k."""
131
+ import torch
132
+ S, I, A, H, _ = pos.shape
133
+ T = torch.from_numpy(np.ascontiguousarray(pos.reshape(S * I * A, H, 2))).to(device=device, dtype=torch.float32)
134
+ _, _, _, _, wpc = task.get_trajs_collision_and_free(T, return_indices=True, num_interpolation=int(k))
135
+ m = wpc.cpu().numpy().reshape(S, I, A, -1)
136
+ return ~m.any(-1).any(-1)
137
+
138
+
139
+ def suite_metrics(mp, results_dir, trained_models):
140
+ """The five table metrics of every saved cell of one map, with the benchmark's definitions: SR (some
141
+ candidate collision-free; here the one plan), CS (fraction of candidates), Coll (mean colliding robot pairs
142
+ at the waypoints), DA (the environment's data adherence), Time (median wall seconds per configuration).
143
+ Collision-free = robot pairs >= 2r at every waypoint and the benchmark's obstacle predicate at k = 0."""
144
+ import torch
145
+ from instances import OBS_SCALE, ENV_CLASS, suite_robot_radius
146
+ os.environ['OBS_SCALE'] = str(OBS_SCALE[mp])
147
+ TA = dict(device='cpu', dtype=torch.float32)
148
+ task = build_task(ENV_CLASS[mp] + '-RobotPlanarDisk', trained_models)
149
+ import torch_robotics.environments as _env_module
150
+ envobj = getattr(_env_module, ENV_CLASS[mp])(tensor_args=TA)
151
+ rob = suite_robot_radius(mp)
152
+ rows = []
153
+ for f in sorted(glob.glob(os.path.join(results_dir, f'{mp}__N*__*.npz'))):
154
+ D = np.load(f, allow_pickle=True); P = np.asarray(D['paths'], float)
155
+ B, I, A, H, _ = P.shape; pos = P[..., :2]
156
+ if A > 1:
157
+ dd = np.linalg.norm(pos[:, :, :, None, :, :] - pos[:, :, None, :, :, :], axis=-1)
158
+ iu = np.triu_indices(A, 1)
159
+ pairhit = (dd[:, :, iu[0], iu[1], :] < 2 * rob).any(-1)
160
+ coll = pairhit.sum(-1).astype(float); pair_ok = ~pairhit.any(-1)
161
+ else:
162
+ coll = np.zeros((B, I)); pair_ok = np.ones((B, I), bool)
163
+ feas = pair_ok & obstacle_rates(task, pos, 0)
164
+ da = envobj.compute_traj_data_adherence_batch(
165
+ torch.from_numpy(pos.reshape(-1, H, 2)).to(**TA)).cpu().numpy().reshape(B, I, A)
166
+ n = os.path.basename(f)[:-4].split('__')
167
+ r = dict(map=mp, N=A, regime=n[2], B=int(B), I=int(I),
168
+ SR=100 * float(feas.any(0).mean()), CS=100 * float(feas.mean()), CS_sd=100 * float(feas.mean(0).std()),
169
+ Coll=float(coll.mean()), Coll_sd=float(coll.std()), DA=float(da.mean()), DA_sd=float(da.std()),
170
+ Time=float(np.median(D['secs'])), Time_sd=float(np.std(D['secs'])))
171
+ rows.append(r); print(json.dumps(r), flush=True)
172
+ return rows
173
+
174
+
175
+ def cells_json(metrics_jsonl, results_dir, out_json):
176
+ """figures/gpa_cells.json records (fractions, median and std of wall time) from suite_metrics output."""
177
+ recs = []
178
+ for l in open(metrics_jsonl):
179
+ if not l.startswith('{'): continue
180
+ r = json.loads(l)
181
+ z = np.load(os.path.join(results_dir, f"{r['map']}__N{r['N']}__{r['regime']}.npz")); sec = np.asarray(z['secs'], float)
182
+ recs.append(dict(method='gpa', map=r['map'], N=r['N'], regime=r['regime'], src=os.path.basename(os.path.normpath(results_dir)),
183
+ SR=r['SR'] / 100.0, CS=r['CS'] / 100.0, CS_sd=r['CS_sd'] / 100.0, Coll=r['Coll'], Coll_sd=r['Coll_sd'],
184
+ DA=r['DA'], DA_sd=r['DA_sd'], T=float(np.median(sec)), T_sd=float(sec.std())))
185
+ json.dump(recs, open(out_json, 'w'), indent=0)
186
+ print(f'wrote {len(recs)} records to {out_json}')
187
+
188
+
189
+ if __name__ == '__main__':
190
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
191
+ sub = ap.add_subparsers(dest='cmd', required=True)
192
+ s = sub.add_parser('suite'); s.add_argument('map'); s.add_argument('--results-dir', required=True)
193
+ s.add_argument('--trained-models', required=True, help="the benchmark's data_trained_models directory")
194
+ c = sub.add_parser('cells'); c.add_argument('metrics_jsonl'); c.add_argument('--results-dir', required=True)
195
+ c.add_argument('--out', required=True)
196
+ r = sub.add_parser('robotarium'); r.add_argument('map'); r.add_argument('N', type=int); r.add_argument('plan')
197
+ r.add_argument('--inst', type=int, default=None, help='the plan file holds one instance, benchmark index K')
198
+ a = ap.parse_args()
199
+ if a.cmd == 'suite':
200
+ suite_metrics(a.map, a.results_dir, a.trained_models)
201
+ elif a.cmd == 'cells':
202
+ cells_json(a.metrics_jsonl, a.results_dir, a.out)
203
+ else:
204
+ T = np.load(a.plan)['trajs']
205
+ res = robotarium_exact(T, a.map, a.N, a.inst)
206
+ for x in res: print(json.dumps(x))
207
+ print(json.dumps(dict(map=a.map, N=a.N, SR_exact=float(np.mean([x['free'] for x in res])),
208
+ failed=[x['inst'] for x in res if not x['free']])))
applications/multi_robot/robotarium/figures.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Multi-robot figures of the paper, from result files only (no solving).
3
+
4
+ # Sec. 4.2, figures/robotarium_sr_one_compact.pdf: single-plan success on the Robotarium arena
5
+ python figures.py robotarium --cells data/results/robotarium_cells.json --gpa data/results/robotarium_gpa_sr_one.json --out robotarium_sr_one_compact.pdf
6
+ # appendix, figures/hardsuite_single_plan{,_compact}.pdf and hardsuite_best64.pdf
7
+ python figures.py suite single --baselines data/results/baseline_cells.json --gpa data/results/gpa_cells.json --out hardsuite_single_plan.pdf
8
+ python figures.py suite compact ... --out hardsuite_single_plan_compact.pdf
9
+ python figures.py suite best64 ... --out hardsuite_best64.pdf
10
+ # appendix, figures/hardsuite_velocity.pdf, read back from the generated tables
11
+ python figures.py velocity --tables hardsuite_tables.tex --out hardsuite_velocity.pdf [--n 9]
12
+
13
+ GlobMP returns one trajectory per instance; for a sampling baseline, single-plan success is that of its first
14
+ candidate (SR_at['1'] in the baseline records), best-of-64 is the tables' SR. Every figure is written as .pdf
15
+ and .png.
16
+ """
17
+ import argparse
18
+ import glob as _glob
19
+ import json
20
+ import re
21
+
22
+ import matplotlib
23
+ matplotlib.use('Agg')
24
+ import matplotlib.pyplot as plt
25
+ import numpy as np
26
+ from matplotlib import font_manager as _fm
27
+
28
+ # ---- print style of the paper (true ICLR text width, Times, thin axes; GlobMP in blue)
29
+ for _f in _glob.glob('/usr/share/fonts/truetype/msttcorefonts/[Tt]imes*.ttf'):
30
+ _fm.fontManager.addfont(_f)
31
+ TEXTWIDTH = 5.5
32
+ BLUE = '#2a78d6'
33
+ INK, INK2, GRID = '#222222', '#5a5a5a', '#e9e9e6'
34
+ plt.rcParams.update({
35
+ 'font.family': 'serif', 'font.serif': ['Times New Roman', 'Nimbus Roman', 'STIXGeneral'], 'mathtext.fontset': 'stix',
36
+ 'font.size': 8, 'axes.labelsize': 8, 'axes.titlesize': 8, 'xtick.labelsize': 7, 'ytick.labelsize': 7, 'legend.fontsize': 7,
37
+ 'axes.linewidth': 0.6, 'axes.edgecolor': INK2, 'axes.labelcolor': INK, 'xtick.color': INK2, 'ytick.color': INK2,
38
+ 'xtick.major.width': 0.6, 'ytick.major.width': 0.6, 'xtick.major.size': 2.5, 'ytick.major.size': 2.5,
39
+ 'xtick.minor.size': 1.5, 'ytick.minor.size': 1.5, 'xtick.minor.width': 0.4, 'ytick.minor.width': 0.4,
40
+ 'axes.spines.top': False, 'axes.spines.right': False, 'axes.axisbelow': True,
41
+ 'lines.linewidth': 1.1, 'lines.markersize': 3.8, 'errorbar.capsize': 1.6,
42
+ 'legend.frameon': False, 'legend.handlelength': 2.2, 'legend.columnspacing': 1.2, 'legend.handletextpad': 0.5,
43
+ 'pdf.fonttype': 42, 'ps.fonttype': 42, 'savefig.dpi': 300, 'savefig.bbox': 'tight', 'savefig.pad_inches': 0.02,
44
+ })
45
+ OURS = 'GlobMP (Ours)'
46
+ STY = {'pdm': dict(color='#7f7f7f', marker='h', label='PDM'), 'cd': dict(color='#8c564b', marker='P', label='CD'),
47
+ 'mmd_cbs': dict(color='#17becf', marker='<', label='MMD-CBS'), 'mdoc': dict(color='#bcbd22', marker='>', label='MDOC'),
48
+ 'diffusion_policy': dict(color='#9467bd', marker='x', label='Diff. Policy'),
49
+ 'pcd_lb': dict(color='#ff7f0e', marker='v', label='PCD-LB'), 'pcd_shd': dict(color='#2ca02c', marker='s', label='PCD-SHD'),
50
+ 'final_projection': dict(color='#e377c2', marker='D', label='Final Proj.'), 'direct': dict(color='#d62728', marker='o', label='DiRecT')}
51
+
52
+
53
+ def _save(fig, out):
54
+ for ext in ('.pdf', '.png'):
55
+ fig.savefig(out.rsplit('.', 1)[0] + ext, dpi=300)
56
+
57
+
58
+ def _mfc(st):
59
+ return 'white' if st['marker'] not in ('x', 'P') else st['color']
60
+
61
+
62
+ # ---- Sec. 4.2: Robotarium
63
+ def robotarium(cells_json, gpa_json, out):
64
+ """One row of five map panels: single-plan success against team size."""
65
+ MAPS = ['empty', 'corridor', 'shelf', 'corridorbay', 'crossroom']
66
+ TITLES = {'empty': 'Empty', 'corridor': 'Conveyor', 'shelf': 'Drop-Region', 'corridorbay': 'Corridor-Bay', 'crossroom': 'Cross-Room'}
67
+ NS = [3, 6, 9, 12]
68
+ ORDER = ['cd', 'mmd_cbs', 'pcd_lb', 'pcd_shd', 'final_projection', 'direct']
69
+ C = {(r['method'], r['map'], r['N']): r.get('SR_one') for r in json.load(open(cells_json))}
70
+ G = {(r['map'], r['N']): r.get('SR_one', r.get('SR')) for r in json.load(open(gpa_json))}
71
+ order = [m for m in ORDER if any(C.get((m, mp, n)) is not None for mp in MAPS for n in NS)]
72
+ FH = 1.35
73
+ fig, axes = plt.subplots(1, len(MAPS), figsize=(TEXTWIDTH, FH), sharex=True, sharey=True)
74
+ fig.subplots_adjust(left=0.07, right=0.995, top=1 - 0.42 / FH, bottom=0.35 / FH, wspace=0.12)
75
+ H = []
76
+ for k, mp in enumerate(MAPS):
77
+ ax = axes[k]
78
+ for m in order:
79
+ y = np.array([np.nan if C.get((m, mp, n)) is None else C[m, mp, n] for n in NS], float)
80
+ if np.all(np.isnan(y)): continue
81
+ st = STY[m]
82
+ h, = ax.plot(NS, y, color=st['color'], marker=st['marker'], ms=3.1, lw=0.9, mew=0.6, mfc=_mfc(st), label=st['label'])
83
+ if k == 0: H.append(h)
84
+ yg = np.array([np.nan if G.get((mp, n)) is None else G[mp, n] for n in NS], float)
85
+ ax.plot(NS, yg, color='white', lw=3.2, zorder=5.5, solid_capstyle='round') # casing where lines cross
86
+ h, = ax.plot(NS, yg, color=BLUE, marker='o', ms=3.8, lw=1.7, label=OURS, zorder=6)
87
+ if k == 0: H.insert(0, h)
88
+ ax.set_title(TITLES[mp], fontsize=8, pad=2.5, color=INK)
89
+ ax.set_ylim(-0.05, 1.05); ax.set_yticks([0, 0.5, 1]); ax.set_xticks(NS)
90
+ ax.grid(axis='y', color=GRID, lw=0.5); ax.set_axisbelow(True)
91
+ fig.supxlabel('number of robots', fontsize=8, y=0.015)
92
+ fig.supylabel('single-plan success', fontsize=7, x=0.012, y=(1 - 0.42 / FH + 0.35 / FH) / 2)
93
+ fig.legend(H, [h.get_label() for h in H], loc='upper center', bbox_to_anchor=(0.53, 1.0), ncol=len(H), frameon=False,
94
+ fontsize=7, handlelength=1.8, columnspacing=0.9)
95
+ _save(fig, out); print('wrote', out)
96
+
97
+
98
+ # ---- appendix: simulated suite
99
+ SUITE_MAPS = [('empty', 'Empty'), ('basic', 'Basic'), ('dense', 'Dense'), ('corridor', 'Conveyor'), ('room', 'Highways'),
100
+ ('shelf', 'Drop-Region'), ('corridorbay', 'Corridor-Bay'), ('crossroom', 'Cross-Room')]
101
+ VMAX = {'empty': (0.483, 0.571, 0.675), 'basic': (0.477, 0.567, 0.675), 'dense': (0.565, 0.617, 0.675), 'room': (0.647, 0.783, 0.878),
102
+ 'corridor': (1.21, 1.46, 1.76), 'corridorbay': (1.21, 1.46, 1.76), 'shelf': (0.928, 1.13, 1.34), 'crossroom': (0.928, 1.13, 1.34)}
103
+ REGS = ['low', 'medium', 'high']; SNS = [3, 6, 9, 12, 15, 18, 21]
104
+ ORDER_ALL = ['pdm', 'cd', 'mmd_cbs', 'mdoc', 'diffusion_policy', 'pcd_lb', 'pcd_shd', 'final_projection', 'direct']
105
+
106
+
107
+ def _sr_at(r, b):
108
+ if not r or not r.get('SR_at'): return np.nan
109
+ s = r['SR_at']; ks = sorted(int(k) for k in s); bb = max([k for k in ks if k <= b] or [ks[0]]); return s[str(bb)]
110
+
111
+
112
+ def _metric(r, kind):
113
+ if r is None: return np.nan
114
+ if kind == 'single': return _sr_at(r, 1)
115
+ return r.get('SR', np.nan) if r.get('SR') is not None else np.nan
116
+
117
+
118
+ def suite(kind, baselines_json, gpa_json, out):
119
+ """single / best64: one row per map, one column per velocity cap (all 168 cells). compact: one panel per
120
+ map, single-plan success averaged over the three caps."""
121
+ C = {(r['method'], r['map'], r['N'], r['regime']): r for r in json.load(open(baselines_json))}
122
+ G = {(r['map'], r['N'], r['regime']): r for r in json.load(open(gpa_json))}
123
+ mk = 'single' if kind in ('single', 'compact') else 'best64'
124
+ order = [m for m in ORDER_ALL if any(not np.isnan(_metric(C.get((m, mp, n, rg)), mk)) for mp, _ in SUITE_MAPS for n in SNS for rg in REGS)]
125
+ if kind == 'compact':
126
+ fig, axes = plt.subplots(2, 4, figsize=(TEXTWIDTH, 2.95), sharex=True, sharey=True)
127
+ fig.subplots_adjust(left=0.07, right=0.995, top=0.86, bottom=0.115, wspace=0.12, hspace=0.34)
128
+ H = []
129
+ for k, (mp, title) in enumerate(SUITE_MAPS):
130
+ ax = axes.ravel()[k]
131
+ for m in order:
132
+ y = np.nanmean([[_metric(C.get((m, mp, n, rg)), 'single') for n in SNS] for rg in REGS], axis=0)
133
+ if np.all(np.isnan(y)): continue
134
+ st = STY[m]; h, = ax.plot(SNS, y, color=st['color'], marker=st['marker'], ms=3.1, lw=0.9, mew=0.6, mfc=_mfc(st), label=st['label'])
135
+ if k == 0: H.append(h)
136
+ yg = np.nanmean([[(G.get((mp, n, rg)) or {}).get('SR', np.nan) for n in SNS] for rg in REGS], axis=0)
137
+ ax.plot(SNS, yg, color='white', lw=3.2, zorder=5.5, solid_capstyle='round')
138
+ h, = ax.plot(SNS, yg, color=BLUE, marker='o', ms=3.8, lw=1.7, label=OURS, zorder=6)
139
+ if k == 0: H.append(h)
140
+ ax.set_title(title, fontsize=8, pad=2.5, color=INK)
141
+ ax.set_ylim(-0.05, 1.05); ax.set_yticks([0, 0.5, 1]); ax.set_xticks([3, 9, 15, 21])
142
+ ax.grid(axis='y', color=GRID, lw=0.5); ax.set_axisbelow(True)
143
+ fig.supxlabel('number of agents', fontsize=8, y=0.015); fig.supylabel('single-plan success', fontsize=8, x=0.012)
144
+ H = [H[-1]] + H[:-1]
145
+ fig.legend(H, [h.get_label() for h in H], loc='upper center', bbox_to_anchor=(0.53, 1.0), ncol=len(H), frameon=False,
146
+ fontsize=7, handlelength=1.8, columnspacing=0.9)
147
+ _save(fig, out); print('wrote', out); return
148
+ fig, axes = plt.subplots(len(SUITE_MAPS), 3, figsize=(TEXTWIDTH, 8.3), sharex=True, sharey=True)
149
+ for ri, (mp, title) in enumerate(SUITE_MAPS):
150
+ for ci, rg in enumerate(REGS):
151
+ ax = axes[ri, ci]
152
+ for m in order:
153
+ y = [_metric(C.get((m, mp, n, rg)), mk) for n in SNS]
154
+ if all(np.isnan(y)): continue
155
+ st = STY[m]; ax.plot(SNS, y, color=st['color'], marker=st['marker'], ms=2.4, lw=0.8, mew=0.5, mfc=_mfc(st), label=st['label'])
156
+ yg = [(G.get((mp, n, rg)) or {}).get('SR', np.nan) for n in SNS]
157
+ ax.plot(SNS, yg, color=BLUE, marker='o', ms=2.8, lw=1.5, label=OURS, zorder=6)
158
+ ax.set_xticks([3, 9, 15, 21]); ax.set_ylim(-0.05, 1.05); ax.set_yticks([0, 0.5, 1]); ax.grid(axis='y', color=GRID, lw=0.5); ax.set_axisbelow(True)
159
+ ax.set_title(f"{title}, $v_{{\\max}}={VMAX[mp][ci]:g}$ ({rg})", fontsize=7, pad=2, color=INK)
160
+ fig.supxlabel('number of agents', fontsize=8, y=0.005)
161
+ fig.supylabel({'single': 'single-plan success', 'best64': 'success, best of 64'}[kind], fontsize=8, x=0.005)
162
+ fig.subplots_adjust(left=0.07, right=0.995, top=0.945, bottom=0.045, wspace=0.08, hspace=0.42)
163
+ h, l = axes[0, 1].get_legend_handles_labels(); h = [h[-1]] + h[:-1]; l = [l[-1]] + l[:-1]
164
+ fig.legend(h, l, loc='upper center', ncol=min(len(l), 6), frameon=False, fontsize=7, bbox_to_anchor=(0.5, 1.0), handlelength=1.8, columnspacing=0.9)
165
+ _save(fig, out); print('wrote', out)
166
+
167
+
168
+ # ---- appendix: velocity axis, read back from the generated tables
169
+ def parse_tables(path):
170
+ """Rows of hardsuite_tables.tex: N & Method & (SR CS Coll DA Time) x 3 caps; bolding and deviations stripped,
171
+ SR and CS back to fractions."""
172
+ s = open(path).read()
173
+ out = []
174
+ for t in re.findall(r'\\begin\{table\}.*?\\end\{table\}', s, re.S):
175
+ lab = re.search(r'\\label\{tab:hard-([a-z]+)\}', t).group(1)
176
+ caps = re.findall(r'\\multicolumn\{5\}\{c\}\{(Low|Medium|High) \(\$v_\{\\max\}=([0-9.]+)\$\)\}', t)
177
+ regimes = [(r.lower(), float(v)) for r, v in caps]
178
+ cur_n = None
179
+ for line in t.split(r'\midrule', 1)[1].split('\\\\'):
180
+ line = line.replace(r'\midrule', '').replace(r'\bottomrule', '').replace(r'\end{tabular}', '').strip()
181
+ if not line or '&' not in line: continue
182
+ cells = [c.strip() for c in line.split('&')]
183
+ if len(cells) != 17: continue
184
+ if cells[0]: cur_n = int(cells[0])
185
+ meth = re.sub(r'\\textbf\{(.*?)\}', r'\1', cells[1]).strip()
186
+ meth = re.sub(r'\$\^\{\\dagger\}\$', '', meth).replace('\\ ', ' ').strip()
187
+
188
+ def num(x, pct=False):
189
+ x = re.sub(r'\$_\{\\pm[^}]*\}\$', '', x); x = re.sub(r'\\textbf\{(.*?)\}', r'\1', x).strip()
190
+ try: v = float(x)
191
+ except ValueError: return None
192
+ return v / 100.0 if pct else v
193
+ vals = cells[2:]
194
+ for ri, (rname, vcap) in enumerate(regimes):
195
+ sr, cs, coll, da, tm = vals[ri * 5:(ri + 1) * 5]
196
+ out.append(dict(map=lab, N=cur_n, method=meth, regime=rname, vcap=vcap, SR=num(sr, True), CS=num(cs, True),
197
+ Coll=num(coll), DA=num(da), T=num(tm)))
198
+ return out
199
+
200
+
201
+ VSTYLE = {'CD': dict(color='#8c564b', marker='P', ls='-'), 'PCD-LB': dict(color='#ff7f0e', marker='v', ls=':'),
202
+ 'PCD-SHD': dict(color='#2ca02c', marker='s', ls='--'), 'Final Proj.': dict(color='#e377c2', marker='D', ls='-.'),
203
+ 'DiRecT': dict(color='#d62728', marker='o', ls='-'), 'MDOC': dict(color='#9467bd', marker='h', ls=(0, (4, 1.5))),
204
+ OURS: dict(color=BLUE, marker='o', ls='-')}
205
+
206
+
207
+ def velocity(tables, out, n=9):
208
+ """Success and constraint safety against the velocity cap at N = n, two panels per map. Methods with no
209
+ velocity mechanism (MMD-CBS, Diffusion Policy) are omitted."""
210
+ rows = parse_tables(tables)
211
+ NO_AXIS = {'MMD-CBS', 'Diff. Policy', 'Diffusion Policy'}
212
+ ORDER = [OURS, 'CD', 'PCD-LB', 'PCD-SHD', 'MDOC', 'Final Proj.', 'DiRecT']
213
+ METRICS = [('SR', 'success'), ('CS', 'safety')]
214
+ series = {}
215
+ for r in rows:
216
+ if r['N'] != n or r['method'] in NO_AXIS: continue
217
+ series.setdefault((r['map'], r['method']), []).append((r['vcap'], r['SR'], r['CS']))
218
+ for k in series: series[k].sort(key=lambda t: t[0])
219
+ PER_ROW = 2; ncol = PER_ROW * len(METRICS); nrow = (len(SUITE_MAPS) + PER_ROW - 1) // PER_ROW
220
+ fig, axes = plt.subplots(nrow, ncol, figsize=(TEXTWIDTH, 1.42 * nrow + 0.45), squeeze=False, sharey=True)
221
+ plt.subplots_adjust(left=0.06, right=0.995, top=0.945, bottom=0.07, hspace=0.62, wspace=0.12)
222
+ drawn = []
223
+ for mi, (key, disp) in enumerate(SUITE_MAPS):
224
+ ri, slot = divmod(mi, PER_ROW)
225
+ for mj, (metric, mlabel) in enumerate(METRICS):
226
+ ax = axes[ri][slot * len(METRICS) + mj]
227
+ for meth in ORDER:
228
+ pts = series.get((key, meth))
229
+ if not pts: continue
230
+ keep = [(p[0], p[1 + mj]) for p in pts if p[1 + mj] is not None]
231
+ if len(keep) < 2: continue
232
+ st = VSTYLE.get(meth, {}); ours = meth == OURS
233
+ ax.plot([k[0] for k in keep], [k[1] for k in keep], color=st.get('color'), marker=st.get('marker', 'o'),
234
+ ls=st.get('ls', '-'), lw=1.6 if ours else 0.8, ms=2.8 if ours else 2.2, mew=0.5,
235
+ mfc=st.get('color') if ours else 'white', zorder=6 if ours else 4, label=meth)
236
+ if meth not in drawn: drawn.append(meth)
237
+ ax.set_ylim(-0.04, 1.04); ax.set_yticks([0, 0.5, 1.0])
238
+ ax.tick_params(labelsize=6, length=2.2, pad=1.5)
239
+ ax.grid(axis='y', color=GRID, lw=0.5); ax.set_axisbelow(True)
240
+ ax.set_title('%s, %s' % (disp, mlabel), fontsize=7, pad=2.5, color=INK)
241
+ ax.set_xlabel(r'$v_{\max}$', fontsize=6.5, labelpad=1)
242
+ handles = [plt.Line2D([], [], color=VSTYLE[m]['color'], marker=VSTYLE[m]['marker'], ls=VSTYLE[m]['ls'],
243
+ lw=1.6 if m == OURS else 0.8, ms=2.8, mew=0.5, mfc=VSTYLE[m]['color'] if m == OURS else 'white', label=m)
244
+ for m in ORDER if m in drawn]
245
+ fig.supylabel('success / safety', fontsize=7, x=0.005)
246
+ fig.legend(handles=handles, loc='upper center', ncol=len(handles), fontsize=7, frameon=False, handlelength=1.6,
247
+ handletextpad=0.4, columnspacing=1.2, borderaxespad=0.1, bbox_to_anchor=(0.5, 1.0))
248
+ _save(fig, out); print('wrote', out, f'(N={n}, {len(drawn)} methods)')
249
+
250
+
251
+ if __name__ == '__main__':
252
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
253
+ sub = ap.add_subparsers(dest='cmd', required=True)
254
+ r = sub.add_parser('robotarium'); r.add_argument('--cells', required=True); r.add_argument('--gpa', required=True); r.add_argument('--out', required=True)
255
+ s = sub.add_parser('suite'); s.add_argument('kind', choices=['single', 'best64', 'compact'])
256
+ s.add_argument('--baselines', required=True); s.add_argument('--gpa', required=True); s.add_argument('--out', required=True)
257
+ v = sub.add_parser('velocity'); v.add_argument('--tables', required=True); v.add_argument('--out', required=True); v.add_argument('--n', type=int, default=9)
258
+ a = ap.parse_args()
259
+ if a.cmd == 'robotarium': robotarium(a.cells, a.gpa, a.out)
260
+ elif a.cmd == 'suite': suite(a.kind, a.baselines, a.gpa, a.out)
261
+ else: velocity(a.tables, a.out, a.n)
applications/multi_robot/robotarium/instances.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Start-goal instances and map geometry for the multi-robot experiments.
2
+
3
+ Two settings:
4
+
5
+ * the simulated suite (appendix, app:exp-multirobot): 8 maps x N in {3,...,21}, 20 shared start-goal
6
+ configurations per (map, N), 3 velocity caps per map. One file per (map, N) in data/suite/, written by
7
+ `export_suite` from the benchmark's shared-instance file and its own environment classes.
8
+ * the Robotarium setting (Sec. 4.2): 5 maps fitted to the 3.2 x 2.0 m arena, N in {3, 6, 9, 12}, 10 instances
9
+ per (map, N) (nine sampled, one antipodal ring), robot radius 0.13 m. data/robotarium/instances.npz (keys
10
+ '<map>rbt/N<n>/{starts,goals}', world metres), maps.json and geometry.json (axis-aligned boxes in metres).
11
+
12
+ Map keys follow the benchmark's code names; the paper's names are in PAPER_NAME.
13
+ """
14
+ import json
15
+ import os
16
+
17
+ import numpy as np
18
+
19
+ DATA = os.environ.get('GLOBMP_MR_DATA', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data'))
20
+
21
+ PAPER_NAME = {'empty': 'Empty', 'basic': 'Basic', 'dense': 'Dense', 'corridor': 'Conveyor', 'room': 'Highways',
22
+ 'shelf': 'Drop-Region', 'corridorbay': 'Corridor-Bay', 'crossroom': 'Cross-Room'}
23
+
24
+ # ---- simulated suite
25
+ SUITE_MAPS = ['empty', 'basic', 'dense', 'corridor', 'room', 'shelf', 'corridorbay', 'crossroom']
26
+ SUITE_NS = [3, 6, 9, 12, 15, 18, 21]
27
+ REGIMES = ['low', 'medium', 'high']
28
+ OBS_SCALE = {'empty': 1.0, 'basic': 2.2, 'dense': 1.0, 'corridor': 1.5, 'room': 1.45, 'shelf': 1.6,
29
+ 'corridorbay': 1.15, 'crossroom': 1.1}
30
+ ENV_CLASS = {'empty': 'EnvEmptyNoWait2D', 'basic': 'EnvEmptyNoWait2D', 'dense': 'EnvEmptyNoWait2D',
31
+ 'corridor': 'EnvConveyor2D', 'room': 'EnvHighways2D', 'shelf': 'EnvDropRegion2D',
32
+ 'corridorbay': 'EnvCorridorSMD2D', 'crossroom': 'EnvCrossRoom2D'}
33
+ # velocity caps (low, medium, high); must equal VCAP in tables.py
34
+ VCAP = {'empty': (0.483, 0.571, 0.675), 'basic': (0.477, 0.567, 0.675), 'dense': (0.565, 0.617, 0.675),
35
+ 'room': (0.647, 0.783, 0.878), 'corridor': (1.21, 1.46, 1.76), 'corridorbay': (1.21, 1.46, 1.76),
36
+ 'shelf': (0.928, 1.13, 1.34), 'crossroom': (0.928, 1.13, 1.34)}
37
+
38
+
39
+ def suite_robot_radius(mp):
40
+ return 0.1 if mp == 'empty' else 0.05
41
+
42
+
43
+ def load_suite(mp, N, inst_dir=None):
44
+ """dict with centers (K,2), radii (K,), ws (2,2) [lo; hi], box_c, box_h, starts (I,N,2), goals (I,N,2),
45
+ horizon (64), robot_r and the three velocity caps vmax_low / vmax_med / vmax_high."""
46
+ f = os.path.join(inst_dir or os.path.join(DATA, 'suite'), f'{mp}_N{N}.npz')
47
+ D = np.load(f, allow_pickle=True)
48
+ return {k: D[k] for k in D.files}
49
+
50
+
51
+ # ---- Robotarium setting
52
+ RBT_MAPS = ['empty', 'corridor', 'shelf', 'corridorbay', 'crossroom']
53
+ RBT_NS = [3, 6, 9, 12]
54
+ RBT_ROBOT_R = 0.13
55
+ RBT_ARENA = np.array([[-1.6, -1.0], [1.6, 1.0]])
56
+
57
+
58
+ def load_robotarium(mp, N, data_dir=None):
59
+ """dict with starts, goals (10, N, 2) in metres, boxes (centers (K,2), half sizes (K,2)), ws, robot_r."""
60
+ d = data_dir or os.path.join(DATA, 'robotarium')
61
+ I = np.load(os.path.join(d, 'instances.npz'))
62
+ geo = json.load(open(os.path.join(d, 'geometry.json')))['maps'][mp]
63
+ BC = np.array([o['center'] for o in geo['obstacles']], float).reshape(-1, 2)
64
+ BH = np.array([o['size'] for o in geo['obstacles']], float).reshape(-1, 2) / 2.0
65
+ return dict(starts=I[f'{mp}rbt/N{N}/starts'], goals=I[f'{mp}rbt/N{N}/goals'], box_c=BC, box_h=BH,
66
+ ws=RBT_ARENA.copy(), robot_r=RBT_ROBOT_R, obs_scale=geo['obs_scale'],
67
+ wide_corridor=bool(geo.get('wide_corridor', False)), env_class=geo['env_class'])
68
+
69
+
70
+ # ---- geometry helpers
71
+ def boxes_to_circles(centers, half_sizes, halo=0.02, max_circles_per_box=400):
72
+ """Cover each axis-aligned box by discs whose union contains it, so avoiding every disc implies avoiding
73
+ the box. The union overshoots the box by (sqrt(2)-1) times the cell half-size, so the box is subdivided
74
+ until that halo is at most `halo`."""
75
+ out = []
76
+ C = np.asarray(centers, dtype=float).reshape(-1, 2)
77
+ Hs = np.asarray(half_sizes, dtype=float).reshape(-1, 2)
78
+ for c, h in zip(C, Hs):
79
+ hx, hy = float(h[0]), float(h[1])
80
+ if hx <= 1e-9 or hy <= 1e-9:
81
+ continue
82
+ cell = halo / (np.sqrt(2.0) - 1.0)
83
+ nx = max(1, int(np.ceil(hx / cell)))
84
+ ny = max(1, int(np.ceil(hy / cell)))
85
+ while nx * ny > max_circles_per_box:
86
+ nx = max(1, nx - 1); ny = max(1, ny - 1)
87
+ rx, ry = hx / nx, hy / ny
88
+ r = float(np.hypot(rx, ry))
89
+ xs = (np.arange(nx) + 0.5) * 2 * rx - hx
90
+ ys = (np.arange(ny) + 0.5) * 2 * ry - hy
91
+ for xq in xs:
92
+ for yq in ys:
93
+ out.append((c + np.array([xq, yq]), r))
94
+ return out
95
+
96
+
97
+ def box_discs(BC, BH, halo, halo_wall=0.003, aspect=3.0):
98
+ """Discs whose union contains each box (the Robotarium projector's obstacle model). Square-ish boxes: the
99
+ grid cover above with overshoot <= `halo`. Thin walls (aspect >= 3): ONE row of discs of radius about the
100
+ wall's half-thickness, spaced so the long faces overshoot by <= `halo_wall` (a 2 cm halo on a wall would
101
+ narrow the 0.29 m Conveyor channel below the 0.26 m a robot needs)."""
102
+ C, R = [], []
103
+ for c, h in zip(np.asarray(BC, float).reshape(-1, 2), np.asarray(BH, float).reshape(-1, 2)):
104
+ L, t = (0, 1) if h[0] >= h[1] else (1, 0) # long axis, thin axis
105
+ if h[L] >= aspect * h[t]:
106
+ rl = np.sqrt((h[t] + halo_wall) ** 2 - h[t] ** 2) # half-spacing along the wall
107
+ nl = int(np.ceil(h[L] / rl)); rl = h[L] / nl
108
+ rad = float(np.hypot(rl, h[t]))
109
+ for k in range(nl):
110
+ p = np.array(c, float); p[L] += -h[L] + (2 * k + 1) * rl
111
+ C.append(p); R.append(rad)
112
+ else:
113
+ for cc, rr in boxes_to_circles(c[None], h[None], halo=halo):
114
+ C.append(np.asarray(cc, float)); R.append(float(rr))
115
+ return np.array(C).reshape(-1, 2), np.array(R).reshape(-1)
116
+
117
+
118
+ def benchmark_geometry(task, halo):
119
+ """Obstacle discs and workspace in world coordinates, read off the benchmark environment of `task`:
120
+ sphere fields as they are, box fields covered by boxes_to_circles(halo). Returns (circles, ws, BC, BH)."""
121
+ env = task.env
122
+ lim = env.limits.cpu().numpy()
123
+ ws = np.stack([lim[0, :2], lim[1, :2]])
124
+ C, Hs, Sph = [], [], []
125
+ for obj in (env.obj_fixed_list or []):
126
+ for fld in (getattr(obj, 'fields', None) or []):
127
+ cen = getattr(fld, 'centers', None)
128
+ if cen is None or len(cen) == 0:
129
+ continue
130
+ cen = np.asarray(cen.cpu(), dtype=float)[:, :2]
131
+ sz = getattr(fld, 'sizes', None)
132
+ if sz is not None: # box field: sizes are full extents
133
+ C.append(cen); Hs.append(np.asarray(sz.cpu(), dtype=float)[:, :2] / 2.0)
134
+ else:
135
+ rad = np.asarray(getattr(fld, 'radii', getattr(fld, 'radius')).cpu(), dtype=float).reshape(-1)
136
+ Sph += [(c, float(r)) for c, r in zip(cen, rad)]
137
+ circles = list(Sph)
138
+ if C:
139
+ circles += boxes_to_circles(np.concatenate(C), np.concatenate(Hs), halo=halo)
140
+ BC = np.concatenate(C) if C else np.zeros((0, 2)); BH = np.concatenate(Hs) if Hs else np.zeros((0, 2))
141
+ return circles, ws, BC, BH
142
+
143
+
144
+ def export_suite(shared_npz, trained_models_dir, out_dir, maps=None):
145
+ """Write data/suite/<map>_N<n>.npz from the benchmark's shared instances (the same configurations every
146
+ method plans). Runs in the benchmark's environment (needs mmd and torch_robotics on the path). The disc
147
+ cover of the box obstacles uses a halo of half the endpoints' clearance, clipped to [0.004, 0.030]."""
148
+ from evaluate import build_task
149
+ os.makedirs(out_dir, exist_ok=True)
150
+ S = np.load(shared_npz, allow_pickle=True)
151
+ for mp in (maps or SUITE_MAPS):
152
+ os.environ['OBS_SCALE'] = str(OBS_SCALE[mp]) # read by the benchmark at environment construction
153
+ rr = suite_robot_radius(mp)
154
+ t = build_task(ENV_CLASS[mp] + '-RobotPlanarDisk', trained_models_dir)
155
+ P0 = np.concatenate([S[f'{mp}/N3/starts'].reshape(-1, 2), S[f'{mp}/N3/goals'].reshape(-1, 2)])
156
+ c_fine, _, _, _ = benchmark_geometry(t, 0.002)
157
+ if c_fine:
158
+ Cf = np.array([c for c, _ in c_fine]); Rf = np.array([r for _, r in c_fine])
159
+ clear = float((np.linalg.norm(P0[:, None, :] - Cf[None], axis=2) - (Rf[None] + rr)).min())
160
+ else:
161
+ clear = 1.0
162
+ halo = float(min(0.030, max(0.004, 0.5 * clear)))
163
+ circ, ws, BC, BH = benchmark_geometry(t, halo)
164
+ C = np.array([c for c, _ in circ]) if circ else np.zeros((0, 2))
165
+ Rd = np.array([r for _, r in circ]) if circ else np.zeros((0,))
166
+ for n in SUITE_NS:
167
+ k = f'{mp}/N{n}'
168
+ if k + '/starts' not in S.files:
169
+ print('# missing', k, flush=True); continue
170
+ np.savez(os.path.join(out_dir, f'{mp}_N{n}.npz'), centers=C, radii=Rd, ws=ws, box_c=BC, box_h=BH,
171
+ starts=S[k + '/starts'], goals=S[k + '/goals'], agents=n, horizon=64,
172
+ robot_r=rr, obs_scale=OBS_SCALE[mp], vmax_low=VCAP[mp][0], vmax_med=VCAP[mp][1],
173
+ vmax_high=VCAP[mp][2], map=mp, env_class=ENV_CLASS[mp])
174
+ print(f'# {mp}: {len(circ)} discs, {len(BC)} boxes, r={rr}, halo={halo:.4f}, endpoint clearance {clear:+.4f}', flush=True)
175
+
176
+
177
+ if __name__ == '__main__':
178
+ import argparse
179
+ ap = argparse.ArgumentParser(description='Export the simulated-suite instances from the benchmark (benchmark env).')
180
+ ap.add_argument('--shared-npz', required=True, help="the benchmark's shared instance file (instances_shared.npz)")
181
+ ap.add_argument('--trained-models', required=True, help="the benchmark's data_trained_models directory")
182
+ ap.add_argument('--out', default=os.path.join(DATA, 'suite'))
183
+ ap.add_argument('--maps', nargs='*')
184
+ a = ap.parse_args()
185
+ export_suite(a.shared_npz, a.trained_models, a.out, a.maps)
applications/multi_robot/robotarium/projector.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GlobMP as the projector inside a diffusion sampler (the BaseProjector interface of DiRecT).
2
+
3
+ This is the configuration that produced the GlobMP Robotarium results (figures/robotarium_gpa_sr_one.json):
4
+ the sampler's single-agent diffusion models propose a batch of team plans, and at every projection call the
5
+ proposal of each candidate team is the piecewise-linear reference of a tracking cost. GlobMP restores the
6
+ team (Algorithm 1) and descends on the tracking cost (Algorithm 2) under continuous-time blocks: pairwise
7
+ separation 2r, a uniform speed bound, the workspace box and the obstacle discs. The waypoints handed back
8
+ are samples of a curve that satisfies every block at every t.
9
+
10
+ Batch layout (DiRecT): B = n_agents * G samples, agent-major, so sample a*G+j is robot a in team j.
11
+ Index 0 of each plan is pinned by params['pos_init'].
12
+
13
+ Schedule over the denoising steps (GPA_NCALLS calls per instance): intermediate calls only steer (short
14
+ restoration, no descent, partially restored teams are handed back); the last call is the full solve and
15
+ returns certified teams only. A team whose piecewise-linear motion already satisfies every constraint
16
+ exactly is left alone. With GPA_PRIORITIZED=1, if no team certifies jointly at the last call, the teams are
17
+ certified one robot at a time (each robot against the robots already placed, as moving obstacles), with
18
+ wall, timing and detour repairs of the reference; with GPA_FIRST_CERT=1 the first certified team ends the
19
+ call, since the method returns one plan.
20
+
21
+ Every knob is an environment variable (read inside the sampler process); the values used for the paper are
22
+ in run/run_cell.sh and the README.
23
+ """
24
+ import os
25
+ import time
26
+
27
+ import numpy as np
28
+ import torch
29
+
30
+ from globmp import Problem, init_waypoints, spline as bern
31
+
32
+ DT = torch.float64
33
+
34
+
35
+ def seg_min_dist(A, B):
36
+ """Exact minimum distance between two robots moving linearly between waypoints. A, B: (..., H, 2)."""
37
+ P = A[..., :-1, :] - B[..., :-1, :]
38
+ D = (A[..., 1:, :] - A[..., :-1, :]) - (B[..., 1:, :] - B[..., :-1, :])
39
+ dd = (D * D).sum(-1)
40
+ s = np.clip(np.where(dd > 1e-18, -(P * D).sum(-1) / np.maximum(dd, 1e-18), 0.0), 0.0, 1.0)
41
+ Q = P + s[..., None] * D
42
+ return np.sqrt((Q * Q).sum(-1)).min(-1)
43
+
44
+
45
+ def _static_route(w, discs, clr, lo, hi, res=0.01, lam=4.0):
46
+ """Wall-free route for ONE robot from w[0] to w[-1] on a grid over [lo, hi]: A* with step cost = length +
47
+ lam * distance to the robot's own reference path, so the route follows the reference where the reference
48
+ is feasible. A cell is free if every obstacle disc is at least `clr` away. Returns the reference re-timed
49
+ onto the route (same progress-versus-time profile), or None if no route exists."""
50
+ import heapq
51
+ nx = int(np.ceil((hi[0] - lo[0]) / res)) + 1; ny = int(np.ceil((hi[1] - lo[1]) / res)) + 1
52
+ xs = lo[0] + res * np.arange(nx); ys = lo[1] + res * np.arange(ny)
53
+ X, Y = np.meshgrid(xs, ys, indexing='ij'); Pg = np.stack([X, Y], -1)
54
+ free = np.ones((nx, ny), bool)
55
+ for c, r in discs:
56
+ free &= np.linalg.norm(Pg - np.asarray(c, float), axis=-1) >= float(r) + clr
57
+ dref = np.full((nx, ny), np.inf)
58
+ for q in w[::2]: dref = np.minimum(dref, np.linalg.norm(Pg - q, axis=-1))
59
+ cell = lambda p: (int(np.clip(round((p[0] - lo[0]) / res), 0, nx - 1)), int(np.clip(round((p[1] - lo[1]) / res), 0, ny - 1)))
60
+ s, g = cell(w[0]), cell(w[-1])
61
+ free[s] = free[g] = True # the benchmark's endpoints are feasible
62
+ nb = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)]
63
+ dist = {s: 0.0}; prev = {}; pq = [(0.0, s)]; gx, gy = g
64
+ while pq:
65
+ f_, u = heapq.heappop(pq)
66
+ if u == g: break
67
+ du = dist[u]
68
+ for dx_, dy_ in nb:
69
+ v = (u[0] + dx_, u[1] + dy_)
70
+ if not (0 <= v[0] < nx and 0 <= v[1] < ny) or not free[v]: continue
71
+ step = res * (1.4142 if dx_ and dy_ else 1.0)
72
+ nd = du + step * (1.0 + lam * dref[v])
73
+ if nd < dist.get(v, np.inf):
74
+ dist[v] = nd; prev[v] = u
75
+ heapq.heappush(pq, (nd + res * np.hypot(v[0] - gx, v[1] - gy), v))
76
+ if g not in dist: return None
77
+ path = [g]
78
+ while path[-1] != s: path.append(prev[path[-1]])
79
+ path = np.array([[xs[i], ys[j]] for i, j in path[::-1]]); path[0] = w[0]; path[-1] = w[-1]
80
+ arc = lambda P_: np.r_[0, np.cumsum(np.linalg.norm(np.diff(P_, axis=0), axis=1))]
81
+ ar, ap = arc(w), arc(path)
82
+ frac = ar / max(ar[-1], 1e-12)
83
+ return np.stack([np.interp(frac * ap[-1], ap, path[:, k]) for k in range(2)], 1)
84
+
85
+
86
+ class GPAProjector:
87
+ """Implements BaseProjector.project(x, params) -> x_proj."""
88
+
89
+ def __init__(self, n_agents, robot_radius, ws_bounds=None, obstacles=None,
90
+ degree=None, segments=None, ell_j=2, ell=0, elev=None, margin=0.0,
91
+ alg1_cap=None, alg2_blocks=None, alg2_steps=3, max_teams=None,
92
+ team_budget=None, verbose=False):
93
+ env = lambda k, d, f=int: f(os.environ.get(k) or d)
94
+ self.n_agents = int(n_agents)
95
+ self.robot_radius = float(robot_radius)
96
+ self.ws_bounds = None if ws_bounds is None else np.asarray(ws_bounds, dtype=float)
97
+ self.obstacles = obstacles or []
98
+ self.d = env('GPA_DEG', degree or 5)
99
+ self.M = env('GPA_M', segments or 21)
100
+ self.De = env('GPA_ELEV', elev or 30)
101
+ self.ell_j, self.ell = int(ell_j), int(ell)
102
+ self.margin = env('GPA_MARGIN', margin or 0.005, float)
103
+ self.alg1_cap = env('GPA_CAP', alg1_cap or 3000)
104
+ self.alg2_blocks = env('GPA_BLOCKS', alg2_blocks if alg2_blocks is not None else 4)
105
+ self.alg2_steps = int(alg2_steps)
106
+ self.max_teams = env('GPA_MAXTEAMS', max_teams or 10 ** 6)
107
+ self.team_budget = env('GPA_TEAMBUDGET', team_budget or 20, float)
108
+ self.reach = env('GPA_REACH', 0.25, float)
109
+ self.verbose = verbose or bool(int(os.environ.get('GPA_VERBOSE', '0')))
110
+ self.smoothness_weight = 0.0 # interface parity with the other projectors, unused
111
+ self.stats = dict(calls=0, teams_seen=0, teams_feasible=0, teams_solved=0,
112
+ teams_ok=0, teams_failed=0, time=0.0)
113
+
114
+ def _problem(self, n, w0, w1, M):
115
+ return Problem(n, self.d, 4, self.ell, M, w0, w1, ell_j=self.ell_j, delta=self.margin, mode='orthant', De=self.De)
116
+
117
+ # ---------------- one team ----------------
118
+ def _keep_right(self, Wf):
119
+ """Steer the reference before a solve. Keep-right (GPA_RIGHTHAND): wherever a robot has an ONCOMING
120
+ robot within GPA_RH_RANGE robot radii, shift its reference to its own right, weighted by proximity, so
121
+ head-on pairs pass side by side. Circulation (GPA_CIRC): a robot whose heading would pass the
122
+ workspace centre against the team's net rotation is nudged to the side that carries its direction of
123
+ travel (GPA_CIRC_MODE=reflect: moved toward its mirror position instead). GPA_SHIFT_SMOOTH spreads each
124
+ shift over that many waypoints; GPA_RH_WALLSAFE drops shifts that would approach a wall. Endpoints stay
125
+ fixed; not applied at the final call."""
126
+ w = float(os.environ.get('GPA_RIGHTHAND', '0')); wc = float(os.environ.get('GPA_CIRC', '0'))
127
+ if (w <= 0 and wc <= 0) or getattr(self, 'final_call', False):
128
+ return Wf
129
+ L1, n = Wf.shape; na = n // 2
130
+ P = Wf.reshape(L1, na, 2)
131
+ V = np.gradient(P, axis=0)
132
+ sp = np.linalg.norm(V, axis=-1, keepdims=True)
133
+ T = V / np.maximum(sp, 1e-9)
134
+ Rt = np.stack([T[..., 1], -T[..., 0]], -1) # right-hand normal
135
+ R0 = float(os.environ.get('GPA_RH_RANGE', '4')) * self.robot_radius
136
+ D = np.linalg.norm(P[:, :, None] - P[:, None], axis=-1) # (L1, na, na)
137
+ onc = (np.einsum('tai,tbi->tab', T, T) < -0.3) & (D < R0) & (D > 0)
138
+ wt = (np.clip(1 - D / R0, 0, 1) * onc).sum(-1) # (L1, na)
139
+ wt *= (sp[..., 0] > 1e-6)
140
+ shift = w * np.tanh(wt)[..., None] * Rt
141
+ if wc > 0:
142
+ c = np.zeros(2) if self.ws_bounds is None else 0.5 * (np.asarray(self.ws_bounds[0]) + np.asarray(self.ws_bounds[1]))
143
+ # sign of the team's net rotation about the centre, from each robot's start->goal displacement
144
+ # (ties, e.g. antipodal swaps, default to counter-clockwise)
145
+ r0 = P[0] - c[None]; dv = P[-1] - P[0]
146
+ Lz = float((r0[:, 0] * dv[:, 1] - r0[:, 1] * dv[:, 0]).sum())
147
+ scale = float((np.linalg.norm(r0, axis=1) * np.linalg.norm(dv, axis=1)).sum()) + 1e-12
148
+ sgn = -1.0 if Lz < -0.05 * scale else 1.0
149
+ rel = c[None, None] - P
150
+ d = sgn * (T[..., 0] * rel[..., 1] - T[..., 1] * rel[..., 0]) # > 0: moving the team's way round
151
+ Rt = sgn * Rt
152
+ if os.environ.get('GPA_CIRC_MODE', 'push') == 'reflect':
153
+ # one-way lanes: a robot heading the wrong way for its side of the centre (d < 0) is moved toward
154
+ # its mirror position across the centre line (distance 2|d|, capped); right-way robots stay put
155
+ cap = float(os.environ.get('GPA_CIRC_CAP', '0.35'))
156
+ shift = shift + wc * np.minimum(2 * np.clip(-d, 0, None), cap)[..., None] * Rt * (sp > 1e-6)
157
+ else:
158
+ dmin = float(os.environ.get('GPA_CIRC_D', '4')) * self.robot_radius
159
+ shift = shift + wc * np.clip((dmin - d) / dmin, 0, 1)[..., None] * Rt * (sp > 1e-6)
160
+ sw = int(os.environ.get('GPA_SHIFT_SMOOTH', '0'))
161
+ if sw > 1:
162
+ # spread every lane change over about sw waypoints (moving average along time), so a shift never
163
+ # opens a sideways jump that breaks the speed bound
164
+ k = np.ones(sw) / sw; pad = sw // 2
165
+ Sp = np.pad(shift, ((pad, sw - 1 - pad), (0, 0), (0, 0)), mode='edge')
166
+ shift = np.stack([np.stack([np.convolve(Sp[:, a, c], k, mode='valid') for c in range(2)], -1)
167
+ for a in range(shift.shape[1])], 1)
168
+ shift[0] = 0; shift[-1] = 0
169
+ if self.obstacles and os.environ.get('GPA_RH_WALLSAFE', '0') == '1':
170
+ # keep a shift only where the shifted point stays clear of every obstacle disc by r + margin
171
+ Cc = np.array([c for c, _ in self.obstacles], float); Rc = np.array([rr for _, rr in self.obstacles], float)
172
+ Q = P + shift
173
+ clr = (np.linalg.norm(Q[..., None, :] - Cc[None, None], axis=-1) - Rc[None, None]).min(-1)
174
+ need = self.robot_radius + max(self.margin, 0.0) + float(os.environ.get('GPA_RH_CLEAR', '0.02'))
175
+ shift = shift * (clr >= need)[..., None]
176
+ return (P + shift).reshape(L1, n)
177
+
178
+ def _certify_prioritized(self, Wf, dx_max):
179
+ """Final-call certification one robot at a time: each robot is restored alone against the walls, the
180
+ arena, the speed bound and every robot already certified (their spline curves as moving obstacles,
181
+ exact continuous-time separation 2r). Order: fewest conflicts in the reference first; robots that do
182
+ not fit are retried once after the rest, then the ordering restarts with the unplaced robots first.
183
+ The team is certified iff every robot is. Same blocks and tolerances as the joint solve."""
184
+ L = Wf.shape[0] - 1; n = Wf.shape[1]; na = n // 2; M = min(self.M, L); r2 = 2.0 * self.robot_radius
185
+ P = Wf.reshape(L + 1, na, 2)
186
+ conf = np.zeros(na)
187
+ for a in range(na):
188
+ for b in range(na):
189
+ if a != b: conf[a] += float(seg_min_dist(P[None, :, a], P[None, :, b])[0] < r2)
190
+ order = list(np.argsort(conf, kind='stable')); t_end = time.time() + self.team_budget
191
+ t_cap = time.time() + float(os.environ.get('GPA_PRIO_TEAM_MAX', '900')) # safety cap per team
192
+ safety = float(os.environ.get('GPA_PRIO_SAFETY', '600'))
193
+ for rnd in range(int(os.environ.get('GPA_PRIO_RESTARTS', '3'))):
194
+ placed = {} # a -> (segments (M,2,d+1), curve (L+1,2))
195
+ for attempt in range(2):
196
+ pending = [a for a in order if a not in placed]
197
+ if not pending: break
198
+ for a in pending:
199
+ if time.time() > t_cap: return None
200
+ w = P[:, a]; w2 = None
201
+
202
+ def build(refs):
203
+ # one robot: walls near ANY of the given reference paths, speed, arena (hard), and every
204
+ # already certified robot as a moving obstacle (exact continuous-time 2r)
205
+ keep = [(c, r) for c, r in self.obstacles
206
+ if min(np.linalg.norm(q - np.asarray(c, float)[None], axis=1).min() for q in refs)
207
+ <= float(r) + self.robot_radius + self.margin + self.reach]
208
+ p = self._problem(2, w[0], w[-1], M)
209
+ if dx_max and dx_max > 0: p.add_speed(float(dx_max) * L / M)
210
+ for c, r in keep: p.add_sphere(np.asarray(c, float), float(r) + self.robot_radius)
211
+ for _, (Sb, _c) in placed.items(): p.add_moving(Sb, r2)
212
+ if self.ws_bounds is not None:
213
+ p.add_box(self.ws_bounds[0] + self.robot_radius - self.margin + 1e-6, self.ws_bounds[1] - self.robot_radius + self.margin - 1e-6)
214
+ p.set_tracking_fine(refs[-1])
215
+ tv = p.tolvec()
216
+ if self.ws_bounds is not None: tv[torch.tensor([t[0] == 'box' for t in p.types])] = 1e-12
217
+ return p, tv
218
+
219
+ def fits(p_, Z_):
220
+ # the robots execute (and the benchmark scores) the straight lines between waypoints:
221
+ # accept only if that motion clears walls, arena, speed and every placed robot exactly
222
+ S = np.stack([v[1] for v in placed.values()] + [self._curve(p_, Z_, L, M).numpy()])
223
+ return bool(self._pl_slack(S[:, None], dx_max)[0] >= 0)
224
+
225
+ p, tv = build([w])
226
+ Z = init_waypoints(p, w[np.round(np.linspace(0, L, M + 1)).astype(int)])
227
+ Z, eta, ok = p.project(Z, tv, cap=self.alg1_cap, deadline=time.time() + safety); ok = ok and fits(p, Z)
228
+ if not ok and os.environ.get('GPA_REPAIR', '1') == '1' and self.ws_bounds is not None:
229
+ # wall repair: the reference crosses walls; re-route to the nearest wall-free route,
230
+ # keep its timing, certify that instead
231
+ w2 = _static_route(w, self.obstacles, self.robot_radius + self.margin + 0.002,
232
+ self.ws_bounds[0] + self.robot_radius, self.ws_bounds[1] - self.robot_radius)
233
+ if w2 is not None:
234
+ p, tv = build([w, w2])
235
+ Z = init_waypoints(p, w2[np.round(np.linspace(0, L, M + 1)).astype(int)])
236
+ Z, eta, ok = p.project(Z, tv, cap=self.alg1_cap, deadline=time.time() + safety); ok = ok and fits(p, Z)
237
+ if not ok and os.environ.get('GPA_RETIME', '1') == '1':
238
+ # timing repair: the remaining conflicts are with robots already placed; keep the route
239
+ # and try re-timings of it (start later or arrive earlier) within the speed bound
240
+ base = w2 if w2 is not None else w
241
+ arc = np.r_[0, np.cumsum(np.linalg.norm(np.diff(base, axis=0), axis=1))]; tt = np.linspace(0, 1, L + 1)
242
+ frac0 = np.interp(np.arange(L + 1), np.arange(L + 1), arc / max(arc[-1], 1e-12))
243
+ for kind, k in [('delay', .1), ('early', .1), ('delay', .2), ('early', .2), ('delay', .3), ('delay', .4)]:
244
+ if kind == 'delay': u_ = np.clip((tt - k) / (1 - k), 0, 1)
245
+ else: u_ = np.clip(tt / (1 - k), 0, 1)
246
+ fr = np.interp(u_, tt, frac0) * arc[-1]
247
+ wk = np.stack([np.interp(fr, arc, base[:, j]) for j in range(2)], 1)
248
+ if np.linalg.norm(np.diff(wk, axis=0), axis=1).max() > float(dx_max) * 0.98: continue
249
+ p, tv = build([w, wk])
250
+ Z = init_waypoints(p, wk[np.round(np.linspace(0, L, M + 1)).astype(int)])
251
+ Z, eta, ok = p.project(Z, tv, cap=self.alg1_cap, deadline=time.time() + safety); ok = ok and fits(p, Z)
252
+ if ok: break
253
+ if not ok and os.environ.get('GPA_DETOUR', '1') == '1' and self.ws_bounds is not None and placed:
254
+ # detour repair: blocked by robots already placed (e.g. head-on in a one-lane passage);
255
+ # re-route around the walls AND the whole paths of the placed robots it conflicts with
256
+ wr = w2 if w2 is not None else w
257
+ cl = [cb for _, (Sb, cb) in placed.items() if seg_min_dist(wr[None], cb[None])[0] < r2 + self.margin]
258
+ ext = [(q, self.robot_radius) for cb in cl for q in cb
259
+ if min(np.linalg.norm(q - w[0]), np.linalg.norm(q - w[-1])) > r2 + self.margin]
260
+ w3 = None
261
+ if ext:
262
+ w3 = _static_route(w, list(self.obstacles) + ext, self.robot_radius + self.margin + 0.002,
263
+ self.ws_bounds[0] + self.robot_radius + self.margin, self.ws_bounds[1] - self.robot_radius - self.margin)
264
+ if w3 is not None and np.linalg.norm(np.diff(w3, axis=0), axis=1).max() > float(dx_max) * 0.98:
265
+ # longer route: traverse it at constant speed instead of the reference's profile
266
+ a3 = np.r_[0, np.cumsum(np.linalg.norm(np.diff(w3, axis=0), axis=1))]
267
+ w3 = np.stack([np.interp(np.linspace(0, a3[-1], L + 1), a3, w3[:, j]) for j in range(2)], 1)
268
+ if a3[-1] / L > float(dx_max) * 0.9: w3 = None # keep slack for the spline
269
+ if w3 is not None:
270
+ p, tv = build([w, w3])
271
+ Z = init_waypoints(p, w3[np.round(np.linspace(0, L, M + 1)).astype(int)])
272
+ Z, eta, ok = p.project(Z, tv, cap=self.alg1_cap, deadline=time.time() + safety); ok = ok and fits(p, Z)
273
+ if ok: placed[a] = (p.segs(Z).detach().clone(), self._curve(p, Z, L, M).numpy())
274
+ if len(placed) == na: break
275
+ order = [a for a in order if a not in placed] + [a for a in order if a in placed]
276
+ if len(placed) < na: return None
277
+ return torch.as_tensor(np.concatenate([placed[a][1] for a in range(na)], axis=1))
278
+
279
+ def _pl_slack(self, Wt, dx_max):
280
+ """Exact slack of the piecewise-linear motion through the waypoints (what the robots execute and the
281
+ benchmark scores): robot spacing 2r, speed, walls and the arena (a segment stays inside the box iff
282
+ its endpoints do). Wt: (na, G, L+1, 2) -> (G,); feasible iff >= 0."""
283
+ na, G = Wt.shape[0], Wt.shape[1]
284
+ need = 2.0 * self.robot_radius
285
+ slack = np.full(G, np.inf)
286
+ for a in range(na):
287
+ for b in range(a + 1, na):
288
+ slack = np.minimum(slack, seg_min_dist(Wt[a], Wt[b]) - need)
289
+ if dx_max:
290
+ step = np.linalg.norm(np.diff(Wt, axis=2), axis=-1).max(-1).max(0) # (G,)
291
+ slack = np.minimum(slack, dx_max * (1 + 1e-9) - step)
292
+ if self.obstacles:
293
+ Cc = np.array([c for c, _ in self.obstacles], float); Rc = np.array([r for _, r in self.obstacles], float)
294
+ P0, D_ = Wt[..., :-1, :], np.diff(Wt, axis=2) # (na,G,L,2)
295
+ w = Cc[None, None, None] - P0[..., None, :] # (na,G,L,K,2)
296
+ dd = (D_ * D_).sum(-1)[..., None]
297
+ tt = np.clip((w * D_[..., None, :]).sum(-1) / np.maximum(dd, 1e-18), 0, 1)
298
+ dist = np.linalg.norm(w - tt[..., None] * D_[..., None, :], axis=-1) # (na,G,L,K)
299
+ oslack = (dist - Rc - self.robot_radius).min(-1).min(-1).min(0) # (G,)
300
+ slack = np.minimum(slack, oslack)
301
+ if self.ws_bounds is not None:
302
+ lo, hi = np.asarray(self.ws_bounds[0], float), np.asarray(self.ws_bounds[1], float)
303
+ e = np.minimum(Wt - lo, hi - Wt).min(-1).min(-1).min(0) - self.robot_radius
304
+ slack = np.minimum(slack, e)
305
+ return slack
306
+
307
+ def _pl_ok(self, Wf, dx_max):
308
+ """Wf: (L+1, n) team plan -> True iff its piecewise-linear motion is feasible (see _pl_slack)."""
309
+ Wf = np.asarray(Wf, float); na = Wf.shape[1] // 2
310
+ return bool(self._pl_slack(Wf.reshape(Wf.shape[0], na, 2).transpose(1, 0, 2)[:, None], dx_max)[0] >= 0)
311
+
312
+ def _solve_team(self, Wf, dx_max):
313
+ """Wf: (L+1, n) stacked waypoints of one team. Returns (L+1, n) or None."""
314
+ if getattr(self, 'prio', False):
315
+ return self._certify_prioritized(Wf, dx_max)
316
+ Wf = self._keep_right(Wf)
317
+ L = Wf.shape[0] - 1
318
+ n = Wf.shape[1]
319
+ na = n // 2
320
+ M = min(self.M, L)
321
+ t0 = time.time()
322
+ # only the obstacle discs a robot could reach: the solve stays near the reference, and the returned
323
+ # plan is checked against the FULL geometry (_pl_ok, and the benchmark's own scorer)
324
+ obs_near = []
325
+ for a in range(na):
326
+ P_ = Wf[:, 2 * a:2 * a + 2]
327
+ keep = []
328
+ for c, r in self.obstacles:
329
+ d = np.linalg.norm(P_ - np.asarray(c, dtype=float)[None, :], axis=1).min()
330
+ if d <= float(r) + self.robot_radius + self.margin + self.reach:
331
+ keep.append((c, r))
332
+ obs_near.append(keep)
333
+ p = self._problem(n, Wf[0], Wf[-1], M)
334
+ for a in range(na):
335
+ ra = [2 * a, 2 * a + 1]
336
+ for b in range(a + 1, na):
337
+ p.add_sep(ra, [2 * b, 2 * b + 1], 2.0 * self.robot_radius)
338
+ if dx_max and dx_max > 0:
339
+ p.add_speed(float(dx_max) * L / M, rows=ra) # uniform speed bound == every chord <= dx_max
340
+ for c, r in obs_near[a]:
341
+ p.add_sphere(np.asarray(c, dtype=float), float(r) + self.robot_radius, rows=ra)
342
+ if self.ws_bounds is not None:
343
+ # the box is shrunk by delta (= margin) inside the problem: hand it bounds + r - delta so the target is
344
+ # the robot disc inside the arena, and make the box blocks hard
345
+ p.add_box(np.tile(self.ws_bounds[0] + self.robot_radius - self.margin + 1e-6, na),
346
+ np.tile(self.ws_bounds[1] - self.robot_radius + self.margin - 1e-6, na))
347
+ p.set_tracking_fine(Wf)
348
+ tv = p.tolvec()
349
+ if self.ws_bounds is not None:
350
+ tv[torch.tensor([t[0] == 'box' for t in p.types])] = 1e-12
351
+ Z = init_waypoints(p, Wf[np.round(np.linspace(0, L, M + 1)).astype(int)])
352
+ dl = t0 + self.team_budget
353
+ Z, eta, ok = p.project(Z, tv, cap=self.alg1_cap, deadline=dl)
354
+ if not ok:
355
+ # steering call: hand back the partially restored iterate; the final call returns certified plans only
356
+ return self._curve(p, Z, L, M) if getattr(self, 'partial', False) else None
357
+ if self.alg2_blocks > 0:
358
+ Z, eta, _ = p.solve(Z, tv, blocks=self.alg2_blocks, steps=self.alg2_steps, eta=eta, deadline=dl)
359
+ if not bool((p.resid(p.pi_all(Z))[0] <= tv).all()):
360
+ Z, eta, ok = p.project(Z, tv, cap=self.alg1_cap, deadline=dl)
361
+ if not ok:
362
+ return self._curve(p, Z, L, M) if getattr(self, 'partial', False) else None
363
+ return self._curve(p, Z, L, M)
364
+
365
+ @staticmethod
366
+ def _curve(p, Z, L, M):
367
+ """The spline at the L+1 waypoint times -> (L+1, n)."""
368
+ tt = torch.linspace(0, 1, L + 1, dtype=DT)
369
+ seg = torch.clamp((tt * M).floor().long(), 0, M - 1)
370
+ u = tt * M - seg
371
+ bu = bern.bernstein_basis_eval(p.d, u) # (d+1, L+1)
372
+ S = p.segs(Z) # (M, n, d+1)
373
+ return torch.einsum('inp,pi->in', S[seg], bu) # (L+1, n)
374
+
375
+ # ---------------- BaseProjector API ----------------
376
+ def project(self, x, params=None):
377
+ dev, dt_ = x.device, x.dtype
378
+ B, Hm1, _ = x.shape
379
+ na = self.n_agents
380
+ G = B // na
381
+ pos_init = params['pos_init'].detach().to('cpu', DT)
382
+ dx_max = float(params['dx_max']) if params.get('dx_max', None) is not None else None
383
+ xc = x.detach().to('cpu', DT)
384
+ out = xc.clone()
385
+ self.stats['calls'] += 1
386
+ t_call = time.time()
387
+
388
+ idx = np.arange(B).reshape(na, G) # idx[a, j] = a*G + j
389
+ W = torch.cat([pos_init.unsqueeze(1), xc], 1).numpy() # (B, L+1, 2)
390
+ Wt = W[idx] # (na, G, L+1, 2)
391
+ slack = self._pl_slack(Wt, dx_max)
392
+ bad = np.where(slack < 0)[0]
393
+ self.stats['teams_seen'] += G
394
+ self.stats['teams_feasible'] += int(G - len(bad))
395
+ order = bad[np.argsort(-slack[bad])] # least infeasible first
396
+ order = order[:self.max_teams]
397
+ # the projector object lives across instances (n-inits > 1): every GPA_NCALLS-th call is the final one
398
+ ncalls = int(os.environ.get('GPA_NCALLS', '13'))
399
+ final = self.stats['calls'] % ncalls == 0
400
+ saved = (self.team_budget, self.alg1_cap, self.alg2_blocks)
401
+ self.partial = (not final) and os.environ.get('GPA_PARTIAL', '1') == '1'
402
+ self.final_call = final
403
+ if not final:
404
+ self.team_budget = float(os.environ.get('GPA_INTER_BUDGET', '4'))
405
+ self.alg1_cap = int(os.environ.get('GPA_INTER_CAP', '400'))
406
+ self.alg2_blocks = 0
407
+ jobs = [(int(j), np.ascontiguousarray(Wt[:, j].transpose(1, 0, 2).reshape(Hm1 + 1, 2 * na)))
408
+ for j in order]
409
+ self.stats['teams_solved'] += len(jobs)
410
+ if os.environ.get('GPA_BATCH', '0') == '1' and jobs:
411
+ results = _run_teams_batched(self, jobs, dx_max)
412
+ else:
413
+ results = _run_teams(self, jobs, dx_max)
414
+ self.team_budget, self.alg1_cap, self.alg2_blocks = saved
415
+ for j, Wp in results:
416
+ if Wp is None:
417
+ self.stats['teams_failed'] += 1
418
+ continue
419
+ self.stats['teams_ok'] += 1
420
+ Wp = torch.as_tensor(Wp).reshape(Hm1 + 1, na, 2).permute(1, 0, 2) # (na, L+1, 2)
421
+ out[idx[:, j]] = Wp[:, 1:, :].to(DT)
422
+ self.stats['time'] += time.time() - t_call
423
+ if final and os.environ.get('GPA_CERT_LOG'):
424
+ # GlobMP's own certificate for this instance: teams the exact screen already passed plus teams the
425
+ # final solve certified. The returned plan is the first certified candidate.
426
+ cert = sorted(set(int(j) for j in range(G) if j not in set(bad.tolist())) | {int(j) for j, Wp in results if Wp is not None})
427
+ import json as _json
428
+ with open(os.environ['GPA_CERT_LOG'], 'a') as fh:
429
+ fh.write(_json.dumps(dict(call=int(self.stats['calls']), certified=cert)) + '\n')
430
+ if self.verbose:
431
+ print(f" [gpa] call {self.stats['calls']}: {G} teams, {G - len(bad)} already feasible, "
432
+ f"{len(order)} solved, {self.stats['teams_failed']} failed, "
433
+ f"{time.time() - t_call:.1f}s", flush=True)
434
+ return out.to(dev, dt_)
435
+
436
+
437
+ _POOL_SELF = None
438
+ _POOL = None
439
+
440
+
441
+ def _pool_init(proj):
442
+ global _POOL_SELF
443
+ torch.set_num_threads(1)
444
+ _POOL_SELF = proj
445
+
446
+
447
+ def _team_worker(arg):
448
+ j, Wf, dx_max, budget = arg
449
+ if budget is not None:
450
+ _POOL_SELF.team_budget, _POOL_SELF.alg1_cap, _POOL_SELF.alg2_blocks, _POOL_SELF.partial, _POOL_SELF.final_call, _POOL_SELF.prio = budget
451
+ try:
452
+ Wp = _POOL_SELF._solve_team(Wf, dx_max)
453
+ if Wp is not None and getattr(_POOL_SELF, 'final_call', False) and not _POOL_SELF._pl_ok(Wp.numpy(), dx_max):
454
+ Wp = None # the returned plan must also be exact on its waypoint motion
455
+ return j, (None if Wp is None else Wp.numpy())
456
+ except Exception:
457
+ if os.environ.get('GPA_VERBOSE', '0') == '1':
458
+ import traceback; traceback.print_exc()
459
+ return j, None
460
+
461
+
462
+ def _run_teams(proj, jobs, dx_max):
463
+ """Solve one call's teams on a persistent pool of SPAWNED workers (GPA_WORKERS; CPU only). fork is
464
+ unusable here (the sampler has already run autograd, which torch refuses in a forked child); a spawned
465
+ child would re-run the driver script as __mp_main__, so the pool is created with __main__.__file__
466
+ hidden and the workers only import this module."""
467
+ global _POOL, _POOL_SELF
468
+ nw = int(os.environ.get('GPA_WORKERS', '16'))
469
+ budget = (proj.team_budget, proj.alg1_cap, proj.alg2_blocks, getattr(proj, 'partial', False), getattr(proj, 'final_call', False), getattr(proj, 'prio', False))
470
+ if nw <= 1 or len(jobs) <= 1:
471
+ _POOL_SELF = proj
472
+ return [_team_worker((j, W, dx_max, budget)) for j, W in jobs]
473
+ if _POOL is None:
474
+ import multiprocessing as mp, sys as _s
475
+ main = _s.modules.get('__main__'); saved = getattr(main, '__file__', None)
476
+ if saved is not None:
477
+ del main.__file__
478
+ try:
479
+ _POOL = mp.get_context('spawn').Pool(nw, initializer=_pool_init, initargs=(proj,))
480
+ finally:
481
+ if saved is not None:
482
+ main.__file__ = saved
483
+ args = [(j, W, dx_max, budget) for j, W in jobs]
484
+ if getattr(proj, 'prio', False) and os.environ.get('GPA_FIRST_CERT', '1') == '1':
485
+ # the method returns one plan: take the first team that certifies and drop the rest
486
+ out = []
487
+ for j, Wp in _POOL.imap_unordered(_team_worker, args, chunksize=1):
488
+ out.append((j, Wp))
489
+ if Wp is not None:
490
+ _POOL.terminate(); _POOL = None
491
+ break
492
+ return out
493
+ return _POOL.map(_team_worker, args, chunksize=1)
494
+
495
+
496
+ def _run_teams_batched(proj, jobs, dx_max):
497
+ """All teams of one call as ONE batched problem (batch.py); same constraints, steering and
498
+ certified-only final output as the per-team path."""
499
+ from batch import solve_teams
500
+ W = np.stack([proj._keep_right(Wf) for _, Wf in jobs]) # (G, L+1, n)
501
+ dev = os.environ.get('GPA_DEVICE', 'cuda')
502
+ if dev == 'cpu':
503
+ torch.set_num_threads(int(os.environ.get('GPA_BATCH_THREADS', '4')))
504
+ out, ok = solve_teams(W, proj.n_agents, proj.robot_radius, proj.obstacles, proj.ws_bounds, dx_max,
505
+ d=proj.d, M=proj.M, delta=proj.margin, budget=proj.team_budget, cap=proj.alg1_cap,
506
+ alg2_blocks=proj.alg2_blocks, partial=getattr(proj, 'partial', False), device=dev)
507
+ part = getattr(proj, 'partial', False)
508
+ ok = np.asarray(ok, bool).copy()
509
+ if getattr(proj, 'final_call', False):
510
+ for g in np.where(ok)[0]:
511
+ ok[g] = proj._pl_ok(np.asarray(out[g]), dx_max) # exact on the executed waypoint motion too
512
+ res = [(j, (out[g] if (ok[g] or part) else None)) for g, (j, _) in enumerate(jobs)]
513
+ if getattr(proj, 'final_call', False) and os.environ.get('GPA_PRIORITIZED', '0') == '1':
514
+ failed = [(j, Wf) for g, (j, Wf) in enumerate(jobs) if not ok[g]]
515
+ first_only = os.environ.get('GPA_FIRST_CERT', '1') == '1'
516
+ if failed and not (first_only and bool(np.any(ok))):
517
+ # prioritized certification on the CPU pool, fewest-conflict candidates first; with GPA_FIRST_CERT
518
+ # it stops at the first certified team (the returned plan is the first certified candidate)
519
+ na_ = proj.n_agents
520
+ def conflicts(Wf):
521
+ Pw = Wf.reshape(Wf.shape[0], na_, 2); r2 = 2 * proj.robot_radius
522
+ return sum(float(seg_min_dist(Pw[None, :, a], Pw[None, :, b])[0] < r2) for a in range(na_) for b in range(a + 1, na_))
523
+ failed.sort(key=lambda jw: conflicts(jw[1]))
524
+ nw = max(1, int(os.environ.get('GPA_WORKERS', '16')))
525
+ proj.prio = True; pr = {}
526
+ try:
527
+ if first_only:
528
+ pr.update(dict(_run_teams(proj, failed, dx_max)))
529
+ else:
530
+ for k in range(0, len(failed), nw):
531
+ pr.update(dict(_run_teams(proj, failed[k:k + nw], dx_max)))
532
+ finally:
533
+ proj.prio = False
534
+ res = [(j, (pr[j] if (Wp is None and pr.get(j) is not None) else Wp)) for j, Wp in res]
535
+ return res
applications/multi_robot/robotarium/robotarium_baselines/.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ config/paths.env
2
+ runs/
3
+ **/data_trajectories/
4
+ **/data_trained_models/
5
+ **/results/
6
+ **/__pycache__/
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/.gitignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Caches
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+
6
+ # Python packaging
7
+ *.egg-info/
8
+ build/
9
+ dist/
10
+
11
+ # Experiments, logs, and run outputs
12
+ results/
13
+ logs/
14
+ scripts/inference/results/
15
+ scripts/inference/data_trained_models/
16
+
17
+ # Downloaded / generated data
18
+ data_trajectories/
19
+ data_trained_models/
20
+ *.tar.xz
21
+
22
+ # Mujoco
23
+ MUJOCO_LOG.TXT
24
+
25
+ # Notebooks
26
+ *.ipynb
27
+
28
+ # Claude
29
+ .claude/
30
+
31
+ # Slurm
32
+ *.out
33
+ *.err
34
+
35
+ # OS
36
+ .DS_Store
37
+
38
+ # IDE
39
+ .vscode/
40
+ .idea/
41
+
42
+ # Wandb
43
+ wandb/
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/LICENSE ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Julen Urain
4
+ Copyright (c) 2023 Columbia Artificial Intelligence and Robotics Lab
5
+ Copyright (c) 2024 Zeyu Feng and Hao Luan
6
+ Copyright (c) 2024 Yorai Shaoul, Itamar Mishani, and Shivam Vats
7
+ Copyright (c) 2025 Hao Luan and Yi Xian Goh
8
+ Copyright (c) 2025 Wei Xiao
9
+ Copyright (c) 2026 Laboratory for Information and Decision Systems, Azizan Group, MIT
10
+ Copyright (c) 2026 Paolo Giaretta, Zeyang Li, Navid Azizan
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/README.md ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Safe multi-robot motion planning in MMD
2
+
3
+ This folder presents the benchmarks for _DiRecT_, _PCD_, and relevant baselines on the multi-robot motion planning task. The codebase is forked from the [**Projected Coupled Diffusion (PCD)**](https://github.com/EdmundLuan/pcd) [1], which itself extends [MMD](https://github.com/yoraish/mmd) [2]: the planner stack, projection operators, coupling costs, and most of the inference / analysis scripts already exist in the PCD parent. What is contributed here is the **DiRecT** planner on top.
4
+
5
+ ![Multi-robot grid visualization](assets/mrmp-grid.gif)
6
+
7
+ **Table of Contents**
8
+ - [Installation](#installation)
9
+ - [Get Datasets](#get-datasets)
10
+ - [Generating missing Drop Region dataset](#generating-missing-drop-region-dataset)
11
+ - [Planning Inference](#planning-inference)
12
+ - [Analyzing Results](#analyzing-results)
13
+ - [What we modified/added](#what-we-modifiedadded)
14
+ - [Optimizers](#optimizers)
15
+ - [Code structure](#code-structure)
16
+ - [References](#references)
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ Follow the setup steps from the PCD parent ([EdmundLuan/pcd](https://github.com/EdmundLuan/pcd)) as below. In summary:
23
+
24
+ ### Requirements
25
+ - [miniconda](https://docs.conda.io/projects/miniconda/en/latest/index.html)
26
+
27
+ ### Steps
28
+
29
+ 1. Create a conda environment and activate it (the env is named `direct-mmd` in `environment.yml`; this installs Python, PyTorch, JAX, and all pip dependencies):
30
+ ```sh
31
+ conda env create -f environment.yml
32
+ conda activate direct-mmd
33
+ ```
34
+ 2. Install the local (patched) versions of [2, 3]:
35
+ ```sh
36
+ cd deps/torch_robotics && pip install -e . && cd ../..
37
+ cd deps/experiment_launcher && pip install -e . && cd ../..
38
+ cd deps/motion_planning_baselines && pip install -e . && cd ../..
39
+ ```
40
+ 3. Install the `mmd` package:
41
+ ```sh
42
+ pip install -e .
43
+ ```
44
+
45
+ ## Get Datasets
46
+
47
+ Download the sample datasets and trained models provided by the PCD parent:
48
+
49
+ ```sh
50
+ conda activate direct-mmd
51
+
52
+ gdown --id 1Onw0s1pDsMLDfJVOAqmNme4eVVoAkkjz
53
+ tar -xJvf data_trajectories.tar.xz
54
+
55
+ gdown --id 1WO3tpvg-HU0m9RyDvGyfDamo7roBYMud
56
+ tar -xJvf data_trained_models.tar.xz
57
+ ```
58
+
59
+ > If the gdown IDs above have expired, the current dataset locations are tracked in the PCD parent repo: https://github.com/EdmundLuan/pcd.
60
+
61
+ ## Generating missing Drop Region dataset
62
+
63
+ The gdown bundle ships the **DropRegion trained model** but not its **trajectory dataset**. The dataset is still required at inference time — only for the normalizer stats (min/max for `[-1, 1]` scaling), not for retraining.
64
+
65
+ > [!WARNING]
66
+ > To run any DropRegion experiment you must first generate this dataset, otherwise inference will fail to load the normalizer. Before running, edit the SLURM partition, GPU resources, and other system-specific settings in the script to match your cluster. Generation may take a while depending on your hardware.
67
+
68
+ ```sh
69
+ conda activate direct-mmd
70
+ python scripts/generate_data/launch_generate_trajectories.py
71
+ ```
72
+
73
+ The launcher has no CLI; edit `env_id` at the top of the file to switch environments (default `'EnvDropRegion2D'`). Output goes to `logs/<timestamp>/...` — move or symlink it into `data_trajectories/EnvDropRegion2D-RobotPlanarDisk/` to match the layout of the other environments.
74
+
75
+ ---
76
+
77
+ ## Planning Inference
78
+
79
+ All single-agent methods (PCD, DiRecT, baselines) go through `scripts/inference/launch_pcdiff.py`; MMD-CBS uses its own `launch_mmd.py`. Pick a config from `configs/` (`pcdiff_*.yaml` for PCD / baselines, `direct_*.yaml` for DiRecT) and one of the six commands below.
80
+
81
+ Before the first run, create the results directory:
82
+ ```sh
83
+ mkdir -p results/000event_logs
84
+ ```
85
+
86
+ ```sh
87
+ conda activate direct-mmd
88
+
89
+ # Unconstrained — vanilla diffusion, no projection
90
+ python scripts/inference/launch_pcdiff.py --config pcdiff_empty.yaml -g vanilla -p none -m unconstrained
91
+
92
+ # PCD-SHD — coupling guidance + ADMM velocity projection, squared-hinge cost
93
+ python scripts/inference/launch_pcdiff.py --config pcdiff_empty.yaml -g coupling -p max_vel_admm -c hinge_sqr_l2 -r -m pcd_shd
94
+
95
+ # PCD-LB — coupling guidance + ADMM velocity projection, log-barrier cost
96
+ python scripts/inference/launch_pcdiff.py --config pcdiff_empty.yaml -g coupling -p max_vel_admm -c sum_log_l2 -r -m pcd_lb
97
+
98
+ # Final projection — unguided diffusion, project only at the last sampling step
99
+ python scripts/inference/launch_pcdiff.py --config direct_empty.yaml -g final -p scp_admm -m final_projection
100
+
101
+ # DiRecT — unguided diffusion + projection every step (use -p scp_admm or -p ipopt)
102
+ python scripts/inference/launch_pcdiff.py --config direct_empty.yaml -g base -p scp_admm -m direct
103
+
104
+ # MMD-CBS — multi-agent baseline (no CLI; edit MultiAgentPlanningExperimentConfig in the file)
105
+ python scripts/inference/launch_mmd.py
106
+ ```
107
+
108
+ **Optional CLI overrides** (apply to any method above):
109
+ - `-n <int>` — number of initial states (default 100), e.g. `-n 2` for a quick test.
110
+ - `-v <float>` — velocity cap for projection. Repeatable for sweeps, e.g. `-v 0.675 -v 0.703`.
111
+ - `-a <int>` — number of agents (default 4).
112
+
113
+ Swap `pcdiff_empty.yaml` / `direct_empty.yaml` for the `highway`, `conveyor`, or `dropregion` variants to reproduce the other environments. To launch the full sweep across all methods × environments × agent counts in one go, use `python scripts/inference/run_benchmark.py --help`, or one of the shell wrappers in `scripts/inference/` (`run_full_sweep.sh`, `run_single_method.sh`, `run_all_methods.sh`, `sbatch_benchmark.sh`).
114
+
115
+ Results land under `results/<EnvRobot>Random/...` (PCD / DiRecT under `agent___N/method___.../cost___...`, MMD-CBS under `<timestamp>/num_agents___N/planner___CBS/...`); event logs are written to `results/000event_logs/`.
116
+
117
+ ---
118
+
119
+ ## Analyzing Results
120
+
121
+ PCD/DiRecT and MMD-CBS produce different output layouts, so they have separate analyzers:
122
+
123
+ ```sh
124
+ # PCD / DiRecT results (any run produced by launch_pcdiff.py)
125
+ python scripts/analyze/analyzing_pcd.py --config analysis_config.yaml
126
+
127
+ # MMD-CBS results (auto-discover + post-process + analyze)
128
+ python scripts/analyze/run_all_mmd_postprocessing.py
129
+ ```
130
+
131
+ Point the analyzer at the runs you want by editing `log_paths` in `configs/analysis_config.yaml` (PCD/DiRecT) or `configs/analyze_mmd.yaml` (MMD-CBS).
132
+
133
+ ---
134
+
135
+ ## What we modified/added
136
+
137
+ **Inherited from PCD (and from MMD before it):**
138
+ - `mmd/models/projection/` — ADMM-based state-space projection operators.
139
+ - `mmd/coupling_costs/` — pair-wise inter-robot cost functions.
140
+ - `mmd/models/diffusion_models/sample_functions.py::projected_coupling_ddpm_sample_fn` — projection-aware DDPM sampling hook.
141
+ - `mmd/models/diffusion_models/guides.py` — `GuideManager` framework for gradient-based coupling guidance.
142
+ - `mmd/planners/single_agent/` — `MPD`, `MPDEnsemble`, `PCD` planners (upstream MMD + PCD).
143
+ - `mmd/planners/multi_agent/` — `CBS`, `PrioritizedPlanning` multi-agent planners (upstream MMD).
144
+ - `mmd/common/` — conflict bookkeeping (`conflict_conversion.py`, `multi_agent_utils.py`, `constraints.py`, `experiences.py`).
145
+ - `configs/pcdiff_*.yaml`, `launch_pcdiff.py` / `launch_mmd.py`, analysis scripts under `scripts/analyze/`, and data-generation scripts under `scripts/generate_data/`.
146
+
147
+ **Added by this fork (DiRecT):**
148
+ - `mmd/planners/single_agent/direct.py` — new `DiRecT` (unguided diffusion + projection) planner class.
149
+ - `mmd/planners/single_agent/` — new `IpoptProjector` and `JaxScpAdmmProjector` projector classes.
150
+ - `mmd/models/diffusion_models/` — additional sampling-loop wiring to call the new projectors from the `-g base/final` branches.
151
+ - `mmd/config/mmd_params.py` — new entries for the DiRecT hyperparameters.
152
+ - New DiRecT branches in `scripts/inference/launch_pcdiff.py` — the `-g {base, final}` guiders and the `-p {ipopt, scp_admm}` projectors.
153
+ - New configs `configs/direct_{empty,highway,conveyor,dropregion}.yaml`.
154
+ - New benchmark methods `direct` and `final_projection` in `scripts/inference/run_benchmark.py`, plus shell helpers (`run_all_methods.sh`, `run_full_sweep.sh`, `run_single_method.sh`, and SLURM wrappers).
155
+ - Local patches to `deps/torch_robotics/` and other vendored deps for JAX SCP-ADMM and IPOPT projector compatibility.
156
+
157
+ ## Optimizers
158
+
159
+ The planning stack exposes three families of hard-projection optimizers, picked at the command line via `-p, --projector`:
160
+
161
+ - **ADMM velocity projection** *(from the PCD parent)* — implemented in `mmd/models/projection/projectors.py` as `ADMMProjectionOperator` and the GPU-accelerated `ADMMProjectionOperatorCUDAGraph`. These solve the convex problem of projecting a candidate trajectory onto the bounded-velocity set (`‖x_{i+1} − x_i‖₂ ≤ v_max·Δt`) by alternating an x-update (linear solve with cached LU / Cholesky factorization), a z-update (Euclidean L2-ball projection), and a u-update (dual-variable ascent), with primal/dual residual convergence checks. Selected via `-p max_vel_admm`.
162
+ - **JAX Sequential Convex Programming + ADMM** *(DiRecT)* — implemented in `JaxScpAdmmProjector` (`mmd/planners/single_agent/`). It linearizes harder constraints (obstacles / dynamics) via SCP and runs ADMM on each linearized subproblem in JAX, JIT-compiled for batched trajectories. Selected via `-p scp_admm`; this is the default projector for DiRecT and for the `final_projection` method.
163
+ - **IPOPT interior-point** *(DiRecT)* — implemented in `IpoptProjector` as a general-purpose alternative when ADMM-based projectors are not flexible enough for a given constraint set. Selected via `-p ipopt`.
164
+
165
+ ---
166
+
167
+ ## Code structure
168
+
169
+ Only DiRecT-added and implementation-critical files are listed; everything else inherited from PCD/MMD is omitted for brevity.
170
+
171
+ ```
172
+ multi_robot/
173
+ ├── configs/
174
+ │ ├── direct_{empty,highway,conveyor,dropregion}.yaml # NEW: DiRecT configs
175
+ │ └── pcdiff_{empty,highway,conveyor,dropregion}.yaml # PCD / CD configs (inherited)
176
+ ├── mmd/
177
+ │ ├── planners/single_agent/
178
+ │ │ └── direct.py # NEW: DiRecT, IpoptProjector, JaxScpAdmmProjector
179
+ │ ├── models/projection/projectors.py # ADMM projection operators (inherited)
180
+ │ └── coupling_costs/ # inter-robot coupling costs (inherited)
181
+ ├── scripts/
182
+ │ ├── inference/
183
+ │ │ ├── launch_pcdiff.py # main entrypoint (PCD / DiRecT / baselines)
184
+ │ │ ├── launch_mmd.py # MMD-CBS entrypoint (edit-the-file)
185
+ │ │ └── run_benchmark.py # full sweep across methods × envs × #agents
186
+ │ ├── generate_data/launch_generate_trajectories.py # generate DropRegion (or other) datasets
187
+ │ └── analyze/{analyzing_pcd.py, run_all_mmd_postprocessing.py}
188
+ ├── deps/torch_robotics/ # local fork (patched for SCP-ADMM / IPOPT)
189
+ ├── environment.yml # conda env: direct-mmd
190
+ └── setup.py # `pip install -e .` → package `mmd`
191
+ ```
192
+
193
+ ## ✏️ Citation
194
+ If you find our code or paper useful for your research, please consider citing our work:
195
+ ```
196
+ @misc{giaretta2026directsafediffusionbasedplanning,
197
+ title={DiRecT: Safe Diffusion-Based Planning via Receding-Horizon Denoising},
198
+ author={Paolo Giaretta and Zeyang Li and Navid Azizan},
199
+ year={2026},
200
+ eprint={2606.15359},
201
+ archivePrefix={arXiv},
202
+ primaryClass={cs.LG},
203
+ url={https://arxiv.org/abs/2606.15359},
204
+ }
205
+ ```
206
+
207
+ ## References
208
+ - [1] Hao Luan. Projected Coupled Diffusion (PCD) for multi-robot motion planning. Implementation extending MMD. https://github.com/EdmundLuan/pcd
209
+ - [2] Y. Shaoul, I. Mishani, S. Vats, J. Li, M. Likhachev. Multi-Robot Motion Planning with Diffusion Models. ICLR 2025. https://github.com/yoraish/mmd
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/analysis_config.yaml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------
2
+ # ANALYSIS CONFIG
3
+ # -------------------
4
+
5
+ # List of data directories to process. The script will loop through each directory.
6
+ # Add the paths to your own benchmark logs here, e.g.:
7
+ # - "results/000event_logs/benchmark-YYYYMMDD_HHMMSS-my_tag.log"
8
+ log_paths: # []
9
+ # Examples (uncomment / adapt to your runs):
10
+ - "results/000event_logs/eval_runs-260519_152524-unconstrained.log"
11
+ # - "results/000event_logs/benchmark-260423_232958-bench_j1of6_conveyor_v1.21.log"
12
+
13
+
14
+ # Switches for plotting and saving figures/data.
15
+ switches:
16
+ # plot_trajectories: true
17
+ plot_trajectories: false
18
+ plot_velocity_stats: true
19
+ save_plots: true
20
+ save_stats_json: true
21
+
22
+ # General constants and parameters for the analysis.
23
+ constants:
24
+ EPS: 1.0e-6
25
+ VEL_TOL: 5.0e-5 # Tolerance for velocity checks
26
+ ## --------------- Highway -------------------
27
+ # DEFAULT_MAX_VEL: 0.647 # Default maximum velocity threshold (80% of max action)
28
+ # DEFAULT_MAX_VEL: 0.781 # (90% of max action)
29
+ # DEFAULT_MAX_VEL: 0.878 # (95% of max action)
30
+ ## --------------- Empty -------------------
31
+ ##[0.675, 0.692, 0.703]
32
+ # DEFAULT_MAX_VEL: 0.675 # (75% of max action)
33
+ # DEFAULT_MAX_VEL: 0.692 # (90% of max action)
34
+ # DEFAULT_MAX_VEL: 0.703 # (95% of max action)
35
+ ## --------------- Conveyor -------------------
36
+ ## [1.08, 1.46, 1.76]
37
+ # DEFAULT_MAX_VEL: 1.21 # (85% of max action)
38
+ # DEFAULT_MAX_VEL: 1.46 # (90% of max action)
39
+ # DEFAULT_MAX_VEL: 1.76 # (95% of max action)
40
+ ## --------------- DropRegion -------------------
41
+ ## [0.928, 1.13, 1.34]
42
+ DEFAULT_MAX_VEL: 0.928 # (80% of max action)
43
+ # DEFAULT_MAX_VEL: 1.13 # (90% of max action)
44
+ # DEFAULT_MAX_VEL: 1.34 # (95% of max action)
45
+
46
+
47
+ VEL_HIST_QUANTILES: [0.75, 0.8, 0.85, 0.9, 0.95, 0.99]
48
+
49
+ # Plotting-specific parameters.
50
+ plotting:
51
+ # Number of initial states figures to plot
52
+ num_inits_plot: 10
53
+ # Number of trajectory tuples to plot
54
+ num_trj_tups_plot: 64
55
+ # Trajectory plotting settings
56
+ traj_plot_xlim: [-1, 1]
57
+ traj_plot_ylim: [-1, 1]
58
+ traj_plot_h_end: 64
59
+ # Velocity histogram settings
60
+ vel_hist_bins: 100
61
+
62
+ data:
63
+ default_trained_models_dir: "default_trained_models_dir"
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/analyze_mmd.yaml ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -------------------
2
+ # ANALYSIS CONFIG
3
+ # -------------------
4
+
5
+ # List of data directories to process. The script will loop through each directory.
6
+ log_paths:
7
+ ##---------------------- Highway ----------------------
8
+ # - "results/EnvHighways2DRobotPlanarDiskRandom/dummy_logs.log"
9
+ ### -------------- Empty -------------------
10
+ # - "results/EnvEmpty2DRobotPlanarDiskRandom/dummy_logs-04142026_202235.log"
11
+ ### ----------------- Conveyor -------------------
12
+ # - "results/EnvConveyor2DRobotPlanarDiskRandom/dummy_logs-04142026_204123.log"
13
+
14
+ switches:
15
+ plot_trajectories: false
16
+ plot_velocity_stats: false
17
+ save_plots: false
18
+ save_stats_json: true
19
+
20
+ constants:
21
+ EPS: 1.0e-6
22
+ VEL_TOL: 5.0e-5 # Tolerance for velocity checks
23
+ ## --------------- Highway -------------------
24
+ # DEFAULT_MAX_VEL: 0.647 # Default maximum velocity threshold (80% of max action)
25
+ # DEFAULT_MAX_VEL: 0.781 # (90% of max action)
26
+ # DEFAULT_MAX_VEL: 0.878 # (95% of max action)
27
+ ## --------------- Empty -------------------
28
+ ##[0.675, 0.692, 0.703]
29
+ # DEFAULT_MAX_VEL: 0.675 # (75% of max action)
30
+ # DEFAULT_MAX_VEL: 0.692 # (90% of max action)
31
+ # DEFAULT_MAX_VEL: 0.703 # (95% of max action)
32
+ ## --------------- Conveyor -------------------
33
+ ## [1.21, 1.46, 1.76]
34
+ # DEFAULT_MAX_VEL: 1.21 # (85% of max action)
35
+ # DEFAULT_MAX_VEL: 1.46 # (90% of max action)
36
+ DEFAULT_MAX_VEL: 1.76 # (95% of max action)
37
+ ## --------------- DropRegion -------------------
38
+ ## [0.928, 1.13, 1.34]
39
+ # DEFAULT_MAX_VEL: 0.928 # (85% of max action)
40
+ # DEFAULT_MAX_VEL: 1.13 # (90% of max action)
41
+ # DEFAULT_MAX_VEL: 1.34 # (95% of max action)
42
+
43
+ VEL_HIST_QUANTILES: [0.75, 0.8, 0.85, 0.9, 0.95, 0.99]
44
+
45
+ # Plotting-specific parameters.
46
+ plotting:
47
+ # Number of initial states figures to plot
48
+ num_inits_plot: 10
49
+ # Number of trajectory tuples to plot
50
+ num_trj_tups_plot: 64
51
+ # Trajectory plotting settings
52
+ traj_plot_xlim: [-1, 1]
53
+ traj_plot_ylim: [-1, 1]
54
+ traj_plot_h_end: 64
55
+ # Velocity histogram settings
56
+ vel_hist_bins: 100
57
+
58
+ data:
59
+ default_trained_models_dir: "default_trained_models_dir"
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_conveyor.yaml ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ experiment:
2
+ name: "single_tile_conveyor"
3
+ num_agents: 2
4
+ n_inits: 3
5
+ runtime_limit: 60 # seconds
6
+ seed: 18
7
+ time_str: "auto"
8
+ render_animation: true
9
+
10
+ system:
11
+ device: "cuda:0"
12
+ base_dir: "auto"
13
+ results_dir_name: "logs"
14
+ trained_models_dir_name: "data_trained_models"
15
+ log_pth: null
16
+
17
+ robot:
18
+ planar_disk_radius: 0.05
19
+
20
+ environment:
21
+ type: "single_tile"
22
+ global_model_ids: [['EnvConveyor2D-RobotPlanarDisk']]
23
+ agent_skeleton_l: "auto"
24
+ start_goal_config:
25
+ method: "random_in_env"
26
+ env_class_for_random: "EnvConveyor2D"
27
+ margin: 0.2
28
+ obstacle_margin: 0.2
29
+
30
+ planner:
31
+ multi_agent:
32
+ class: "DummyPlanning"
33
+ stagger_start_time_dt: 0
34
+ is_xcbs: false
35
+ is_ecbs: false
36
+ conflict_type_to_constraint_types:
37
+ PointConflict: [ "MultiPointConstraint" ]
38
+
39
+ single_agent:
40
+ class: "DiRecT"
41
+ planner_alg: "base"
42
+ horizon: 64
43
+ trajectory_duration: 5.0
44
+ n_samples: 128
45
+ debug: true
46
+ timeit: false
47
+ use_guide_on_extra_objects_only: false
48
+ n_local_inference_noising_steps: 3
49
+ n_local_inference_denoising_steps: 3
50
+ start_guide_steps_fraction: 1.0
51
+ n_guide_steps: 1
52
+ n_diffusion_steps_without_noise: 0
53
+ cost_func_key: "sum_log_l2"
54
+ cost_func_param:
55
+ margin_coeff: 1.9
56
+ radius_coeff: 6.0
57
+ weight_grad_cost_collision: 0.05
58
+ weight_grad_cost_smoothness: 0.0
59
+ weight_grad_cost_constraints: 0.2
60
+ weight_grad_cost_soft_constraints: 0.0
61
+ factor_num_interpolated_points_for_collision: 0
62
+ projector_type: "JaxScpADMM"
63
+ start_projection_guidance: 0.5 # guidance start
64
+ low_level_choose_path_from_batch_strategy: "least_collisions"
65
+
66
+ projection:
67
+ projector:
68
+ _target_: "ADMMProjectionOperatorCUDAGraph"
69
+ rho: 6.0
70
+ max_iter: 500
71
+ tol: 1.0e-5
72
+ decomp: "lu"
73
+ convergence_check: true
74
+ check_period: 200
75
+ verbose: false
76
+
77
+ project_params:
78
+ vel_max: 0.8
79
+ smoothness_weight: 0.0
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_dropregion.yaml ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ experiment:
2
+ name: "single_tile_dropregion"
3
+ num_agents: 2
4
+ n_inits: 3
5
+ runtime_limit: 60 # seconds
6
+ seed: 18
7
+ time_str: "auto"
8
+ render_animation: true
9
+
10
+ system:
11
+ device: "cuda:0"
12
+ base_dir: "auto"
13
+ results_dir_name: "logs"
14
+ trained_models_dir_name: "data_trained_models"
15
+ log_pth: null
16
+
17
+ robot:
18
+ planar_disk_radius: 0.05
19
+
20
+ environment:
21
+ type: "single_tile"
22
+ global_model_ids: [['EnvDropRegion2D-RobotPlanarDisk']]
23
+ agent_skeleton_l: "auto"
24
+ start_goal_config:
25
+ method: "random_in_env"
26
+ env_class_for_random: "EnvDropRegion2D"
27
+ margin: 0.2
28
+ obstacle_margin: 0.2
29
+
30
+ planner:
31
+ multi_agent:
32
+ class: "DummyPlanning"
33
+ stagger_start_time_dt: 0
34
+ is_xcbs: false
35
+ is_ecbs: false
36
+ conflict_type_to_constraint_types:
37
+ PointConflict: [ "MultiPointConstraint" ]
38
+
39
+ single_agent:
40
+ class: "DiRecT"
41
+ planner_alg: "base"
42
+ horizon: 64
43
+ trajectory_duration: 5.0
44
+ n_samples: 128
45
+ debug: true
46
+ timeit: false
47
+ use_guide_on_extra_objects_only: false
48
+ n_local_inference_noising_steps: 3
49
+ n_local_inference_denoising_steps: 3
50
+ start_guide_steps_fraction: 1.0
51
+ n_guide_steps: 1
52
+ n_diffusion_steps_without_noise: 0
53
+ cost_func_key: "sum_log_l2"
54
+ cost_func_param:
55
+ margin_coeff: 1.9
56
+ radius_coeff: 6.0
57
+ weight_grad_cost_collision: 0.05
58
+ weight_grad_cost_smoothness: 0.0
59
+ weight_grad_cost_constraints: 0.2
60
+ weight_grad_cost_soft_constraints: 0.0
61
+ factor_num_interpolated_points_for_collision: 0
62
+ projector_type: "JaxScpADMM"
63
+ start_projection_guidance: 0.5
64
+ low_level_choose_path_from_batch_strategy: "least_collisions"
65
+
66
+ projection:
67
+ projector:
68
+ _target_: "ADMMProjectionOperatorCUDAGraph"
69
+ rho: 6.0
70
+ max_iter: 500
71
+ tol: 1.0e-5
72
+ decomp: "lu"
73
+ convergence_check: true
74
+ check_period: 200
75
+ verbose: false
76
+
77
+ project_params:
78
+ vel_max: 0.8
79
+ smoothness_weight: 0.0
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_empty.yaml ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ experiment:
3
+ name: "single_tile_empty"
4
+ num_agents: 8
5
+ n_inits: 10
6
+ runtime_limit: 60 # seconds
7
+ seed: 18
8
+ time_str: "auto" # auto timestamp at runtime
9
+ render_animation: true
10
+
11
+ system:
12
+ device: "cuda:0"
13
+ base_dir: "auto" # auto resolve base project directory
14
+ results_dir_name: "logs"
15
+ trained_models_dir_name: "data_trained_models"
16
+ log_pth: null # use default logger path behavior
17
+
18
+ robot:
19
+ planar_disk_radius: 0.05
20
+
21
+ environment:
22
+ type: "single_tile"
23
+ global_model_ids: [['EnvEmpty2D-RobotPlanarDisk']]
24
+ agent_skeleton_l: "auto" # auto-generate skeleton from environment
25
+ start_goal_config:
26
+ method: "random_in_env"
27
+ env_class_for_random: "EnvEmpty2D"
28
+ margin: 0.2
29
+ obstacle_margin: 0.11
30
+
31
+ planner:
32
+ multi_agent:
33
+ class: "DummyPlanning"
34
+ stagger_start_time_dt: 0
35
+ is_xcbs: false
36
+ is_ecbs: false
37
+ conflict_type_to_constraint_types:
38
+ PointConflict: [ "MultiPointConstraint" ]
39
+
40
+ single_agent:
41
+ class: "DiRecT"
42
+ planner_alg: "base"
43
+ horizon: 64
44
+ trajectory_duration: 5.0
45
+ n_samples: 128 # Number of samples **per agent**
46
+ debug: true
47
+ timeit: false # Enable timing for performance measurement
48
+ # --- Guidance and Diffusion Parameters ---
49
+ use_guide_on_extra_objects_only: false
50
+ n_local_inference_noising_steps: 3
51
+ n_local_inference_denoising_steps: 3
52
+ start_guide_steps_fraction: 1.0
53
+ n_guide_steps: 1
54
+ n_diffusion_steps_without_noise: 0
55
+ cost_func_key: "sum_log_l2"
56
+ cost_func_param:
57
+ margin_coeff: 1.9
58
+ radius_coeff: 6.0
59
+ weight_grad_cost_collision: 0.1
60
+ weight_grad_cost_smoothness: 0.0
61
+ weight_grad_cost_constraints: 0.2
62
+ weight_grad_cost_soft_constraints: 0.0
63
+ factor_num_interpolated_points_for_collision: 0
64
+ projector_type: "JaxScpADMM"
65
+ start_projection_guidance: 0.5 # guidance start
66
+ low_level_choose_path_from_batch_strategy: "least_collisions"
67
+
68
+ projection:
69
+ projector:
70
+ _target_: "ADMMProjectionOperatorCUDAGraph"
71
+ rho: 10.0
72
+ max_iter: 500
73
+ tol: 1.0e-5
74
+ decomp: "lu"
75
+ convergence_check: true
76
+ check_period: 200
77
+ verbose: false
78
+
79
+ project_params:
80
+ vel_max: 0.675
81
+ smoothness_weight: 0.0
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_highway.yaml ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ experiment:
2
+ name: "single_tile_highway"
3
+ num_agents: 2
4
+ n_inits: 3
5
+ runtime_limit: 30 # seconds
6
+ seed: 18
7
+ time_str: "auto" # auto timestamp at runtime
8
+ render_animation: true
9
+
10
+ system:
11
+ device: "cuda:0"
12
+ base_dir: "auto" # auto resolve base project directory
13
+ results_dir_name: "logs"
14
+ trained_models_dir_name: "data_trained_models"
15
+ log_pth: null # use default logger path behavior
16
+
17
+ robot:
18
+ planar_disk_radius: 0.05
19
+
20
+ environment:
21
+ type: "single_tile"
22
+ global_model_ids: [['EnvHighways2D-RobotPlanarDisk']]
23
+ agent_skeleton_l: "auto" # auto-generate skeleton from environment
24
+ start_goal_config:
25
+ method: "random_in_env"
26
+ env_class_for_random: "EnvHighways2D"
27
+ margin: 0.2
28
+ obstacle_margin: 0.11
29
+
30
+ planner:
31
+ multi_agent:
32
+ class: "DummyPlanning"
33
+ stagger_start_time_dt: 0
34
+ is_xcbs: false
35
+ is_ecbs: false
36
+ conflict_type_to_constraint_types:
37
+ PointConflict: [ "MultiPointConstraint" ]
38
+
39
+ single_agent:
40
+ class: "DiRecT"
41
+ planner_alg: "base"
42
+ horizon: 64
43
+ trajectory_duration: 5.0
44
+ n_samples: 128
45
+ debug: true
46
+ timeit: false
47
+ use_guide_on_extra_objects_only: false
48
+ n_local_inference_noising_steps: 3
49
+ n_local_inference_denoising_steps: 3
50
+ start_guide_steps_fraction: 1.0
51
+ n_guide_steps: 1
52
+ n_diffusion_steps_without_noise: 0
53
+ cost_func_key: "sum_log_l2"
54
+ cost_func_param:
55
+ margin_coeff: 1.9
56
+ radius_coeff: 6.0
57
+ weight_grad_cost_collision: 0.1
58
+ weight_grad_cost_smoothness: 0.0
59
+ weight_grad_cost_constraints: 0.2
60
+ weight_grad_cost_soft_constraints: 0.0
61
+ factor_num_interpolated_points_for_collision: 0
62
+ projector_type: "JaxScpADMM"
63
+ start_projection_guidance: 0.5 # 0.25 older but overwritte by run_benchmark.py
64
+ low_level_choose_path_from_batch_strategy: "least_collisions"
65
+
66
+ projection:
67
+ projector:
68
+ _target_: "ADMMProjectionOperatorCUDAGraph"
69
+ rho: 6.0
70
+ max_iter: 500
71
+ tol: 1.0e-5
72
+ decomp: "lu"
73
+ convergence_check: true
74
+ check_period: 200
75
+ verbose: false
76
+
77
+ project_params:
78
+ vel_max: 0.8
79
+ smoothness_weight: 0.0
80
+
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_conveyor.yaml ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.yaml
2
+ #
3
+ # This YAML file centralizes all configurations for the multi-robot planning experiment.
4
+ # To run an experiment, execute:
5
+ # python script_name.py --config path/to/this/config.yaml
6
+
7
+ # General experiment settings
8
+ experiment:
9
+ name: "single_tile_conveyor"
10
+ num_agents: 2
11
+ n_inits: 3
12
+ runtime_limit: 60 # in seconds
13
+ seed: 18
14
+ time_str: "auto" # "auto" will generate from current time, or provide a string like "07132025_100000"
15
+ render_animation: true
16
+
17
+ # System and directory settings
18
+ system:
19
+ device: "cuda:0"
20
+ base_dir: "auto" # "auto" resolves to the project root, or provide a full path
21
+ results_dir_name: "logs"
22
+ trained_models_dir_name: "data_trained_models"
23
+ log_pth: null # "auto" resolves to the results directory, or provide a full path
24
+
25
+ # Robot-specific parameters
26
+ robot:
27
+ planar_disk_radius: 0.05
28
+
29
+ # Environment and scenario setup
30
+ environment:
31
+ type: "single_tile"
32
+ global_model_ids: [['EnvConveyor2D-RobotPlanarDisk']]
33
+ agent_skeleton_l: "auto" # auto-generate skeleton from environment
34
+ start_goal_config:
35
+ method: "random_in_env"
36
+ env_class_for_random: "EnvConveyor2D"
37
+ margin: 0.2
38
+ obstacle_margin: 0.2
39
+
40
+ # Planner configurations
41
+ planner:
42
+ multi_agent:
43
+ class: "DummyPlanning"
44
+ stagger_start_time_dt: 0
45
+ is_xcbs: false
46
+ is_ecbs: false
47
+ conflict_type_to_constraint_types:
48
+ PointConflict: [ "MultiPointConstraint" ]
49
+
50
+ # Low-level single-agent trajectory generation planner (the diffusion model)
51
+ single_agent:
52
+ class: "PCD"
53
+ planner_alg: "pcd"
54
+ horizon: 64
55
+ trajectory_duration: 5.0
56
+ n_samples: 128 # Number of samples **per agent**
57
+ debug: true
58
+ timeit: false # Enable timing for performance measurement
59
+ # --- Guidance and Diffusion Parameters ---
60
+ use_guide_on_extra_objects_only: false
61
+ n_local_inference_noising_steps: 3
62
+ n_local_inference_denoising_steps: 3
63
+ start_guide_steps_fraction: 1.0
64
+ n_guide_steps: 1
65
+ n_diffusion_steps_without_noise: 0
66
+ cost_func_key: "sum_log_l2" # Key for the cost function used in guidance
67
+ cost_func_param:
68
+ margin_coeff: 1.9
69
+ radius_coeff: 6.0 # Coefficient for the collision cost radius
70
+ # --- Cost Weights for Guidance ---
71
+ weight_grad_cost_collision: 0.05
72
+ weight_grad_cost_smoothness: 0.0
73
+ weight_grad_cost_constraints: 0.2
74
+ weight_grad_cost_soft_constraints: 0.0
75
+ factor_num_interpolated_points_for_collision: 0
76
+ low_level_choose_path_from_batch_strategy: "least_collisions"
77
+
78
+ projection:
79
+ projector:
80
+ _target_: "ADMMProjectionOperatorCUDAGraph"
81
+ rho: 6.0
82
+ max_iter: 500
83
+ tol: 1.0e-5
84
+ decomp: "lu"
85
+ convergence_check: true
86
+ check_period: 200
87
+ verbose: false
88
+
89
+ project_params:
90
+ vel_max: 0.8
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_dropregion.yaml ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ experiment:
2
+ name: "single_tile_dropregion"
3
+ num_agents: 2
4
+ n_inits: 3
5
+ runtime_limit: 60 # seconds
6
+ seed: 18
7
+ time_str: "auto" # auto timestamp at runtime
8
+ render_animation: true
9
+
10
+ system:
11
+ device: "cuda:0"
12
+ base_dir: "auto" # auto resolve base project directory
13
+ results_dir_name: "logs"
14
+ trained_models_dir_name: "data_trained_models"
15
+ log_pth: null # use default logger path behavior
16
+
17
+ robot:
18
+ planar_disk_radius: 0.05
19
+
20
+ environment:
21
+ type: "single_tile"
22
+ global_model_ids: [['EnvDropRegion2D-RobotPlanarDisk']]
23
+ agent_skeleton_l: "auto" # auto-generate skeleton from environment
24
+ start_goal_config:
25
+ method: "random_in_env"
26
+ env_class_for_random: "EnvDropRegion2D"
27
+ margin: 0.2
28
+ obstacle_margin: 0.2
29
+
30
+ planner:
31
+ multi_agent:
32
+ class: "DummyPlanning"
33
+ stagger_start_time_dt: 0
34
+ is_xcbs: false
35
+ is_ecbs: false
36
+ conflict_type_to_constraint_types:
37
+ PointConflict: [ "MultiPointConstraint" ]
38
+
39
+ single_agent:
40
+ class: "PCD"
41
+ planner_alg: "pcd"
42
+ horizon: 64
43
+ trajectory_duration: 5.0
44
+ n_samples: 128
45
+ debug: true
46
+ timeit: false
47
+ use_guide_on_extra_objects_only: false
48
+ n_local_inference_noising_steps: 3
49
+ n_local_inference_denoising_steps: 3
50
+ start_guide_steps_fraction: 1.0
51
+ n_guide_steps: 1
52
+ n_diffusion_steps_without_noise: 0
53
+ cost_func_key: "sum_log_l2"
54
+ cost_func_param:
55
+ margin_coeff: 1.9
56
+ radius_coeff: 6.0
57
+ weight_grad_cost_collision: 0.05
58
+ weight_grad_cost_smoothness: 0.0
59
+ weight_grad_cost_constraints: 0.2
60
+ weight_grad_cost_soft_constraints: 0.0
61
+ factor_num_interpolated_points_for_collision: 0
62
+ low_level_choose_path_from_batch_strategy: "least_collisions"
63
+
64
+ projection:
65
+ projector:
66
+ _target_: "ADMMProjectionOperatorCUDAGraph"
67
+ rho: 6.0
68
+ max_iter: 500
69
+ tol: 1.0e-5
70
+ decomp: "lu"
71
+ convergence_check: true
72
+ check_period: 200
73
+ verbose: false
74
+
75
+ project_params:
76
+ vel_max: 0.8
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_empty.yaml ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ experiment:
2
+ name: "single_tile_empty"
3
+ num_agents: 8
4
+ n_inits: 10
5
+ runtime_limit: 60 # seconds
6
+ seed: 18
7
+ time_str: "auto" # auto timestamp at runtime
8
+ render_animation: true
9
+
10
+ system:
11
+ device: "cuda:0"
12
+ base_dir: "auto" # auto resolve base project directory
13
+ results_dir_name: "logs"
14
+ trained_models_dir_name: "data_trained_models"
15
+ log_pth: null # use default logger path behavior
16
+
17
+ robot:
18
+ planar_disk_radius: 0.05
19
+
20
+ environment:
21
+ type: "single_tile"
22
+ global_model_ids: [['EnvEmpty2D-RobotPlanarDisk']]
23
+ agent_skeleton_l: "auto" # auto-generate skeleton from environment
24
+ start_goal_config:
25
+ method: "random_in_env"
26
+ env_class_for_random: "EnvEmpty2D"
27
+ margin: 0.2
28
+ obstacle_margin: 0.11
29
+
30
+ planner:
31
+ multi_agent:
32
+ class: "DummyPlanning"
33
+ stagger_start_time_dt: 0
34
+ is_xcbs: false
35
+ is_ecbs: false
36
+ conflict_type_to_constraint_types:
37
+ PointConflict: [ "MultiPointConstraint" ]
38
+
39
+ single_agent:
40
+ class: "PCD"
41
+ planner_alg: "pcd"
42
+ horizon: 64
43
+ trajectory_duration: 5.0
44
+ n_samples: 128 # Number of samples **per agent**
45
+ debug: true
46
+ timeit: false # Enable timing for performance measurement
47
+ # --- Guidance and Diffusion Parameters ---
48
+ use_guide_on_extra_objects_only: false
49
+ n_local_inference_noising_steps: 3
50
+ n_local_inference_denoising_steps: 3
51
+ start_guide_steps_fraction: 1.0
52
+ n_guide_steps: 1
53
+ n_diffusion_steps_without_noise: 0
54
+ cost_func_key: "sum_log_l2"
55
+ cost_func_param:
56
+ margin_coeff: 1.9
57
+ radius_coeff: 6.0
58
+ weight_grad_cost_collision: 0.1
59
+ weight_grad_cost_smoothness: 0.0
60
+ weight_grad_cost_constraints: 0.2
61
+ weight_grad_cost_soft_constraints: 0.0
62
+ factor_num_interpolated_points_for_collision: 0
63
+ low_level_choose_path_from_batch_strategy: "least_collisions"
64
+
65
+ projection:
66
+ projector:
67
+ _target_: "ADMMProjectionOperatorCUDAGraph"
68
+ rho: 10.0
69
+ max_iter: 500
70
+ tol: 1.0e-5
71
+ decomp: "lu"
72
+ convergence_check: true
73
+ check_period: 200
74
+ verbose: false
75
+
76
+ project_params:
77
+ vel_max: 0.675
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_highway.yaml ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ experiment:
2
+ name: "single_tile_highway"
3
+ num_agents: 2
4
+ n_inits: 3
5
+ runtime_limit: 30 # seconds
6
+ seed: 18
7
+ time_str: "auto" # auto timestamp at runtime
8
+ render_animation: true
9
+
10
+ system:
11
+ device: "cuda:0"
12
+ base_dir: "auto" # auto resolve base project directory
13
+ results_dir_name: "logs"
14
+ trained_models_dir_name: "data_trained_models"
15
+ log_pth: null # use default logger path behavior
16
+
17
+ robot:
18
+ planar_disk_radius: 0.05
19
+
20
+ environment:
21
+ type: "single_tile"
22
+ global_model_ids: [['EnvHighways2D-RobotPlanarDisk']]
23
+ agent_skeleton_l: "auto" # auto-generate skeleton from environment
24
+ start_goal_config:
25
+ method: "random_in_env"
26
+ env_class_for_random: "EnvHighways2D"
27
+ margin: 0.2
28
+ obstacle_margin: 0.11
29
+
30
+ planner:
31
+ multi_agent:
32
+ class: "DummyPlanning"
33
+ stagger_start_time_dt: 0
34
+ is_xcbs: false
35
+ is_ecbs: false
36
+ conflict_type_to_constraint_types:
37
+ PointConflict: [ "MultiPointConstraint" ]
38
+
39
+ single_agent:
40
+ class: "PCD"
41
+ planner_alg: "pcd"
42
+ horizon: 64
43
+ trajectory_duration: 5.0
44
+ n_samples: 128
45
+ debug: true
46
+ timeit: false
47
+ use_guide_on_extra_objects_only: false
48
+ n_local_inference_noising_steps: 3
49
+ n_local_inference_denoising_steps: 3
50
+ start_guide_steps_fraction: 1.0
51
+ n_guide_steps: 1
52
+ n_diffusion_steps_without_noise: 0
53
+ cost_func_key: "sum_log_l2"
54
+ cost_func_param:
55
+ margin_coeff: 1.9
56
+ radius_coeff: 6.0
57
+ weight_grad_cost_collision: 0.1
58
+ weight_grad_cost_smoothness: 0.0
59
+ weight_grad_cost_constraints: 0.2
60
+ weight_grad_cost_soft_constraints: 0.0
61
+ factor_num_interpolated_points_for_collision: 0
62
+ low_level_choose_path_from_batch_strategy: "least_collisions"
63
+
64
+ projection:
65
+ projector:
66
+ _target_: "ADMMProjectionOperatorCUDAGraph"
67
+ rho: 6.0
68
+ max_iter: 500
69
+ tol: 1.0e-5
70
+ decomp: "lu"
71
+ convergence_check: true
72
+ check_period: 200
73
+ verbose: false
74
+
75
+ project_params:
76
+ vel_max: 0.8
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Common
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/MANIFEST.in ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ include requirements.txt
2
+ include setup.py
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/Makefile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package:
2
+ python setup.py sdist
3
+
4
+ install: package
5
+ pip install $(shell ls dist/*.tar.gz)
6
+
7
+ all: clean install
8
+
9
+ upload:
10
+ python -m twine upload dist/*
11
+
12
+ clean:
13
+ rm -rf dist
14
+ rm -rf build
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/README.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Experiment Launcher
2
+
3
+ Launch experiments locally or on a cluster running SLURM in a single file.
4
+
5
+ ## Description
6
+
7
+ The ``experiment_launcher`` package provides a simple way to run multiple experiments using SLURM or Joblib with minimum
8
+ effort - you just have to set the `local` parameter to `True` to run locally,
9
+ or to `False` to run with a cluster using SLURM.
10
+
11
+ It is particularly useful to run multiple seeds and/or test multiple configurations of hyperparameters such as learning rates, batch size.
12
+
13
+ ## Installation
14
+
15
+ You can do a minimal installation of ``experiment_launcher`` with:
16
+ ```
17
+ pip install -e .
18
+ ```
19
+
20
+ ## How to Use
21
+
22
+ ### Basic Usage
23
+
24
+ The best way to understand experiment launcher is to look at the basic example in [examples/basic](examples/basic).
25
+
26
+ - The file [examples/basic/test.py](examples/basic/test.py) contains a single experiment
27
+ - The file [examples/basic/launch_test.py](examples/basic/launch_test.py) contains the launcher file configurations and call
28
+
29
+ **Single experiment**
30
+ - [examples/basic/test.py](examples/basic/test.py) consists of:
31
+ - The function `experiment` is the entry point of your experiment
32
+ - It takes as arguments your experiment settings (e.g., the number of layers in a neural
33
+ network, the learning rate, ...)
34
+ - The arguments need to be assigned a type and default value in the function definition
35
+ - Current accepted types are `int`, `float`, `str`, `bool`, `list`
36
+ - The arguments `seed` and `results_dir` **must** always be included
37
+ - Python kwargs can also be added as `**kwargs` (accepted types are the same as above)
38
+ - This function **must** be decorated with a decorator, e.g. `@single_experiment`.
39
+ This will take care of creating proper results directories.
40
+
41
+ - The `if __name__ == '__main__'` block
42
+ - This **must** contain one single line: `run_experiment(experiment)`
43
+ - You can test your code by running
44
+ ```bash
45
+ cd examples/basic
46
+ python test.py
47
+ ```
48
+
49
+ **Launch file**
50
+ - [examples/basic/launch_test.py](examples/basic/launch_test.py) consists of:
51
+ - Creating an instance of the `Launcher` object, that contains the SLURM or Joblib (if run locally) parameters. These are some of the important parameters. For more consult the class definition.
52
+ - `exp_name` is the experiment name, under which a results directory will be created
53
+ - `exp_file` is the path to the python file where the `experiment` is implemented (without the extension `.py`)
54
+ - `project_name` is the project name in the cluster
55
+ - `n_seeds` is the number of random seeds for each single experiment configuration
56
+ - `n_exps_in_parallel` is the number of experiments to be run in parallel. This is useful to run multiple jobs in a single GPU in the cluster
57
+ - `n_cores` is the number of cores for each experiment.
58
+ Note that if `n_exps_in_parallel > 1`, then `n_exps_in_parallel` jobs will share `n_cores`.
59
+ This parameter does not apply if running locally. For that you should use the linux comamnd [taskset](https://man7.org/linux/man-pages/man1/taskset.1.html)
60
+ - `memory_per_core` is amount of memory in MB requested for each core in SLURM. If you specify this too low, SLURM might crash.
61
+ - `partition` is the SLURM partition, which is cluster dependent
62
+ - `gres` are special resources asked for a SLURM experiment
63
+ - `conda_env` if you are using a conda environment, specify its name here
64
+ - Adding experiments with `launcher.add_experiment`
65
+ - Use `launcher.add_experiment` to create an experiment for a particular configuration (e.g., different learning rates)
66
+ - Parameters that are followed by _**two trailing underscores**_ will be used to create experiment directories
67
+ - Results directories are going to be created by default as, e.g. with `param__` and `param2__`
68
+ - `./logs/exp_name_DATE/param1__value/param2__value/SEED/`
69
+ - **Be careful** when specifying the parameters and double underscores.
70
+ The launcher will throw an error if two experiments lead to the same results directory (which would be overwritten).
71
+ - To see this problem, uncomment the line `# dict(floating_arg=11.0, string_arg='test')` in [examples/basic/launch_test.py](examples/basic/launch_test.py)
72
+ - Parameters will with trailing underscores have priority over their counterparts without underscores. E.g., `env='some_env'` will be overwritten by `env__='my_env'`
73
+ - Running the experiments with `launcher.run(LOCAL)`
74
+ - This runs your experiment either locally (LOCAL: `True`) or in the cluster (LOCAL: `False`)
75
+
76
+
77
+ **Running the experiment**
78
+ - To run the launcher simply call
79
+ ```bash
80
+ cd examples/basic
81
+ python launch_test.py
82
+ ```
83
+ - Log files will be placed in
84
+ - `./logs` if running locally or in the IAS cluster
85
+ - `/work/scratch/$USERNAME` if ran in the `Lichtenberg-Hochleistungsrechner of the TU Darmstadt`
86
+
87
+
88
+ ### Integration with Weights and Biases
89
+
90
+ The experiment launcher provides an easy way to integrate with Weights and Biases.
91
+ - In the experiment file add
92
+ - `**kwargs` in the `experiment` function definition
93
+ - In the launcher file, create wandb options and pass them to the `launcher.add_experiment()`
94
+ ```python
95
+ wandb_options = dict(
96
+ wandb_enabled=False, # If True, runs and logs to wandb.
97
+ wandb_entity='joaocorreiacarvalho',
98
+ wandb_project='experiment_launcher_test',
99
+ wandb_group='group_test'
100
+ )
101
+ ```
102
+
103
+
104
+ ### Running experiments with parameters in configuration files
105
+ - If you have many parameters that you need to change, a good idea can be to use configuration files.
106
+ - If you want to specify your parameters in a configuration file, look at the example under `examples/config_files`
107
+
108
+
109
+ ## Notes
110
+ - For reproducibility, the seeds are created sequentially from `0` to `n_exps-1`.
111
+
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/basic/launch_test.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from itertools import product
2
+
3
+ from experiment_launcher import Launcher, is_local
4
+
5
+ LOCAL = is_local()
6
+ TEST = False
7
+ USE_CUDA = False
8
+
9
+ N_SEEDS = 3
10
+
11
+ if LOCAL:
12
+ N_EXPS_IN_PARALLEL = 5
13
+ else:
14
+ N_EXPS_IN_PARALLEL = 3
15
+
16
+ N_CORES = N_EXPS_IN_PARALLEL
17
+ MEMORY_SINGLE_JOB = 1000
18
+ MEMORY_PER_CORE = N_EXPS_IN_PARALLEL * MEMORY_SINGLE_JOB // N_CORES
19
+ PARTITION = 'amd2,amd' # 'amd', 'rtx'
20
+ GRES = 'gpu:1' if USE_CUDA else None # gpu:rtx2080:1, gpu:rtx3080:1
21
+ CONDA_ENV = 'el' # None
22
+
23
+ launcher = Launcher(
24
+ exp_name='test_launcher',
25
+ exp_file='test',
26
+ # project_name='project01234', # for hrz cluster
27
+ n_seeds=N_SEEDS,
28
+ n_exps_in_parallel=N_EXPS_IN_PARALLEL,
29
+ n_cores=N_CORES,
30
+ memory_per_core=MEMORY_PER_CORE,
31
+ days=2,
32
+ hours=23,
33
+ minutes=59,
34
+ seconds=0,
35
+ partition=PARTITION,
36
+ gres=GRES,
37
+ conda_env=CONDA_ENV,
38
+ use_timestamp=True,
39
+ compact_dirs=False
40
+ )
41
+
42
+ envs = {
43
+ 'env_00': {'env_param': 'aa'},
44
+ 'env_01': {'env_param': 'bb'}
45
+ }
46
+ a_l = [1, 2, 3]
47
+ boolean_param_l = [True, False]
48
+ some_default_param = 'b'
49
+
50
+ # These arguments are kwargs of the experiment function
51
+ unknown_args_list = [
52
+ dict(integer_arg=10),
53
+ # dict(floating_arg=11.0, string_arg='test')
54
+ ]
55
+
56
+ for env in envs:
57
+ d = envs[env]
58
+ for a, boolean_param in product(a_l, boolean_param_l):
59
+ for unknown_args in unknown_args_list:
60
+ launcher.add_experiment(
61
+ # A subdirectory will be created for parameters with a trailing double underscore.
62
+ env__=env,
63
+ a__=a,
64
+ boolean_param__=boolean_param,
65
+
66
+ env='some_env', # This value will be overwritten by env__
67
+
68
+ **d,
69
+ some_default_param=some_default_param,
70
+
71
+ **unknown_args,
72
+ debug=False,
73
+ )
74
+
75
+ launcher.run(LOCAL, TEST)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/basic/test.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import wandb
4
+
5
+ from experiment_launcher import run_experiment, single_experiment
6
+
7
+
8
+ # This decorator creates results_dir as results_dir/seed, and saves the experiment arguments into a file.
9
+ @single_experiment
10
+ def experiment(
11
+ #######################################
12
+ env: str = 'env', # You need to specify the argument type if you use the automatic parser.
13
+ env_param: str = 'aa',
14
+ a: int = 1,
15
+ boolean_param: bool = True,
16
+ some_default_param: str = 'b',
17
+
18
+ debug: bool = True,
19
+
20
+ #######################################
21
+ # MANDATORY
22
+ seed: int = 0,
23
+ results_dir: str = 'logs',
24
+
25
+ #######################################
26
+ # OPTIONAL
27
+ # accept unknown arguments
28
+ **kwargs
29
+ ):
30
+ # EXPERIMENT
31
+ print(f'DEBUG MODE: {debug}')
32
+
33
+ print(f'kwargs: {kwargs}')
34
+
35
+ filename = os.path.join(results_dir, 'log_' + str(seed) + '.txt')
36
+ out_str = f'Running experiment with seed {seed}, env {env}, ' \
37
+ f'env_param {env_param}, a {a}, ' \
38
+ f'boolean_param {boolean_param}, some_default_param {some_default_param}'
39
+ print(out_str)
40
+ with open(filename, 'w') as file:
41
+ file.write('Some logs in a log file.\n')
42
+ file.write(out_str)
43
+
44
+
45
+ if __name__ == '__main__':
46
+ # Leave unchanged
47
+ run_experiment(experiment)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/configs/config00.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ network: mynetwork
2
+ layers: 3
3
+ activations:
4
+ - relu
5
+ - relu
6
+ - tanh
7
+ lr: 1e-3
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/configs/config01.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ network: myothernetwork
2
+ layers: 3
3
+ activations:
4
+ - relu
5
+ - relu
6
+ - tanh
7
+ lr: 1e-3
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/launch_test.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from experiment_launcher import Launcher, is_local
2
+
3
+ LOCAL = is_local()
4
+ TEST = False
5
+ USE_CUDA = False
6
+
7
+ N_SEEDS = 3
8
+
9
+ if LOCAL:
10
+ N_EXPS_IN_PARALLEL = 5
11
+ else:
12
+ N_EXPS_IN_PARALLEL = 3
13
+
14
+ N_CORES = N_EXPS_IN_PARALLEL
15
+ MEMORY_SINGLE_JOB = 1000
16
+ MEMORY_PER_CORE = N_EXPS_IN_PARALLEL * MEMORY_SINGLE_JOB // N_CORES
17
+ PARTITION = 'amd2,amd' # 'amd', 'rtx'
18
+ GRES = 'gpu:1' if USE_CUDA else None # gpu:rtx2080:1, gpu:rtx3080:1
19
+ CONDA_ENV = 'el' # None
20
+
21
+ launcher = Launcher(
22
+ exp_name='test_launcher',
23
+ exp_file='test',
24
+ # project_name='project01234', # for hrz cluster
25
+ n_seeds=N_SEEDS,
26
+ n_exps_in_parallel=N_EXPS_IN_PARALLEL,
27
+ n_cores=N_CORES,
28
+ memory_per_core=MEMORY_PER_CORE,
29
+ days=2,
30
+ hours=23,
31
+ minutes=59,
32
+ seconds=0,
33
+ partition=PARTITION,
34
+ conda_env=CONDA_ENV,
35
+ gres=GRES,
36
+ use_timestamp=True,
37
+ compact_dirs=False
38
+ )
39
+
40
+ config_files_l = [
41
+ 'configs/config00.yaml',
42
+ 'configs/config01.yaml',
43
+ ]
44
+
45
+ # Optional arguments for Weights and Biases
46
+ wandb_options = dict(
47
+ wandb_mode='disabled', # "online", "offline" or "disabled"
48
+ wandb_entity='joaocorreiacarvalho',
49
+ wandb_project='test_experiment_launcher_config_files'
50
+ )
51
+
52
+ for i, config_file in enumerate(config_files_l):
53
+ launcher.add_experiment(
54
+ # A subdirectory will be created for parameters with a trailing double underscore.
55
+ config__=f'config-{str(i).zfill(len(config_files_l))}',
56
+
57
+ config_file_path=config_file,
58
+
59
+ debug=False,
60
+
61
+ **wandb_options,
62
+ wandb_group=f'test_group-el-{config_file}'
63
+ )
64
+
65
+ launcher.run(LOCAL, TEST)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/test.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import wandb
4
+ import yaml
5
+
6
+ from experiment_launcher import run_experiment, single_experiment_yaml
7
+
8
+
9
+ # This decorator creates results_dir as results_dir/seed, and saves the experiment arguments into a file.
10
+ @single_experiment_yaml
11
+ def experiment(
12
+ #######################################
13
+ config_file_path: str = './configs/config00.yaml',
14
+
15
+ some_default_param: str = 'b',
16
+
17
+ debug: bool = True,
18
+
19
+ #######################################
20
+ # MANDATORY
21
+ seed: int = 0,
22
+ results_dir: str = 'logs',
23
+
24
+ #######################################
25
+ # OPTIONAL
26
+ # accept unknown arguments
27
+ **kwargs
28
+ ):
29
+ # EXPERIMENT
30
+ print(f'DEBUG MODE: {debug}')
31
+
32
+ with open(config_file_path, 'r') as f:
33
+ configs = yaml.load(f, yaml.Loader)
34
+
35
+ print('Config file content:')
36
+ print(configs)
37
+
38
+ filename = os.path.join(results_dir, 'log_' + str(seed) + '.txt')
39
+ out_str = f'Running experiment with seed {seed}'
40
+ with open(filename, 'w') as file:
41
+ file.write('Some logs in a log file.\n')
42
+ file.write(out_str)
43
+
44
+ wandb.log({'seed': seed}, step=1)
45
+
46
+
47
+ if __name__ == '__main__':
48
+ # Leave unchanged
49
+ run_experiment(experiment)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/multi_exp_same_gpu/launch_test.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from experiment_launcher import Launcher, is_local
2
+
3
+ LOCAL = is_local()
4
+ TEST = False
5
+ USE_CUDA = True
6
+
7
+ N_SEEDS = 10
8
+
9
+ if LOCAL:
10
+ N_EXPS_IN_PARALLEL = 2
11
+ else:
12
+ N_EXPS_IN_PARALLEL = 3
13
+
14
+ N_CORES = N_EXPS_IN_PARALLEL
15
+ MEMORY_SINGLE_JOB = 1000
16
+ MEMORY_PER_CORE = N_EXPS_IN_PARALLEL * MEMORY_SINGLE_JOB // N_CORES
17
+ PARTITION = 'rtx2,rtx' if USE_CUDA else 'amd2,amd'
18
+ GRES = 'gpu:1' if USE_CUDA else None # gpu:rtx2080:1, gpu:rtx3080:1
19
+ CONDA_ENV = 'el' # None
20
+
21
+ launcher = Launcher(
22
+ exp_name='test_launcher',
23
+ exp_file='test',
24
+ # project_name='project01234', # for hrz cluster
25
+ n_seeds=N_SEEDS,
26
+ n_exps_in_parallel=N_EXPS_IN_PARALLEL,
27
+ n_cores=N_CORES,
28
+ memory_per_core=MEMORY_PER_CORE,
29
+ days=2,
30
+ hours=23,
31
+ minutes=59,
32
+ seconds=0,
33
+ partition=PARTITION,
34
+ conda_env=CONDA_ENV,
35
+ gres=GRES,
36
+ use_timestamp=True,
37
+ compact_dirs=False
38
+ )
39
+
40
+ tensor_sizes_l = [1000000 + i for i in range(5)]
41
+
42
+ # Optional arguments for Weights and Biases
43
+ wandb_options = dict(
44
+ wandb_mode='disabled', # "online", "offline" or "disabled"
45
+ wandb_entity='joaocorreiacarvalho',
46
+ wandb_project='test_experiment_launcher_config_files'
47
+ )
48
+
49
+ for tensor_size in tensor_sizes_l:
50
+ launcher.add_experiment(
51
+ # A subdirectory will be created for parameters with a trailing double underscore.
52
+ tensor_size__=tensor_size,
53
+
54
+ debug=False,
55
+
56
+ **wandb_options,
57
+ wandb_group=f'test_group-el-{tensor_size}',
58
+ )
59
+
60
+ launcher.run(LOCAL, TEST)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/multi_exp_same_gpu/test.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+
4
+ import torch.cuda
5
+ import wandb
6
+
7
+ from experiment_launcher import run_experiment, single_experiment_yaml
8
+
9
+
10
+ # This decorator creates results_dir as results_dir/seed, and saves the experiment arguments into a file.
11
+ @single_experiment_yaml
12
+ def experiment(
13
+ #######################################
14
+ env: str = 'env-name', # You need to specify the argument type if you use the automatic parser.
15
+ tensor_size: int = 10000,
16
+
17
+ debug: bool = True,
18
+
19
+ #######################################
20
+ # MANDATORY
21
+ seed: int = 0,
22
+ results_dir: str = 'logs',
23
+
24
+ #######################################
25
+ # OPTIONAL
26
+ # accept unknown arguments
27
+ **kwargs
28
+ ):
29
+ # EXPERIMENT
30
+ print(f'DEBUG MODE: {debug}')
31
+
32
+ tensor1 = torch.ones(tensor_size, device='cuda' if torch.cuda.is_available() else 'cpu')
33
+ tensor2 = tensor1.clone()
34
+
35
+ print(f'Env: {env}')
36
+ print(f'Tensor shape: {tensor1.shape}')
37
+
38
+ filename = os.path.join(results_dir, 'log_' + str(seed) + '.txt')
39
+ out_str = f'Running experiment with seed {seed}'
40
+ with open(filename, 'w') as file:
41
+ file.write('Some logs in a log file.\n')
42
+ file.write(out_str)
43
+
44
+ wandb.log({'seed': seed}, step=1)
45
+
46
+ end_time = time.time() + 0.01
47
+ while time.time() < end_time:
48
+ tensor = tensor1 * tensor2
49
+ print('Finished experiment.')
50
+
51
+
52
+ if __name__ == '__main__':
53
+ # Leave unchanged
54
+ run_experiment(experiment)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/start_seed/launch_test.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from itertools import product
3
+
4
+ from experiment_launcher import Launcher, is_local
5
+
6
+ LOCAL = is_local()
7
+ TEST = False
8
+ USE_CUDA = False
9
+
10
+ N_SEEDS = 103
11
+
12
+ if LOCAL:
13
+ N_EXPS_IN_PARALLEL = 5
14
+ else:
15
+ N_EXPS_IN_PARALLEL = 3
16
+
17
+ N_CORES = N_EXPS_IN_PARALLEL
18
+ MEMORY_SINGLE_JOB = 1000
19
+ MEMORY_PER_CORE = N_EXPS_IN_PARALLEL * MEMORY_SINGLE_JOB // N_CORES
20
+ PARTITION = 'amd2,amd' # 'amd', 'rtx'
21
+ GRES = 'gpu:1' if USE_CUDA else None # gpu:rtx2080:1, gpu:rtx3080:1
22
+ CONDA_ENV = 'el' # None
23
+
24
+
25
+ # E.g. SLURM only allows arrays with N_SEEDS_MAX_SLURM size
26
+ N_SEEDS_MAX_SLURM = 100
27
+ start_seed = 0
28
+ if LOCAL:
29
+ n_seeds_split = [N_SEEDS]
30
+ else:
31
+ if N_SEEDS <= N_SEEDS_MAX_SLURM:
32
+ n_seeds_split = [N_SEEDS]
33
+ elif N_SEEDS % N_SEEDS_MAX_SLURM == 0:
34
+ n_seeds_split = [N_SEEDS_MAX_SLURM for _ in range(N_SEEDS // N_SEEDS_MAX_SLURM)]
35
+ else:
36
+ n_seeds_split = [N_SEEDS_MAX_SLURM for _ in range(N_SEEDS // N_SEEDS_MAX_SLURM)] + [N_SEEDS % N_SEEDS_MAX_SLURM]
37
+
38
+ for _n_seeds in n_seeds_split:
39
+ print(f"---------> Running seeds {start_seed}-{start_seed + _n_seeds}")
40
+
41
+ time.sleep(1)
42
+ launcher = Launcher(
43
+ exp_name='test_launcher',
44
+ exp_file='test',
45
+ # project_name='project01234', # for hrz cluster
46
+ n_seeds=_n_seeds,
47
+ start_seed=start_seed,
48
+ n_exps_in_parallel=N_EXPS_IN_PARALLEL,
49
+ n_cores=N_CORES,
50
+ memory_per_core=MEMORY_PER_CORE,
51
+ days=2,
52
+ hours=23,
53
+ minutes=59,
54
+ seconds=0,
55
+ partition=PARTITION,
56
+ gres=GRES,
57
+ conda_env=CONDA_ENV,
58
+ use_timestamp=True,
59
+ compact_dirs=False
60
+ )
61
+
62
+ envs = {
63
+ 'env_00': {'env_param': 'aa'},
64
+ 'env_01': {'env_param': 'bb'}
65
+ }
66
+ a_l = [1, 2, 3]
67
+ boolean_param_l = [True, False]
68
+ some_default_param = 'b'
69
+
70
+ # These arguments are kwargs of the experiment function
71
+ unknown_args_list = [
72
+ dict(integer_arg=10),
73
+ # dict(floating_arg=11.0, string_arg='test')
74
+ ]
75
+
76
+ for env in envs:
77
+ d = envs[env]
78
+ for a, boolean_param in product(a_l, boolean_param_l):
79
+ for unknown_args in unknown_args_list:
80
+ launcher.add_experiment(
81
+ # A subdirectory will be created for parameters with a trailing double underscore.
82
+ env__=env,
83
+ a__=a,
84
+ boolean_param__=boolean_param,
85
+
86
+ env='some_env', # This value will be overwritten by env__
87
+
88
+ **d,
89
+ some_default_param=some_default_param,
90
+
91
+ **unknown_args,
92
+ debug=False,
93
+ )
94
+
95
+ launcher.run(LOCAL, TEST)
96
+
97
+ ################################################################################################################
98
+ start_seed += _n_seeds
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/start_seed/test.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import wandb
4
+
5
+ from experiment_launcher import run_experiment, single_experiment
6
+
7
+
8
+ # This decorator creates results_dir as results_dir/seed, and saves the experiment arguments into a file.
9
+ @single_experiment
10
+ def experiment(
11
+ #######################################
12
+ env: str = 'env', # You need to specify the argument type if you use the automatic parser.
13
+ env_param: str = 'aa',
14
+ a: int = 1,
15
+ boolean_param: bool = True,
16
+ some_default_param: str = 'b',
17
+
18
+ debug: bool = True,
19
+
20
+ #######################################
21
+ # MANDATORY
22
+ seed: int = 0,
23
+ results_dir: str = 'logs',
24
+
25
+ #######################################
26
+ # OPTIONAL
27
+ # accept unknown arguments
28
+ **kwargs
29
+ ):
30
+ # EXPERIMENT
31
+ print(f'DEBUG MODE: {debug}')
32
+
33
+ print(f'kwargs: {kwargs}')
34
+
35
+ filename = os.path.join(results_dir, 'log_' + str(seed) + '.txt')
36
+ out_str = f'Running experiment with seed {seed}, env {env}, ' \
37
+ f'env_param {env_param}, a {a}, ' \
38
+ f'boolean_param {boolean_param}, some_default_param {some_default_param}'
39
+ print(out_str)
40
+ with open(filename, 'w') as file:
41
+ file.write('Some logs in a log file.\n')
42
+ file.write(out_str)
43
+
44
+
45
+ if __name__ == '__main__':
46
+ # Leave unchanged
47
+ run_experiment(experiment)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ try:
2
+ from .launcher import Launcher, run_experiment
3
+ from .decorators import single_experiment, single_experiment_yaml, single_experiment_flat
4
+ from .utils import is_local
5
+ except ImportError:
6
+ pass
7
+
8
+ __version__ = '2.3'
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/decorators.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ from functools import wraps, partial
4
+
5
+ from experiment_launcher.utils import save_args, start_wandb, create_results_dir
6
+
7
+
8
+ def wrapper_single_experiment(exp_func, save_args_yaml=False, use_logging=False,
9
+ make_dirs_with_seed=True, print_exp_args=False):
10
+ @wraps(exp_func)
11
+ def wrapper(
12
+ # Function arguments
13
+ *args,
14
+ **kwargs
15
+ ):
16
+ # Make results directory
17
+ create_results_dir(kwargs, make_dirs_with_seed)
18
+
19
+ # Setup logging
20
+ if use_logging:
21
+ logging.basicConfig(level=logging.INFO,
22
+ filename=os.path.join(kwargs['results_dir'], "logfile"),
23
+ # stream=sys.stdout,
24
+ filemode="a+",
25
+ format="%(asctime)-15s %(levelname)-8s %(message)s")
26
+
27
+ # Save arguments
28
+ save_args(kwargs['results_dir'], kwargs,
29
+ git_repo_path='./', save_args_as_yaml=save_args_yaml, print_exp_args=print_exp_args)
30
+
31
+ # Start WandB
32
+ wandb_activated = True if 'wandb_mode' in kwargs else False
33
+ if wandb_activated:
34
+ wandb_run = start_wandb(**kwargs)
35
+
36
+ # Run the experiment
37
+ exp_func(*args, **kwargs)
38
+
39
+ # Clean up
40
+ if wandb_activated:
41
+ wandb_run.finish()
42
+
43
+ return wrapper
44
+
45
+
46
+ single_experiment = partial(wrapper_single_experiment)
47
+ single_experiment_flat = partial(wrapper_single_experiment, make_dirs_with_seed=False)
48
+ single_experiment_yaml = partial(wrapper_single_experiment, save_args_yaml=True, use_logging=True, print_exp_args=False)
49
+ single_experiment_flat_yaml = partial(wrapper_single_experiment, make_dirs_with_seed=False, save_args_yaml=True)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/exceptions.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ class ResultsDirException(Exception):
2
+ """
3
+ Raised when the results directory already exists
4
+ """
5
+ def __init__(self, exp, results_dir='./logs'):
6
+ message = f"\n" \
7
+ f"When trying to add the experiment: {exp}\n" \
8
+ f"Results directory {results_dir} already exists.\n" \
9
+ f"Make sure that varying parameters have a trailing double underscore."
10
+ super().__init__(message)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/launcher.py ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import datetime
3
+ import inspect
4
+ import os
5
+ import traceback
6
+ from copy import copy, deepcopy
7
+ from distutils.util import strtobool
8
+ from importlib import import_module
9
+
10
+ import numpy as np
11
+ from joblib import Parallel, delayed
12
+
13
+ from experiment_launcher.exceptions import ResultsDirException
14
+
15
+
16
+ class Launcher(object):
17
+ """
18
+ Creates and starts jobs with Joblib or SLURM.
19
+
20
+ """
21
+
22
+ def __init__(self, exp_name, exp_file, n_seeds, start_seed=0, n_cores=1, memory_per_core=2000,
23
+ days=0, hours=24, minutes=0, seconds=0,
24
+ project_name=None, base_dir=None,
25
+ n_exps_in_parallel=1,
26
+ conda_env=None, gres=None, constraint=None, partition=None,
27
+ begin=None, use_timestamp=True, compact_dirs=False):
28
+ """
29
+ Constructor.
30
+
31
+ Args:
32
+ exp_name (str): name of the experiment
33
+ exp_file (str): name of the python module running a single experiment (relative path)
34
+ n_seeds (int): number of seeds for each experiment configuration
35
+ start_seed (int): first seed
36
+ n_cores (int): number of cpu cores
37
+ memory_per_core (int): maximum memory per core (slurm will kill the job if this is reached)
38
+ days (int): number of days the experiment can last (in slurm)
39
+ hours (int): number of hours the experiment can last (in slurm)
40
+ minutes (int): number of minutes the experiment can last (in slurm)
41
+ seconds (int): number of seconds the experiment can last (in slurm)
42
+ project_name (str): name of the project for slurm. This is important if you have
43
+ different projects (e.g. in the hhlr cluster)
44
+ base_dir (str): path to directory to save the results (in hhlr results are saved to /work/scratch/$USER)
45
+ n_exps_in_parallel (int): number of experiment configurations to run in parallel.
46
+ If running in the cluster, and the gpu is selected, then it is the number of jobs in each slurm file
47
+ (e.g. for multiple experiments in the same gpu)
48
+ conda_env (str): name of the conda environment to run the experiments in
49
+ gres (str): request cluster resources. E.g. to add a GPU in the IAS cluster specify gres='gpu:rtx2080:1'
50
+ constraint (str): constraint for the slurm job. E.g. to add a GPU in the IAS cluster: constraint='rtx2080'
51
+ partition (str, None): the partition to use in case of slurm execution. If None, no partition is specified.
52
+ begin (str): start the slurm experiment at a given time (see --begin in slurm docs)
53
+ use_timestamp (bool): add a timestamp to the experiment name
54
+ compact_dirs (bool): If true, only the parameter value is used for the directory name.
55
+
56
+ """
57
+ self._exp_name = exp_name
58
+ self._exp_file = exp_file
59
+ self._start_seed = start_seed
60
+ self._n_seeds = n_seeds
61
+ self._n_cores = n_cores
62
+ self._memory_per_core = memory_per_core
63
+ self._duration = Launcher._to_duration(days, hours, minutes, seconds)
64
+ self._project_name = project_name
65
+ self._n_exps_in_parallel = n_exps_in_parallel
66
+ self._conda_env = conda_env
67
+ self._gres = gres
68
+ self._constraint = constraint
69
+ self._partition = partition
70
+ self._begin = begin
71
+
72
+ self._experiment_list = list()
73
+
74
+ if use_timestamp:
75
+ self._exp_name += datetime.datetime.now().strftime('_%Y-%m-%d_%H-%M-%S')
76
+
77
+ base_dir = './logs' if base_dir is None else base_dir
78
+ self._exp_dir_local = os.path.join(base_dir, self._exp_name)
79
+
80
+ # tracks the results directories
81
+ self._results_dir_l = []
82
+
83
+ self._exp_dir_slurm = self._exp_dir_local
84
+ if os.getenv("USER"):
85
+ scratch_dir = os.path.join('/work', 'scratch', os.getenv('USER'))
86
+ if os.path.isdir(scratch_dir):
87
+ self._exp_dir_slurm = os.path.join(scratch_dir, self._exp_name)
88
+ os.makedirs(self._exp_dir_slurm)
89
+
90
+ # directories for slurm sbatch files and logs
91
+ self._exp_dir_slurm_files = os.path.join(self._exp_dir_slurm, "slurm_files")
92
+ self._exp_dir_slurm_logs = os.path.join(self._exp_dir_slurm, "slurm_logs")
93
+
94
+ self._compact_dirs = compact_dirs
95
+
96
+ def add_experiment(self, **kwargs):
97
+ self._experiment_list.append(deepcopy(kwargs))
98
+
99
+ def run(self, local, test=False, sequential=False):
100
+ self._check_experiments_results_directories()
101
+ if local:
102
+ if sequential:
103
+ self._run_sequential(test)
104
+ else:
105
+ self._run_local(test)
106
+ else:
107
+ self._run_slurm(test)
108
+
109
+ self._experiment_list = list()
110
+
111
+ def generate_slurm(self, command_line_list=None):
112
+ project_name_option = ''
113
+ partition_option = ''
114
+ begin_option = ''
115
+ gres_option = ''
116
+ constraint_option = ''
117
+
118
+ if self._project_name:
119
+ project_name_option = '#SBATCH -A ' + self._project_name + '\n'
120
+ if self._partition:
121
+ partition_option += f'#SBATCH -p {self._partition}\n'
122
+ if self._begin:
123
+ begin_option += f'#SBATCH --begin={self._begin}\n'
124
+ if self._gres:
125
+ print(self._gres)
126
+ gres_option += '#SBATCH --gres=' + str(self._gres) + '\n'
127
+ if self._constraint:
128
+ print(self._constraint)
129
+ constraint_option += '#SBATCH --constraint=' + str(self._constraint) + '\n'
130
+
131
+ conda_code = ''
132
+ if self._conda_env:
133
+ if os.path.exists(f'{os.getenv("HOME")}/miniconda3'):
134
+ conda_code += f'eval \"$({os.getenv("HOME")}/miniconda3/bin/conda shell.bash hook)\"\n'
135
+ elif os.path.exists(f'{os.getenv("HOME")}/anaconda3'):
136
+ conda_code += f'eval \"$({os.getenv("HOME")}/anaconda/bin/conda shell.bash hook)\"\n'
137
+ else:
138
+ raise Exception('You do not have a /home/USER/miniconda3 or /home/USER/anaconda3 directories')
139
+ conda_code += f'conda activate {self._conda_env}\n\n'
140
+ python_code = f'python {self._exp_file_path} \\'
141
+ else:
142
+ python_code = f'python3 {self._exp_file_path} \\'
143
+
144
+ experiment_args = '\t\t'
145
+ experiment_args += r'${@: 2}'
146
+ experiment_args += ' \\'
147
+
148
+ seed_code = f'\t\t--seed $(({self._start_seed} + $SLURM_ARRAY_TASK_ID)) \\'
149
+ result_dir_code = '\t\t--results_dir $1'
150
+
151
+ code = f"""\
152
+ #!/usr/bin/env bash
153
+
154
+ ###############################################################################
155
+ # SLURM Configurations
156
+
157
+ # Optional parameters
158
+ {project_name_option}{partition_option}{begin_option}{gres_option}{constraint_option}
159
+ # Mandatory parameters
160
+ #SBATCH -J {self._exp_name}
161
+ #SBATCH -a 0-{self._n_seeds - 1}
162
+ #SBATCH -t {self._duration}
163
+ #SBATCH --ntasks 1
164
+ #SBATCH --cpus-per-task {self._n_cores}
165
+ #SBATCH --mem-per-cpu={self._memory_per_core}
166
+ #SBATCH -o {self._exp_dir_slurm_logs}/%A_%a.out
167
+ #SBATCH -e {self._exp_dir_slurm_logs}/%A_%a.err
168
+
169
+ ###############################################################################
170
+ # Your PROGRAM call starts here
171
+ echo "Starting Job $SLURM_JOB_ID, Index $SLURM_ARRAY_TASK_ID"
172
+
173
+ # Program specific arguments
174
+ {conda_code}
175
+
176
+ """
177
+ code += f"""\
178
+ # Program specific arguments
179
+
180
+ echo "Running scripts in parallel..."
181
+ echo "########################################################################"
182
+
183
+ """
184
+ for command_line in command_line_list:
185
+ code += f"""\
186
+
187
+ {python_code}
188
+ {seed_code}
189
+ \t\t{command_line} &
190
+
191
+ """
192
+
193
+ code += f"""\
194
+
195
+ wait # This will wait until both scripts finish
196
+ echo "########################################################################"
197
+ echo "...done."
198
+ """
199
+ return code
200
+
201
+ def save_slurm(self, command_line_list=None, idx: str = None):
202
+ code = self.generate_slurm(command_line_list)
203
+
204
+ label = f"_{idx}" if idx is not None else ""
205
+ script_name = f'slurm_{self._exp_name}{label}.sh'
206
+ full_path = os.path.join(self._exp_dir_slurm_files, script_name)
207
+
208
+ with open(full_path, "w") as file:
209
+ file.write(code)
210
+
211
+ return full_path
212
+
213
+ def _run_slurm(self, test):
214
+ # Create slurm directories for sbatch and log files
215
+ os.makedirs(self._exp_dir_slurm_files)
216
+ os.makedirs(self._exp_dir_slurm_logs)
217
+
218
+ # Generate and save slurm files
219
+ slurm_files_path_l = []
220
+ experiment_list_chunked = []
221
+ for i in range(0, len(self._experiment_list), self._n_exps_in_parallel):
222
+ experiment_list_chunked.append(self._experiment_list[i:i + self._n_exps_in_parallel])
223
+
224
+ for i, exps in enumerate(experiment_list_chunked):
225
+ command_line_l = []
226
+ for exp in exps:
227
+ exp_new_without_underscore = self.remove_last_underscores_dict(exp)
228
+ command_line_arguments = self._convert_to_command_line(exp_new_without_underscore)
229
+ results_dir = self._generate_results_dir(self._exp_dir_slurm, exp)
230
+ command_line_l.append(f'--results_dir {results_dir} {command_line_arguments}')
231
+ slurm_files_path_l.append(self.save_slurm(command_line_l,
232
+ str(i).zfill(len(str(len(experiment_list_chunked))))))
233
+
234
+ # Launch slurm files in parallel
235
+ for slurm_file_path in slurm_files_path_l:
236
+ command = f"sbatch {slurm_file_path}"
237
+ if test:
238
+ print(command)
239
+ else:
240
+ os.system(command)
241
+
242
+ def _run_local(self, test):
243
+ if not test:
244
+ os.makedirs(self._exp_dir_local, exist_ok=True)
245
+
246
+ module = import_module(self._exp_file)
247
+ experiment = module.experiment
248
+
249
+ if test:
250
+ self._test_experiment_local()
251
+ else:
252
+ def experiment_wrapper(params):
253
+ try:
254
+ experiment(**params)
255
+ except Exception:
256
+ print("Experiment failed with parameters:")
257
+ print(params)
258
+ traceback.print_exc()
259
+
260
+ params_dict = get_experiment_default_params(experiment)
261
+
262
+ Parallel(n_jobs=self._n_exps_in_parallel)(delayed(experiment_wrapper)(deepcopy(params))
263
+ for params in self._generate_exp_params(params_dict))
264
+
265
+ def _run_sequential(self, test):
266
+ if not test:
267
+ os.makedirs(self._exp_dir_local, exist_ok=True)
268
+
269
+ module = import_module(self._exp_file)
270
+ experiment = module.experiment
271
+
272
+ if test:
273
+ self._test_experiment_local()
274
+ else:
275
+ default_params_dict = get_experiment_default_params(experiment)
276
+
277
+ for params in self._generate_exp_params(default_params_dict):
278
+ try:
279
+ experiment(**params)
280
+ except Exception:
281
+ print("Experiment failed with parameters:")
282
+ print(params)
283
+ traceback.print_exc()
284
+
285
+ def _check_experiments_results_directories(self):
286
+ """
287
+ Check if the results directory produced for each experiment clash.
288
+ """
289
+ for exp in self._experiment_list:
290
+ results_dir = self._generate_results_dir(self._exp_dir_local, exp)
291
+ # Check if the results directory already exists.
292
+ if results_dir in self._results_dir_l:
293
+ # Terminate to prevent overwriting the results directory.
294
+ raise ResultsDirException(exp, results_dir)
295
+ self._results_dir_l.append(results_dir)
296
+
297
+ def _test_experiment_local(self):
298
+ for exp, results_dir in zip(self._experiment_list, self._results_dir_l):
299
+ for i in range(self._start_seed, self._n_seeds):
300
+ params = str(exp).replace('{', '(').replace('}', '').replace(': ', '=').replace('\'', '')
301
+ if params:
302
+ params += ', '
303
+ print('experiment' + params + 'seed=' + str(i) + ', results_dir=' + results_dir + ')')
304
+
305
+ def _generate_results_dir(self, results_dir, exp, n=6):
306
+ for key, value in exp.items():
307
+ if key.endswith('__'):
308
+ if self._compact_dirs:
309
+ subfolder = str(value)
310
+ else:
311
+ subfolder = key + '_' + str(value).replace(' ', '')
312
+ subfolder = subfolder.replace('/', '-') # avoids creating subfolders if there is a slash in the name
313
+ results_dir = os.path.join(results_dir, subfolder)
314
+ return results_dir
315
+
316
+ def _generate_exp_params(self, default_params_dict):
317
+ seeds = np.arange(self._start_seed, self._start_seed + self._n_seeds)
318
+ for exp in self._experiment_list:
319
+ params_dict = deepcopy(default_params_dict)
320
+ exp_new_without_underscore = self.remove_last_underscores_dict(exp)
321
+ params_dict.update(exp_new_without_underscore)
322
+ params_dict['results_dir'] = self._generate_results_dir(self._exp_dir_local, exp)
323
+ for seed in seeds:
324
+ params_dict['seed'] = int(seed)
325
+ yield params_dict
326
+
327
+ @staticmethod
328
+ def remove_last_underscores_dict(exp_dict):
329
+ exp_dict_new = copy(exp_dict)
330
+ for key, value in exp_dict.items():
331
+ if key.endswith('__'):
332
+ exp_dict_new[key[:-2]] = value
333
+ del exp_dict_new[key]
334
+ return exp_dict_new
335
+
336
+ @staticmethod
337
+ def _convert_to_command_line(exp):
338
+ command_line = ''
339
+ for key, value in exp.items():
340
+ new_command = '--' + key + ' '
341
+
342
+ if isinstance(value, list):
343
+ new_command += ' '.join(map(str, value)) + ' '
344
+ else:
345
+ new_command += str(value) + ' '
346
+
347
+ command_line += new_command
348
+
349
+ # remove last space
350
+ command_line = command_line[:-1]
351
+
352
+ return command_line
353
+
354
+ @staticmethod
355
+ def _to_duration(days, hours, minutes, seconds):
356
+ h = "0" + str(hours) if hours < 10 else str(hours)
357
+ m = "0" + str(minutes) if minutes < 10 else str(minutes)
358
+ s = "0" + str(seconds) if seconds < 10 else str(seconds)
359
+
360
+ return str(days) + '-' + h + ":" + m + ":" + s
361
+
362
+ @property
363
+ def exp_name(self):
364
+ return self._exp_name
365
+
366
+ def log_dir(self, local=True):
367
+ if local:
368
+ return self._exp_dir_local
369
+ else:
370
+ return self._exp_dir_slurm
371
+
372
+ @property
373
+ def _exp_file_path(self):
374
+ module = import_module(self._exp_file)
375
+ return module.__file__
376
+
377
+
378
+ def get_experiment_default_params(func):
379
+ signature = inspect.signature(func)
380
+ defaults = {}
381
+ for k, v in signature.parameters.items():
382
+ if v.default is not inspect.Parameter.empty:
383
+ defaults[k] = v.default
384
+ return defaults
385
+
386
+
387
+ def translate_experiment_params_to_argparse(parser, func):
388
+ annotation_to_argparse = {
389
+ 'str': str,
390
+ 'int': int,
391
+ 'float': float,
392
+ 'bool': bool,
393
+ 'list': None,
394
+ 'tuple': None,
395
+ }
396
+ arg_experiments = parser.add_argument_group('Experiment')
397
+ signature = inspect.signature(func)
398
+ for k, v in signature.parameters.items():
399
+ if k not in ['seed', 'results_dir']:
400
+ if v.default is not inspect.Parameter.empty:
401
+ if v.annotation.__name__ in annotation_to_argparse:
402
+ if v.annotation.__name__ == 'bool':
403
+ arg_experiments.add_argument(f"--{str(k)}", type=lambda x: bool(strtobool(x)),
404
+ nargs='?', const=v.default, default=v.default)
405
+ elif v.annotation.__name__ == 'list':
406
+ arg_experiments.add_argument(f"--{str(k)}", nargs='+')
407
+ else:
408
+ arg_experiments.add_argument(f"--{str(k)}", type=annotation_to_argparse[v.annotation.__name__])
409
+ else:
410
+ raise NotImplementedError(f'{v.annotation.__name__} not found in annotation_to_argparse.')
411
+ return parser
412
+
413
+
414
+ def add_launcher_base_args(parser):
415
+ arg_default = parser.add_argument_group('Default')
416
+ arg_default.add_argument('--seed', type=int)
417
+ arg_default.add_argument('--results_dir', type=str)
418
+ return parser
419
+
420
+
421
+ def has_kwargs(func):
422
+ signature = inspect.signature(func)
423
+ for k, v in signature.parameters.items():
424
+ if v.kind == v.VAR_KEYWORD:
425
+ return True
426
+
427
+ return False
428
+
429
+
430
+ def string_to_primitive(string):
431
+ try:
432
+ return int(string)
433
+ except ValueError:
434
+ try:
435
+ return float(string)
436
+ except ValueError:
437
+ try:
438
+ # boolean
439
+ return bool(strtobool(string))
440
+ except ValueError:
441
+ return string
442
+
443
+
444
+ def parse_unknown_args(unknown):
445
+ kwargs = dict()
446
+
447
+ key_idxs = [i for i, arg in enumerate(unknown) if arg.startswith('--')]
448
+
449
+ if len(key_idxs) > 0:
450
+ key_n_args = [key_idxs[i+1] - 1 - key_idxs[i] for i in range(len(key_idxs)-1)]
451
+ key_n_args.append(len(unknown) - 1 - key_idxs[-1])
452
+
453
+ for i, idx in enumerate(key_idxs):
454
+ key = unknown[idx][2:]
455
+ n_args = key_n_args[i]
456
+ if n_args > 1:
457
+ values = list()
458
+
459
+ for v in unknown[idx+1:idx + 1 + n_args]:
460
+ values.append(string_to_primitive(v))
461
+
462
+ kwargs[key] = values
463
+
464
+ elif n_args == 1:
465
+ kwargs[key] = string_to_primitive(unknown[idx+1])
466
+
467
+ return kwargs
468
+
469
+
470
+ def parse_args(func):
471
+ parser = argparse.ArgumentParser()
472
+
473
+ parser = translate_experiment_params_to_argparse(parser, func)
474
+
475
+ parser = add_launcher_base_args(parser)
476
+ parser.set_defaults(**get_experiment_default_params(func))
477
+
478
+ if has_kwargs(func):
479
+ args, unknown = parser.parse_known_args()
480
+ kwargs = parse_unknown_args(unknown)
481
+
482
+ args = vars(args)
483
+ args.update(kwargs)
484
+
485
+ return args
486
+ else:
487
+ args = parser.parse_args()
488
+ return vars(args)
489
+
490
+
491
+ def run_experiment(func, args=None):
492
+ if not args:
493
+ args = parse_args(func)
494
+
495
+ func(**args)
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/utils.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import json
3
+ import os
4
+ import random
5
+ from random import randint
6
+ from shutil import which
7
+
8
+ import git
9
+ import wandb
10
+ import yaml
11
+ from git import InvalidGitRepositoryError
12
+
13
+
14
+ def save_args(results_dir, args, git_repo_path=None, seed=None, save_args_as_yaml=False, print_exp_args=False):
15
+ try:
16
+ repo = git.Repo(git_repo_path, search_parent_directories=True)
17
+ args['git_hash'] = repo.head.object.hexsha
18
+ args['git_url'] = repo.remotes.origin.url
19
+ except InvalidGitRepositoryError:
20
+ args['git_hash'] = ''
21
+ args['git_url'] = ''
22
+ except ValueError:
23
+ args['git_hash'] = ''
24
+ args['git_url'] = ''
25
+
26
+ if save_args_as_yaml:
27
+ filename = 'args.yaml' if seed is None else f'args-{seed}.yaml'
28
+ with open(os.path.join(results_dir, filename), 'w') as f:
29
+ yaml.dump(args, f, Dumper=yaml.Dumper)
30
+ else:
31
+ filename = 'args.json' if seed is None else f'args-{seed}.json'
32
+ with open(os.path.join(results_dir, filename), 'w') as f:
33
+ json.dump(args, f, indent=2)
34
+
35
+ del args['git_hash']
36
+ del args['git_url']
37
+
38
+ if print_exp_args:
39
+ print('------------------------------------------------------------------------------------')
40
+ print('--------> Experiment arguments')
41
+ print(json.dumps(args, indent=2))
42
+ print('------------------------------------------------------------------------------------')
43
+
44
+
45
+ def is_local():
46
+ return which('sbatch') is None
47
+
48
+
49
+ def start_wandb(
50
+ wandb_mode='disabled',
51
+ wandb_entity='experiment_launcher',
52
+ wandb_project='test_experiment_launcher',
53
+ wandb_group=None,
54
+ wandb_run_name=None,
55
+ **kwargs
56
+ ):
57
+ # https://github.com/wandb/wandb/issues/3911#issuecomment-1409769887
58
+ # workaround to make sure that wandb does not crash
59
+ os.environ["WANDB__SERVICE_WAIT"] = "600"
60
+
61
+ modes = ['online', 'offline', 'disabled']
62
+ assert wandb_mode in modes, f"wandb_mode must be in {modes}"
63
+
64
+ if wandb_mode == 'disabled':
65
+ return wandb.init(mode="disabled", reinit=True)
66
+
67
+ init = {
68
+ "mode": wandb_mode,
69
+ "entity": wandb_entity,
70
+ "project": wandb_project,
71
+ "group": wandb_group,
72
+ "name": wandb_run_name,
73
+ "reinit": True,
74
+ "notes": datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'),
75
+ }
76
+
77
+ return wandb.init(**init)
78
+
79
+
80
+ def fix_random_seed(seed):
81
+ random.seed(seed)
82
+
83
+ try:
84
+ import numpy as np
85
+ import torch
86
+ except ImportError:
87
+ pass
88
+
89
+ try:
90
+ np.random.seed(seed)
91
+ except NameError:
92
+ pass
93
+
94
+ try:
95
+ torch.manual_seed(seed)
96
+ torch.cuda.manual_seed(seed)
97
+ # https://pytorch.org/docs/stable/notes/randomness.html#cuda-convolution-benchmarking
98
+ # torch.backends.cudnn.benchmark = False
99
+ except NameError:
100
+ pass
101
+
102
+
103
+ def random_int_with_n_digits(n):
104
+ range_start = 10 ** (n - 1)
105
+ range_end = (10 ** n) - 1
106
+ return randint(range_start, range_end)
107
+
108
+
109
+ def create_results_dir(kwargs, make_dirs_with_seed=True):
110
+ seed = kwargs['seed']
111
+ results_dir = kwargs['results_dir']
112
+ assert results_dir is not None and seed is not None, "results_dir and seed must exist"
113
+ results_dir_new = results_dir
114
+ if make_dirs_with_seed:
115
+ results_dir_new = os.path.join(results_dir_new, str(seed))
116
+ os.makedirs(results_dir_new, exist_ok=True)
117
+ kwargs['results_dir'] = results_dir_new
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ joblib
2
+ gitpython
3
+ numpy
4
+ pyyaml
5
+ wandb
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/setup.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+ from codecs import open
3
+ from os import path
4
+
5
+ from experiment_launcher import __version__
6
+
7
+ here = path.abspath(path.dirname(__file__))
8
+
9
+ requires_list = []
10
+ with open(path.join(here, 'requirements.txt'), encoding='utf-8') as f:
11
+ for line in f:
12
+ requires_list.append(str(line))
13
+
14
+ long_description = 'experiment_launcher is a Python library used to run experiments.' \
15
+ ' Supports Joblib and slurm jobs.'
16
+
17
+ setup(
18
+ name='experiment-launcher',
19
+ version=__version__,
20
+ description='A Python toolkit for launching experiments.',
21
+ long_description=long_description,
22
+ url='https://git.ias.informatik.tu-darmstadt.de/common/experiment_launcher',
23
+ author="Davide Tateo, Joao Carvalho",
24
+ author_email='davide@robot-learning.de, joao@robot-learning.de',
25
+ license='MIT',
26
+ packages=[package for package in find_packages()
27
+ if package.startswith('experiment_launcher')],
28
+ zip_safe=False,
29
+ install_requires=requires_list,
30
+ extras_require={},
31
+ classifiers=["Programming Language :: Python :: 3",
32
+ "License :: OSI Approved :: MIT License",
33
+ "Operating System :: OS Independent",
34
+ ]
35
+ )
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/LICENSE ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Yorai Shaoul, slightly modified original code written by An Thai Le, Joao Carvalho.
4
+ Original license below:
5
+
6
+ Copyright (c) 2023 An Thai Le, Joao Carvalho
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/README.md ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The original repository is this one: https://github.com/anindex/motion_planning_baselines.
2
+ Their original README follows.
3
+
4
+ # Motion Planning Baselines in PyTorch
5
+
6
+ This library implements various Motion Planning methods.
7
+
8
+ **NOTE**: `mp_baslines` is under heavy development and highly experimental.
9
+
10
+ ## Installation
11
+
12
+ Simply activate your conda/Python environment and run
13
+
14
+ ```bash
15
+ pip install -e .
16
+ ```
17
+
18
+ ## Examples
19
+
20
+ ```bash
21
+ python examples/pointmass_dense_2d_CHOMP.py
22
+ python examples/pointmass_dense_2d_GPMP.py
23
+ python examples/pointmass_grid_circles_2d_Stoch-GPMP.py
24
+ python examples/pointmass_grid_circles_2d_STOMP.py
25
+ python examples/pointmass_grid_circles_2d_MPPI.py
26
+ python examples/pointmass_dense_2d_RRT_multiprocess.py
27
+ ```
28
+
29
+ ## Contact
30
+
31
+ If you have any questions or find any bugs, please let us know:
32
+
33
+ - [An Le](https://www.ias.informatik.tu-darmstadt.de/Team/AnThaiLe), [an@robot-learning.de](an@robot-learning.de)
34
+ - [Joao Carvalho](https://www.ias.informatik.tu-darmstadt.de/Team/JoaoCarvalho), [joao@robot-learning.de](joao@robot-learning.de)
35
+
36
+ ## Citation
37
+
38
+ If you found this repository useful, please consider citing these references:
39
+
40
+ ```bibtex
41
+ @inproceedings{le2023accelerating,
42
+ title={Accelerating Motion Planning via Optimal Transport},
43
+ author={Le, An T. and Chalvatzaki, Georgia and Biess, Armin and Peters, Jan},
44
+ booktitle={Advances in Neural Information Processing Systems (NeurIPS)},
45
+ year={2023}
46
+ }
47
+
48
+ @article{carvalho2023motion,
49
+ title={Motion planning diffusion: Learning and planning of robot motions with diffusion models},
50
+ author={Carvalho, Joao and Le, An T and Baierl, Mark and Koert, Dorothea and Peters, Jan},
51
+ journal={arXiv preprint arXiv:2308.01557},
52
+ year={2023}
53
+ }
54
+ ```
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/__init__.py ADDED
File without changes
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/__init__.py ADDED
File without changes
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/base.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import abc
2
+ from abc import ABC, abstractmethod
3
+ from typing import Tuple
4
+ import torch
5
+
6
+ from mp_baselines.planners.costs.factors.gp_factor import GPFactor
7
+ from mp_baselines.planners.costs.factors.mp_priors_multi import MultiMPPrior
8
+ from mp_baselines.planners.costs.factors.unary_factor import UnaryFactor
9
+ from torch_robotics.trajectory.utils import finite_difference_vector
10
+
11
+
12
+ class MPPlanner(ABC):
13
+ """Base class for all planners."""
14
+
15
+ def __init__(
16
+ self,
17
+ name: str = None,
18
+ tensor_args: dict = None,
19
+ **kwargs
20
+ ):
21
+ self.name = name
22
+ self.tensor_args = tensor_args
23
+ self._kwargs = kwargs
24
+
25
+ @abstractmethod
26
+ def optimize(self, opt_iters: int = 1, **observation) -> Tuple[bool, torch.Tensor]:
27
+ """Plan a path from start to goal.
28
+
29
+ Args:
30
+ opt_iters: Number of optimization iters.
31
+ observation: dict of observations.
32
+
33
+ Returns:
34
+ success: True if a path was found.
35
+ path: Path from start to goal.
36
+ """
37
+ pass
38
+
39
+ def __call__(self, opt_iters: int = 1, **observation) -> Tuple[bool, torch.Tensor]:
40
+ """Plan a path from start to goal.
41
+
42
+ Args:
43
+ start: Start position.
44
+ goal: Goal position.
45
+
46
+ Returns:
47
+ success: True if a path was found.
48
+ path: Path from start to goal.
49
+ """
50
+ return self.optimize(opt_iters, **observation)
51
+
52
+ def __repr__(self):
53
+ return f"{self.name}({self._kwargs})"
54
+
55
+ @abc.abstractmethod
56
+ def render(self, ax, **kwargs):
57
+ raise NotImplementedError
58
+
59
+
60
+ class OptimizationPlanner(MPPlanner):
61
+
62
+ def __init__(
63
+ self,
64
+ name: str = 'OptimizationPlanner',
65
+ n_dof: int = None,
66
+ n_support_points: int = None,
67
+ n_interpolated_points: int = None,
68
+ num_particles_per_goal: int = None,
69
+ opt_iters: int = None,
70
+ dt: float = None,
71
+ start_state: torch.Tensor = None,
72
+ cost=None,
73
+ initial_particle_means=None,
74
+ multi_goal_states: torch.Tensor = None,
75
+ sigma_start_init: float = 0.001,
76
+ sigma_goal_init: float = 0.001,
77
+ sigma_gp_init: float = 10.,
78
+ pos_only: bool = False,
79
+ tensor_args: dict = None, **kwargs
80
+ ):
81
+ super().__init__(name, tensor_args, **kwargs)
82
+ self.n_dof = n_dof
83
+ self.dim = 2 * self.n_dof
84
+ self.n_support_points = n_support_points
85
+ self.n_interpolated_points = n_interpolated_points
86
+ self.num_particles_per_goal = num_particles_per_goal
87
+ self.opt_iters = opt_iters
88
+ self.dt = dt
89
+ self.pos_only = pos_only
90
+
91
+ self.start_state = start_state
92
+ self.multi_goal_states = multi_goal_states
93
+ if multi_goal_states is None: # NOTE(an): if there is no goal, we assume xere is at least one solution
94
+ self.num_goals = 1
95
+ else:
96
+ assert multi_goal_states.ndim == 2
97
+ self.num_goals = multi_goal_states.shape[0]
98
+ self.num_particles = self.num_goals * self.num_particles_per_goal
99
+ self.cost = cost
100
+ self.initial_particle_means = initial_particle_means
101
+ self._particle_means = None
102
+ if self.pos_only:
103
+ self.d_state_opt = self.n_dof
104
+ self.start_state = self.start_state
105
+ else:
106
+ self.d_state_opt = 2 * self.n_dof
107
+ self.start_state = torch.cat([self.start_state, torch.zeros_like(self.start_state)], dim=-1)
108
+ if self.multi_goal_states is not None:
109
+ self.multi_goal_states = torch.cat([self.multi_goal_states, torch.zeros_like(self.multi_goal_states)], dim=-1)
110
+
111
+ self.sigma_start_init = sigma_start_init
112
+ self.sigma_goal_init = sigma_goal_init
113
+ self.sigma_gp_init = sigma_gp_init
114
+
115
+ def get_GP_prior(
116
+ self,
117
+ start_K,
118
+ gp_K,
119
+ goal_K,
120
+ state_init,
121
+ particle_means=None,
122
+ goal_states=None,
123
+ tensor_args=None,
124
+ ):
125
+ if tensor_args is None:
126
+ tensor_args = self.tensor_args
127
+ return MultiMPPrior(
128
+ self.n_support_points - 1,
129
+ self.dt,
130
+ self.dim,
131
+ self.n_dof,
132
+ start_K,
133
+ gp_K,
134
+ state_init,
135
+ K_g_inv=goal_K, # NOTE(sasha) Assume same goal Cov. for now
136
+ means=particle_means,
137
+ goal_states=goal_states,
138
+ tensor_args=tensor_args,
139
+ )
140
+
141
+ def const_vel_trajectories(
142
+ self,
143
+ start_state,
144
+ multi_goal_states,
145
+ ):
146
+ traj_dim = (multi_goal_states.shape[0], self.n_support_points, self.dim)
147
+ state_traj = torch.zeros(traj_dim, **self.tensor_args)
148
+ mean_vel = (multi_goal_states[:, :self.n_dof] - start_state[:, :self.n_dof]) / (self.n_support_points * self.dt)
149
+ for i in range(self.n_support_points):
150
+ state_traj[:, i, :self.n_dof] = start_state[:, :self.n_dof] * (self.n_support_points - i - 1) / (self.n_support_points - 1) \
151
+ + multi_goal_states[:, :self.n_dof] * i / (self.n_support_points - 1)
152
+ state_traj[:, :, self.n_dof:] = mean_vel.unsqueeze(1).repeat(1, self.n_support_points, 1)
153
+ return state_traj
154
+
155
+ def get_random_trajs(self):
156
+ # force torch.float64
157
+ tensor_args = dict(dtype=torch.float64, device=self.tensor_args['device'])
158
+ # set zero velocity for GP prior
159
+ start_state = torch.cat((self.start_state, torch.zeros_like(self.start_state)), dim=-1).to(**tensor_args)
160
+ if self.multi_goal_states is not None:
161
+ multi_goal_states = torch.cat((self.multi_goal_states, torch.zeros_like(self.multi_goal_states)), dim=-1).to(**tensor_args)
162
+ else:
163
+ multi_goal_states = None
164
+ #========= Initialization factors ===============
165
+ self.start_prior_init = UnaryFactor(
166
+ self.n_dof * 2,
167
+ self.sigma_start_init,
168
+ start_state,
169
+ tensor_args,
170
+ )
171
+
172
+ self.gp_prior_init = GPFactor(
173
+ self.n_dof,
174
+ self.sigma_gp_init,
175
+ self.dt,
176
+ self.n_support_points - 1,
177
+ tensor_args,
178
+ )
179
+
180
+ self.multi_goal_prior_init = []
181
+ if multi_goal_states is not None:
182
+ for i in range(self.num_goals):
183
+ self.multi_goal_prior_init.append(
184
+ UnaryFactor(
185
+ self.n_dof * 2,
186
+ self.sigma_goal_init,
187
+ multi_goal_states[i],
188
+ tensor_args,
189
+ )
190
+ )
191
+ self._traj_dist = self.get_GP_prior(
192
+ self.start_prior_init.K,
193
+ self.gp_prior_init.Q_inv[0],
194
+ self.multi_goal_prior_init[0].K if multi_goal_states is not None else None,
195
+ start_state,
196
+ goal_states=multi_goal_states,
197
+ tensor_args=tensor_args,
198
+ )
199
+ particles = self._traj_dist.sample(self.num_particles_per_goal).to(**tensor_args)
200
+ self.traj_dim = particles.shape
201
+ del self._traj_dist # free memory
202
+ return particles.flatten(0, 1).to(**self.tensor_args)
203
+
204
+ def _get_traj(self):
205
+ """
206
+ Get position-velocity trajectory from control distribution.
207
+ """
208
+ trajs = self._particle_means.clone()
209
+ if self.pos_only:
210
+ # Linear velocity by central finite differencing
211
+ vels = finite_difference_vector(trajs, dt=self.dt)
212
+ trajs = torch.cat((trajs, vels), dim=1)
213
+ return trajs
214
+
215
+ def get_traj(self):
216
+ return self._get_traj()
217
+
218
+ def _get_costs(self, state_trajectories, **observation):
219
+ if self.cost is None:
220
+ costs = torch.zeros(self.num_particles, )
221
+ else:
222
+ costs = self.cost(state_trajectories, **observation)
223
+ return costs
224
+
225
+ def render(self, ax, **kwargs):
226
+ raise NotImplementedError
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/chomp.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from mp_baselines.planners.base import OptimizationPlanner
4
+ from torch_robotics.torch_utils.torch_timer import TimerCUDA
5
+ from torch_robotics.torch_utils.torch_utils import batched_weighted_dot_prod
6
+
7
+
8
+ class CHOMP(OptimizationPlanner):
9
+ # https://www.ri.cmu.edu/pub_files/2009/5/icra09-chomp.pdf
10
+
11
+ def __init__(
12
+ self,
13
+ n_dof: int,
14
+ n_support_points: int,
15
+ num_particles_per_goal: int,
16
+ opt_iters: int,
17
+ dt: float,
18
+ start_state: torch.Tensor,
19
+ cost=None,
20
+ weight_prior_cost=0.1,
21
+ initial_particle_means=None,
22
+ step_size: float = 1.,
23
+ grad_clip: float = .01,
24
+ multi_goal_states: torch.Tensor = None,
25
+ sigma_start_init=0.001,
26
+ sigma_goal_init=0.001,
27
+ sigma_gp_init=10.,
28
+ pos_only: bool = True,
29
+ **kwargs
30
+ ):
31
+ super(CHOMP, self).__init__(name='CHOMP',
32
+ n_dof=n_dof,
33
+ n_support_points=n_support_points,
34
+ num_particles_per_goal=num_particles_per_goal,
35
+ opt_iters=opt_iters,
36
+ dt=dt,
37
+ start_state=start_state,
38
+ cost=cost,
39
+ initial_particle_means=initial_particle_means,
40
+ multi_goal_states=multi_goal_states,
41
+ sigma_start_init=sigma_start_init,
42
+ sigma_goal_init=sigma_goal_init,
43
+ sigma_gp_init=sigma_gp_init,
44
+ pos_only=pos_only,
45
+ **kwargs)
46
+
47
+ # CHOMP params
48
+ self.lr = step_size
49
+ self.grad_clip = grad_clip
50
+
51
+ self._particle_means = None
52
+ # Precision matrix, shape: [ctrl_dim, n_support_points, n_support_points]
53
+ self.Sigma_inv = self._get_R_mat(dt=self.dt, n_support_points=self.n_support_points, tensor_args=self.tensor_args)
54
+ self.Sigma = torch.inverse(self.Sigma_inv)
55
+ self.reset(initial_particle_means=initial_particle_means)
56
+
57
+ # Weight prior
58
+ self.weight_prior_cost = weight_prior_cost
59
+
60
+ def _get_R_mat2(self):
61
+ """
62
+ STOMP time-correlated Precision matrix.
63
+ Central finite difference velocity.
64
+ """
65
+ upper_diag = torch.diag(torch.ones(self.n_support_points - 1), diagonal=1)
66
+ lower_diag = torch.diag(torch.ones(self.n_support_points - 1), diagonal=-1,)
67
+ diag = -2 * torch.eye(self.n_support_points)
68
+ A_mat = upper_diag + diag + lower_diag
69
+ A_mat = torch.cat(
70
+ (torch.zeros(1, self.n_support_points),
71
+ A_mat,
72
+ torch.zeros(1, self.n_support_points)),
73
+ dim=0,
74
+ )
75
+ A_mat[0, 0] = 1.
76
+ A_mat[-1, -1] = 1.
77
+ A_mat = A_mat * 1./self.dt**2
78
+ R_mat = A_mat.t() @ A_mat
79
+ return R_mat.to(**self.tensor_args)
80
+
81
+ @classmethod
82
+ def _get_R_mat(
83
+ cls,
84
+ dt=0.01,
85
+ n_support_points=64,
86
+ tensor_args=None,
87
+ **kwargs
88
+ ):
89
+ """
90
+ CHOMP time-correlated Precision matrix.
91
+ Backward finite difference velocity.
92
+ """
93
+ lower_diag = -torch.diag(torch.ones(n_support_points - 1), diagonal=-1)
94
+ diag = 1 * torch.eye(n_support_points)
95
+ K_mat = diag + lower_diag
96
+ K_mat = torch.cat((K_mat, torch.zeros(1, n_support_points)), dim=0)
97
+ K_mat[-1, -1] = -1.
98
+ K_mat = K_mat * 1. / dt ** 2
99
+ R_mat = K_mat.t() @ K_mat
100
+
101
+ return R_mat.to(**tensor_args)
102
+
103
+ def reset(
104
+ self,
105
+ initial_particle_means=None,
106
+ ):
107
+ # Straight line position-trajectory from start to goal with const vel
108
+ if initial_particle_means is not None:
109
+ self._particle_means = initial_particle_means.clone()
110
+ else:
111
+ self._particle_means = self.get_random_trajs()
112
+
113
+ def optimize(
114
+ self,
115
+ opt_iters=None,
116
+ **observation
117
+ ):
118
+ """
119
+ Optimize for best trajectory at current state
120
+ """
121
+
122
+ self._run_optimization(opt_iters, **observation)
123
+ # get mean trajectory
124
+ curr_traj = self._get_traj()
125
+ return curr_traj
126
+
127
+ def _run_optimization(self, opt_iters, **observation):
128
+ """
129
+ Run optimization iterations.
130
+ """
131
+ optim_vis = observation.get('optim_vis', False)
132
+ if opt_iters is None:
133
+ opt_iters = self.opt_iters
134
+ for opt_step in range(opt_iters):
135
+ self._particle_means.requires_grad_(True)
136
+ costs = self._eval(self._particle_means, **observation)
137
+
138
+ # Get grad
139
+ costs.sum().backward(retain_graph=True)
140
+ # For stabilizing and preventing high gradients
141
+ self._particle_means.grad.data.clamp_(-self.grad_clip, self.grad_clip)
142
+ # zeroing grad at start and goal
143
+ self._particle_means.grad.data[..., 0, :] = 0.
144
+ self._particle_means.grad.data[..., -1, :] = 0.
145
+
146
+ # Update trajectory
147
+ self._particle_means.data.add_(-self.lr * self._particle_means.grad.data)
148
+ self._particle_means.grad.detach_()
149
+ self._particle_means.grad.zero_()
150
+
151
+ self._particle_means = self._particle_means.detach()
152
+
153
+ def _eval(self, x, **observation):
154
+ """
155
+ Evaluate costs.
156
+ """
157
+ if x.ndim == 2:
158
+ x = x.unsqueeze(0)
159
+
160
+ # Evaluate (collision, ...) costs
161
+ costs = self._get_costs(x, **observation)
162
+
163
+ # Add smoothness term (prior)
164
+ R_mat = self.Sigma_inv
165
+ smooth_cost = batched_weighted_dot_prod(x, R_mat, x).sum()
166
+
167
+ costs += self.weight_prior_cost * smooth_cost
168
+
169
+ return costs
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/costs/__init__.py ADDED
File without changes
applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/costs/cost_functions.py ADDED
@@ -0,0 +1,745 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+
3
+ import einops
4
+ import numpy as np
5
+ import torch
6
+ from typing import List, Optional
7
+
8
+ from mp_baselines.planners.chomp import CHOMP
9
+ from mp_baselines.planners.costs.factors.field_factor import FieldFactor
10
+ from mp_baselines.planners.costs.factors.gp_factor import GPFactor
11
+ from mp_baselines.planners.costs.factors.unary_factor import UnaryFactor
12
+ from torch_robotics.torch_kinematics_tree.geometrics.utils import link_pos_from_link_tensor
13
+ from torch_robotics.torch_planning_objectives.fields.distance_fields import interpolate_points_v1
14
+ from torch_robotics.torch_utils.torch_utils import batched_weighted_dot_prod
15
+ from torch_robotics.trajectory.utils import finite_difference_vector
16
+
17
+
18
+ class Cost(ABC):
19
+ def __init__(self, robot, n_support_points, tensor_args=None, **kwargs):
20
+ self.robot = robot
21
+ self.n_dof = robot.q_dim
22
+ self.dim = 2 * self.n_dof # position + velocity
23
+ self.n_support_points = n_support_points
24
+
25
+ self.tensor_args = tensor_args
26
+
27
+ def set_cost_factors(self):
28
+ pass
29
+
30
+ def __call__(self, trajs, **kwargs):
31
+ return self.eval(trajs, **kwargs)
32
+
33
+ @abstractmethod
34
+ def eval(self, trajs, **kwargs):
35
+ pass
36
+
37
+ @abstractmethod
38
+ def get_linear_system(self, trajs, **kwargs):
39
+ pass
40
+
41
+ def get_q_pos_vel_and_fk_map(self, trajs, **kwargs):
42
+ assert trajs.ndim == 3 or trajs.ndim == 4
43
+ N = 1
44
+ if trajs.ndim == 4:
45
+ N, B, H, D = trajs.shape # n_goals (or steps), batch of trajectories, length, dim
46
+ trajs = einops.rearrange(trajs, 'N B H D -> (N B) H D')
47
+ else:
48
+ B, H, D = trajs.shape
49
+
50
+ q_pos = self.robot.get_position(trajs)
51
+ q_vel = self.robot.get_velocity(trajs)
52
+ H_positions = self.robot.fk_map_collision(
53
+ q_pos) # I, taskspaces, x_dim+1, x_dim+1 (homogeneous transformation matrices)
54
+ return trajs, q_pos, q_vel, H_positions
55
+
56
+
57
+ class CostComposite(Cost):
58
+
59
+ def __init__(
60
+ self,
61
+ robot,
62
+ n_support_points,
63
+ cost_list,
64
+ weights_cost_l=None,
65
+ **kwargs
66
+ ):
67
+ super().__init__(robot, n_support_points, **kwargs)
68
+ self.cost_l = cost_list
69
+ self.weight_cost_l = weights_cost_l if weights_cost_l is not None else [1.0] * len(cost_list)
70
+
71
+ def eval(self, trajs, trajs_interpolated=None, return_invidual_costs_and_weights=False, **kwargs):
72
+ trajs, q_pos, q_vel, H_positions = self.get_q_pos_vel_and_fk_map(trajs)
73
+
74
+ if not return_invidual_costs_and_weights:
75
+ cost_total = 0
76
+ for cost, weight_cost in zip(self.cost_l, self.weight_cost_l):
77
+ if trajs_interpolated is not None:
78
+ # Compute only collision costs with interpolated trajectories.
79
+ # Other costs are computed with non-interpolated trajectories, e.g. smoothness
80
+ if isinstance(cost, CostCollision):
81
+ trajs_tmp = trajs_interpolated
82
+ else:
83
+ trajs_tmp = trajs
84
+ else:
85
+ trajs_tmp = trajs
86
+ cost_tmp = weight_cost * cost(trajs_tmp, q_pos=q_pos, q_vel=q_vel, H_positions=H_positions, **kwargs)
87
+ cost_total += cost_tmp
88
+ return cost_total
89
+ else:
90
+ cost_l = []
91
+ for cost in self.cost_l:
92
+ if trajs_interpolated is not None:
93
+ # Compute only collision costs with interpolated trajectories.
94
+ # Other costs are computed with non-interpolated trajectories, e.g. smoothness
95
+ if isinstance(cost, CostCollision):
96
+ trajs_tmp = trajs_interpolated
97
+ else:
98
+ trajs_tmp = trajs
99
+ else:
100
+ trajs_tmp = trajs
101
+
102
+ cost_tmp = cost(trajs_tmp, q_pos=q_pos, q_vel=q_vel, H_positions=H_positions, **kwargs)
103
+ cost_l.append(cost_tmp)
104
+
105
+ if return_invidual_costs_and_weights:
106
+ return cost_l, self.weight_cost_l
107
+
108
+ def get_linear_system(self, trajs, n_interpolated_points=None, **kwargs):
109
+ trajs.requires_grad = True
110
+ # TODO - join fk map into one call for trajs and trajs_interp
111
+ trajs, q_pos, q_vel, H_positions = self.get_q_pos_vel_and_fk_map(trajs)
112
+
113
+ # Upsample trajectory for finer collision checking
114
+ # Interpolate in joint space
115
+ # TODO - change from linear interpolation to GP interpolation
116
+ if n_interpolated_points is None:
117
+ trajs_interp, q_pos_interp, q_vel_interp, H_positions_interp = None, None, None, None
118
+ else:
119
+ trajs_interp = interpolate_points_v1(trajs, n_interpolated_points)
120
+ trajs_interp, q_pos_interp, q_vel_interp, H_positions_interp = self.get_q_pos_vel_and_fk_map(trajs_interp)
121
+
122
+ batch_size = trajs.shape[0]
123
+ As, bs, Ks = [], [], []
124
+ optim_dim = 0
125
+ for cost, weight_cost in zip(self.cost_l, self.weight_cost_l):
126
+ A, b, K = cost.get_linear_system(
127
+ trajs, q_pos=q_pos, q_vel=q_vel, H_positions=H_positions,
128
+ trajs_interp=trajs_interp, q_pos_interp=q_pos_interp, q_vel_interp=q_vel_interp,
129
+ H_positions_interp=H_positions_interp,
130
+ **kwargs)
131
+ if A is None or b is None or K is None:
132
+ continue
133
+ optim_dim += A.shape[1]
134
+ As.append(A.detach())
135
+ bs.append(b.detach())
136
+ Ks.append(K.detach())
137
+
138
+ A = torch.cat(As, dim=1)
139
+ b = torch.cat(bs, dim=1)
140
+ K = torch.zeros(batch_size, optim_dim, optim_dim, **self.tensor_args)
141
+ offset = 0
142
+ for i in range(len(Ks)):
143
+ dim = Ks[i].shape[1]
144
+ K[:, offset:offset + dim, offset:offset + dim] = Ks[i]
145
+ offset += dim
146
+ return A, b, K
147
+
148
+
149
+ class CostCollision(Cost):
150
+
151
+ def __init__(
152
+ self,
153
+ robot,
154
+ n_support_points,
155
+ field=None,
156
+ sigma_coll=None,
157
+ **kwargs
158
+ ):
159
+ super().__init__(robot, n_support_points, **kwargs)
160
+ self.obst_factor = None
161
+ self.field = field
162
+ self.sigma_coll = sigma_coll
163
+
164
+ self.set_cost_factors()
165
+
166
+ def set_cost_factors(self):
167
+ # ========= Cost factors ===============
168
+ self.obst_factor = FieldFactor(
169
+ self.n_dof,
170
+ self.sigma_coll,
171
+ # [45, 50] # Can consider also only a subset of the trajectory.
172
+ [1, None] # Take the whole trajectory except for the first point.
173
+ )
174
+
175
+ def eval(self, trajs, q_pos=None, q_vel=None, H_positions=None, **observation):
176
+ costs = 0
177
+ if self.field is not None:
178
+ # H_pos = link_pos_from_link_tensor(H) # get translation part from transformation matrices
179
+ H_pos = H_positions
180
+ err_obst = self.obst_factor.get_error(
181
+ trajs,
182
+ self.field,
183
+ q_pos=q_pos,
184
+ q_vel=q_vel,
185
+ H_pos=H_pos,
186
+ calc_jacobian=False, # TODO: NOTE(an): no need for grads in StochGPMP
187
+ obstacle_spheres=observation.get('obstacle_spheres', None)
188
+ )
189
+ w_mat = self.obst_factor.K
190
+ obst_costs = w_mat * err_obst.sum(1)
191
+ costs = obst_costs
192
+
193
+ return costs
194
+
195
+ def get_linear_system(self, trajs, q_pos=None, q_vel=None, H_positions=None,
196
+ trajs_interp=None, q_pos_interp=None, q_vel_interp=None, H_positions_interp=None,
197
+ **observation):
198
+ A, b, K = None, None, None
199
+ if self.field is not None:
200
+ batch_size = trajs.shape[0]
201
+ # H_pos = link_pos_from_link_tensor(H) # get translation part from transformation matrices
202
+ # H_pos = link_pos_from_link_tensor(H) # get translation part from transformation matrices
203
+ H_pos = H_positions
204
+
205
+ # Get H_obst wrt to the interpolated trajectory. This is computed inside get_error
206
+
207
+ err_obst, H_obst = self.obst_factor.get_error(
208
+ trajs,
209
+ self.field,
210
+ q_pos=q_pos,
211
+ q_vel=q_vel,
212
+ H_pos=H_pos,
213
+ trajs_interp=trajs_interp,
214
+ q_pos_interp=q_pos_interp,
215
+ q_vel_interp=q_vel_interp,
216
+ H_pos_interp=H_positions_interp,
217
+ calc_jacobian=True,
218
+ obstacle_spheres=observation.get('obstacle_spheres', None)
219
+ )
220
+
221
+ A = torch.zeros(batch_size, self.n_support_points - 1, self.dim * self.n_support_points, **self.tensor_args)
222
+ A[:, :, :H_obst.shape[-1]] = H_obst
223
+ # shift each row by self.dim
224
+ idxs = torch.arange(A.shape[-1], **self.tensor_args).repeat(A.shape[-2], 1)
225
+ idxs = (idxs - torch.arange(self.dim, (idxs.shape[0] + 1) * self.dim, self.dim, **self.tensor_args).view(-1, 1)) % idxs.shape[-1]
226
+ idxs = idxs.to(torch.int64)
227
+ A = torch.gather(A, -1, idxs.repeat(batch_size, 1, 1))
228
+
229
+ # old code not vectorized
230
+ # https://github.com/anindex/stoch_gpmp/blob/main/stoch_gpmp/costs/cost_functions.py#L275
231
+
232
+ b = err_obst.unsqueeze(-1)
233
+ K = self.obst_factor.K * torch.eye((self.n_support_points - 1), **self.tensor_args).repeat(batch_size, 1, 1)
234
+
235
+ return A, b, K
236
+
237
+
238
+ # class CostConstraint(Cost):
239
+ # """
240
+ # Currently implementing a "vertex constraint."
241
+ # Avoid this configuration at this time (interval).
242
+ # """
243
+ # def __init__(
244
+ # self,
245
+ # robot,
246
+ # n_support_points,
247
+ # q: torch.Tensor, # Shape: (n_dof,)
248
+ # traj_range: tuple,
249
+ # radius: float,
250
+ # sigma=None,
251
+ # **kwargs
252
+ # ):
253
+ # super().__init__(robot, n_support_points, **kwargs)
254
+ # self.q = q.to(**self.tensor_args)
255
+ # self.traj_range = traj_range
256
+ # self.radius = radius
257
+ # self.sigma = sigma
258
+ #
259
+ # def eval(self, trajs, **observation):
260
+ # costs = 0
261
+ # if self.q is not None:
262
+ # q_pos = self.robot.get_position(trajs)
263
+ # q_pos = q_pos[:, self.traj_range[0]:self.traj_range[1], :]
264
+ # dist_constraint = torch.norm(q_pos - self.q, dim=-1)
265
+ # mask = dist_constraint > self.radius
266
+ # dist_constraint = torch.where(mask, torch.zeros_like(dist_constraint), dist_constraint)
267
+ # costs = self.radius - dist_constraint
268
+ # costs = costs.sum(-1)
269
+ # return costs
270
+ #
271
+ # def get_linear_system(self, trajs, **kwargs):
272
+ # pass
273
+
274
+
275
+ class CostConstraint(Cost):
276
+ """
277
+ Implements multiple "vertex constraints" in a fully vectorized manner.
278
+ Each constraint is associated with a specific configuration and time interval.
279
+ """
280
+
281
+ def __init__(
282
+ self,
283
+ robot,
284
+ n_support_points,
285
+ q_l: List[torch.Tensor], # Length n.
286
+ traj_range_l: List[tuple], # Length n. A tuple (start, end) for each constraint. Inclusive.
287
+ radius_l: List[float], # Length n. The radius for each constraint.
288
+ is_soft: bool = False, # Sigma stands for the standard deviation of the cost.
289
+ **kwargs
290
+ ):
291
+ super().__init__(robot, n_support_points, **kwargs)
292
+ self.qs = torch.stack([q.to(**self.tensor_args) for q in q_l], dim=0) # (n, q_dim)
293
+ self.traj_ranges = torch.tensor(traj_range_l, **self.tensor_args) # (n, 2)
294
+ self.radii = torch.tensor(radius_l, device=self.qs.device) # (n,)
295
+ self.is_soft = is_soft
296
+
297
+ def eval(self, trajs, **observation):
298
+ q_pos = self.robot.get_position(trajs) # (B, H, q_dim)
299
+ # Get the start and end indices for each range.
300
+ start_indices = self.traj_ranges[:, 0] # (n,)
301
+ end_indices = self.traj_ranges[:, 1] # (n,)
302
+
303
+ # Prepare a mask to extract relevant positions in the trajectory for each constraint.
304
+ mask = torch.arange(q_pos.shape[1], device=q_pos.device).unsqueeze(0).unsqueeze(0) # (1, 1, H)
305
+ mask = (mask >= start_indices.view(-1, 1, 1)) & (mask < end_indices.view(-1, 1, 1)) # (n, 1, H)
306
+
307
+ # Repeat and reshape q_pos to match the constraints dimensions.
308
+ q_pos = q_pos.unsqueeze(0).expand(self.qs.shape[0], -1, -1, -1) # (n, B, H, q_dim)
309
+ q_pos_masked = q_pos * mask.unsqueeze(-1) # (n, B, H, q_dim)
310
+
311
+ # Compute the distance between each position in the range and the corresponding q.
312
+ dist_constraint = torch.norm(q_pos_masked - self.qs.view(-1, 1, 1, q_pos.shape[-1]),
313
+ dim=-1) # (n, B, H)
314
+
315
+ # Apply the radius constraints.
316
+ mask_radius = dist_constraint > self.radii.view(-1, 1, 1) # (n, B, H)
317
+ dist_constraint = torch.where(mask_radius, torch.zeros_like(dist_constraint),
318
+ dist_constraint) # Only penalize inside the radius
319
+
320
+ # Compute the cost.
321
+ costs = (self.radii.view(-1, 1, 1) - dist_constraint).sum(dim=-1) # (n, B)
322
+
323
+ # Sum the costs across all constraints.
324
+ total_costs = costs.sum() # (1,)
325
+
326
+ return total_costs
327
+
328
+ def get_linear_system(self, trajs, **kwargs):
329
+ pass
330
+
331
+
332
+ class CostMaxVelocity(Cost):
333
+ def __init__(
334
+ self,
335
+ robot,
336
+ n_support_points,
337
+ max_vel=None,
338
+ sigma_max_vel=None,
339
+ **kwargs
340
+ ):
341
+ super().__init__(robot, n_support_points, **kwargs)
342
+ self.max_vel = max_vel
343
+ self.sigma_max_vel = sigma_max_vel
344
+
345
+ def eval(self, trajs, **observation):
346
+ costs = 0
347
+ q_pos_trajs = self.robot.get_position(trajs)
348
+ if self.max_vel is not None:
349
+ vel = finite_difference_vector(q_pos_trajs, dt=self.robot.dt, method='central')
350
+ cost = torch.pow(vel - self.max_vel, 2)
351
+ costs = cost.sum(-1)
352
+ return costs
353
+
354
+ def get_linear_system(self, trajs, **observation):
355
+ pass
356
+
357
+
358
+ class CostVelocityAndDirectionAlignment(Cost):
359
+ def __init__(
360
+ self,
361
+ robot,
362
+ n_support_points,
363
+ **kwargs
364
+ ):
365
+ super().__init__(robot, n_support_points, **kwargs)
366
+
367
+ def eval(self, trajs, **observation):
368
+ q_direction_trajs = self.robot.get_direction_vector(trajs)
369
+ q_spatial = self.robot.get_spatial_component(trajs)
370
+ q_spatial_vel = finite_difference_vector(q_spatial, dt=self.robot.dt, method='central')
371
+ q_spatial_vel = q_spatial_vel / torch.norm(q_spatial_vel, dim=-1, keepdim=True)
372
+ cost = torch.sum(q_direction_trajs * q_spatial_vel, dim=-1) # Just dot.
373
+ costs = cost.sum(-1)
374
+ return costs
375
+
376
+ def get_linear_system(self, trajs, **observation):
377
+ batch_size = trajs.shape[0]
378
+ dim = trajs.shape[-1]
379
+
380
+ # Initialize the linear system components A, b, and K.
381
+ A = torch.zeros(batch_size, dim * self.n_support_points, dim * self.n_support_points, **self.tensor_args)
382
+ b = torch.zeros(batch_size, dim * self.n_support_points, 1, **self.tensor_args)
383
+ K = torch.zeros(batch_size, dim * self.n_support_points, dim * self.n_support_points, **self.tensor_args)
384
+
385
+ # Get direction and velocity trajectories
386
+ q_direction_trajs = self.robot.get_direction_vector(trajs)
387
+ q_spatial = self.robot.get_spatial_component(trajs)
388
+ q_spatial_vel = finite_difference_vector(q_spatial, dt=self.robot.dt, method='central')
389
+ q_spatial_vel = q_spatial_vel / torch.norm(q_spatial_vel, dim=-1, keepdim=True)
390
+
391
+ # Calculate the error as the difference between direction and normalized velocity vectors
392
+ error = q_direction_trajs - q_spatial_vel
393
+
394
+ # Now, populate A, b, and K matrices.
395
+ # A will represent the linearized relationship between the deviation and the state
396
+ # b will represent the error between the direction and velocity
397
+ # K will represent a weighting matrix for this cost (often identity or diagonal)
398
+
399
+ # For simplicity, we assume a diagonal weighting matrix (you can adjust based on problem setup).
400
+ K_weight = torch.eye(dim, **self.tensor_args)
401
+ K_weight = K_weight.unsqueeze(0).repeat(batch_size, self.n_support_points, 1, 1)
402
+
403
+ # Fill in the matrices with appropriate dimensions
404
+ for i in range(self.n_support_points):
405
+ idx_start = i * dim
406
+ idx_end = (i + 1) * dim
407
+
408
+ # Populate A matrix for this support point (Jacobian of the error wrt the state)
409
+ A[:, idx_start:idx_end, idx_start:idx_end] = K_weight[:, i]
410
+
411
+ # Populate b with the error at this support point
412
+ b[:, idx_start:idx_end] = error[:, i].unsqueeze(-1)
413
+
414
+ # Populate K with the weighting matrix
415
+ K[:, idx_start:idx_end, idx_start:idx_end] = K_weight[:, i]
416
+
417
+ return A, b, K
418
+
419
+
420
+ class CostGP(Cost):
421
+
422
+ def __init__(
423
+ self,
424
+ robot,
425
+ n_support_points,
426
+ start_state,
427
+ dt,
428
+ sigma_params,
429
+ **kwargs
430
+ ):
431
+ super().__init__(robot, n_support_points, **kwargs)
432
+ self.start_state = start_state
433
+ self.dt = dt
434
+
435
+ self.sigma_start = sigma_params['sigma_start']
436
+ self.sigma_gp = sigma_params['sigma_gp']
437
+
438
+ self.set_cost_factors()
439
+
440
+ def set_cost_factors(self):
441
+ # ========= Cost factors ===============
442
+ self.start_prior = UnaryFactor(
443
+ self.dim,
444
+ self.sigma_start,
445
+ self.start_state,
446
+ self.tensor_args,
447
+ )
448
+
449
+ self.gp_prior = GPFactor(
450
+ self.n_dof,
451
+ self.sigma_gp,
452
+ self.dt,
453
+ self.n_support_points - 1,
454
+ self.tensor_args,
455
+ )
456
+
457
+ def eval(self, trajs, **observation):
458
+ # trajs = trajs.reshape(-1, self.n_support_points, self.dim)
459
+ # Start cost
460
+ err_p = self.start_prior.get_error(trajs[:, [0]], calc_jacobian=False)
461
+ w_mat = self.start_prior.K
462
+ start_costs = err_p @ w_mat.unsqueeze(0) @ err_p.transpose(1, 2)
463
+ start_costs = start_costs.squeeze()
464
+
465
+ # GP Trajectory cost
466
+ err_gp = self.gp_prior.get_error(trajs, calc_jacobian=False)
467
+ w_mat = self.gp_prior.Q_inv[0] # repeated Q_inv
468
+ w_mat = w_mat.reshape(1, 1, self.dim, self.dim)
469
+ gp_costs = err_gp.transpose(2, 3) @ w_mat @ err_gp
470
+ gp_costs = gp_costs.sum(1)
471
+ gp_costs = gp_costs.squeeze()
472
+
473
+ costs = start_costs + gp_costs
474
+
475
+ return costs
476
+
477
+ def get_linear_system(self, trajs, **observation):
478
+ batch_size = trajs.shape[0]
479
+ A = torch.zeros(batch_size, self.dim * self.n_support_points, self.dim * self.n_support_points,
480
+ **self.tensor_args)
481
+ b = torch.zeros(batch_size, self.dim * self.n_support_points, 1, **self.tensor_args)
482
+ K = torch.zeros(batch_size, self.dim * self.n_support_points, self.dim * self.n_support_points,
483
+ **self.tensor_args)
484
+
485
+ # Start prior factor
486
+ err_p, H_p = self.start_prior.get_error(trajs[:, [0]])
487
+ A[:, :self.dim, :self.dim] = H_p
488
+ b[:, :self.dim] = err_p
489
+ K[:, :self.dim, :self.dim] = self.start_prior.K
490
+
491
+ # GP factors
492
+ err_gp, H1_gp, H2_gp = self.gp_prior.get_error(trajs)
493
+
494
+ A[:, self.dim:, :-self.dim] = torch.block_diag(*H1_gp)
495
+ A[:, self.dim:, self.dim:] += torch.block_diag(*H2_gp)
496
+ b[:, self.dim:] = einops.rearrange(err_gp, "b h d 1 -> b (h d) 1")
497
+ K[:, self.dim:, self.dim:] += torch.block_diag(*self.gp_prior.Q_inv)
498
+
499
+ # old code not vectorized
500
+ # https://github.com/anindex/stoch_gpmp/blob/main/stoch_gpmp/costs/cost_functions.py#L161
501
+
502
+ return A, b, K
503
+
504
+
505
+ class CostGPTrajectory(Cost):
506
+
507
+ def __init__(
508
+ self,
509
+ robot,
510
+ n_support_points,
511
+ dt,
512
+ sigma_gp=None,
513
+ **kwargs
514
+ ):
515
+ super().__init__(robot, n_support_points, **kwargs)
516
+ self.dt = dt
517
+
518
+ self.sigma_gp = sigma_gp
519
+
520
+ self.set_cost_factors()
521
+
522
+ def set_cost_factors(self):
523
+ # ========= Cost factors ===============
524
+ self.gp_prior = GPFactor(
525
+ self.n_dof,
526
+ self.sigma_gp,
527
+ self.dt,
528
+ self.n_support_points - 1,
529
+ self.tensor_args,
530
+ )
531
+
532
+ def eval(self, trajs, **observation):
533
+ # trajs = trajs.reshape(-1, self.n_support_points, self.dim)
534
+
535
+ # GP cost
536
+ err_gp = self.gp_prior.get_error(trajs, calc_jacobian=False)
537
+ w_mat = self.gp_prior.Q_inv[0] # repeated Q_inv
538
+ w_mat = w_mat.reshape(1, 1, self.dim, self.dim)
539
+ gp_costs = err_gp.transpose(2, 3) @ w_mat @ err_gp
540
+ gp_costs = gp_costs.sum(1).squeeze()
541
+ costs = gp_costs
542
+ return costs
543
+
544
+ def get_linear_system(self, trajs, **observation):
545
+ pass
546
+
547
+
548
+ class CostGPTrajectoryPositionOnlyWrapper(CostGPTrajectory):
549
+
550
+ def __init__(self, *args, **kwargs):
551
+ super().__init__(*args, **kwargs)
552
+
553
+ def eval(self, trajs, **observation):
554
+ vel = finite_difference_vector(trajs, dt=self.dt, method='central')
555
+ trajs_tmp = torch.cat((trajs, vel), dim=-1)
556
+ return super().eval(trajs_tmp, **observation)
557
+
558
+
559
+ class CostSmoothnessCHOMP(Cost):
560
+
561
+ def __init__(
562
+ self,
563
+ robot,
564
+ n_support_points,
565
+ **kwargs
566
+ ):
567
+ super().__init__(robot, n_support_points, **kwargs)
568
+ self.dt = robot.dt
569
+
570
+ self.Sigma_inv = CHOMP._get_R_mat(dt=self.dt, n_support_points=n_support_points, **kwargs)
571
+
572
+ def eval(self, trajs, **observation):
573
+ R_mat = self.Sigma_inv
574
+ cost = batched_weighted_dot_prod(trajs, R_mat, trajs)
575
+ return cost
576
+
577
+ def get_linear_system(self, trajs, **observation):
578
+ pass
579
+
580
+
581
+ class CostJointLimits(Cost):
582
+
583
+ def __init__(
584
+ self,
585
+ robot,
586
+ n_support_points,
587
+ eps=np.deg2rad(3),
588
+ **kwargs
589
+ ):
590
+ super().__init__(robot, n_support_points, **kwargs)
591
+
592
+ self.eps = eps
593
+
594
+ def eval(self, trajs, **observation):
595
+ assert trajs.ndim == 3
596
+
597
+ # trajs = trajs.reshape(-1, self.n_support_points, self.dim)
598
+ trajs_pos = self.robot.get_position(trajs)
599
+
600
+ idxs_lower = torch.argwhere(trajs_pos < self.robot.q_min + self.eps)
601
+ cost_lower = torch.pow(
602
+ self.robot.q_min[idxs_lower[:, 2]] + self.eps - trajs_pos[
603
+ idxs_lower[:, 0], idxs_lower[:, 1], idxs_lower[:, 2]],
604
+ 2
605
+ ).sum(-1)
606
+
607
+ idxs_upper = torch.argwhere(trajs_pos > self.robot.q_max - self.eps)
608
+ cost_upper = torch.pow(
609
+ self.robot.q_max[idxs_upper[:, 2]] - self.eps - trajs_pos[
610
+ idxs_upper[:, 0], idxs_upper[:, 1], idxs_upper[:, 2]],
611
+ 2
612
+ ).sum(-1)
613
+
614
+ costs = (cost_lower + cost_upper)
615
+
616
+ return costs
617
+
618
+ def get_linear_system(self, trajs, **observation):
619
+ pass
620
+
621
+
622
+ class CostGoal(Cost):
623
+
624
+ def __init__(
625
+ self,
626
+ robot,
627
+ n_support_points,
628
+ field=None,
629
+ sigma_goal=None,
630
+ **kwargs
631
+ ):
632
+ super().__init__(robot, n_support_points, **kwargs)
633
+ self.field = field
634
+ self.sigma_goal = sigma_goal
635
+
636
+ self.set_cost_factors()
637
+
638
+ def set_cost_factors(self):
639
+ #========= Cost factors ===============
640
+ self.goal_factor = FieldFactor(
641
+ self.n_dof,
642
+ self.sigma_goal,
643
+ [-1, None] # only take last point
644
+ )
645
+
646
+ def eval(self, trajs, x_trajs=None, **observation):
647
+ costs = 0
648
+ if self.field is not None:
649
+ err_obst = self.goal_factor.get_error(
650
+ trajs,
651
+ self.field,
652
+ x_trajs=x_trajs,
653
+ calc_jacobian=False, # NOTE(an): no need for grads in StochGPMP
654
+ )
655
+ w_mat = self.goal_factor.K
656
+ obst_costs = w_mat * err_obst.sum(1)
657
+ costs = obst_costs
658
+
659
+ return costs
660
+
661
+ def get_linear_system(self, trajs, x_trajs=None, **observation):
662
+ A, b, K = None, None, None
663
+ if self.field is not None:
664
+ batch_size = trajs.shape[0]
665
+ A = torch.zeros(batch_size, 1, self.dim * self.n_support_points, **self.tensor_args)
666
+ err_goal, H_goal = self.goal_factor.get_error(
667
+ trajs,
668
+ self.field,
669
+ x_trajs=x_trajs,
670
+ calc_jacobian=True,
671
+ )
672
+ A[:, :, -self.dim:(-self.dim + self.n_dof)] = H_goal
673
+ b = err_goal.unsqueeze(-1)
674
+ K = self.goal_factor.K * torch.eye(1, **self.tensor_args).repeat(batch_size, 1, 1)
675
+ return A, b, K
676
+
677
+
678
+ class CostGoalPrior(Cost):
679
+
680
+ def __init__(
681
+ self,
682
+ robot,
683
+ n_support_points,
684
+ multi_goal_states=None, # num_goal x n_dim (pos + vel)
685
+ num_particles_per_goal=None,
686
+ num_samples=None,
687
+ sigma_goal_prior=None,
688
+ **kwargs
689
+ ):
690
+ super().__init__(robot, n_support_points, **kwargs)
691
+ self.multi_goal_states = multi_goal_states
692
+ self.num_goals = multi_goal_states.shape[0]
693
+ self.num_particles_per_goal = num_particles_per_goal
694
+ self.num_particles = num_particles_per_goal * self.num_goals
695
+ self.num_samples = num_samples
696
+ self.sigma_goal_prior = sigma_goal_prior
697
+
698
+ self.set_cost_factors()
699
+
700
+ def set_cost_factors(self):
701
+ self.multi_goal_prior = []
702
+ # TODO: remove this for loop
703
+ for i in range(self.num_goals):
704
+ self.multi_goal_prior.append(
705
+ UnaryFactor(
706
+ self.dim,
707
+ self.sigma_goal_prior,
708
+ self.multi_goal_states[i],
709
+ self.tensor_args,
710
+ )
711
+ )
712
+
713
+ def eval(self, trajs, **observation):
714
+ costs = 0
715
+ if self.multi_goal_states is not None:
716
+ x = trajs.reshape(self.num_goals, self.num_particles_per_goal * self.num_samples, self.n_support_points,
717
+ self.dim)
718
+ costs = torch.zeros(self.num_goals, self.num_particles_per_goal * self.num_samples, **self.tensor_args)
719
+ # TODO: remove this for loop
720
+ for i in range(self.num_goals):
721
+ err_g = self.multi_goal_prior[i].get_error(x[i, :, [-1]], calc_jacobian=False)
722
+ w_mat = self.multi_goal_prior[i].K
723
+ goal_costs = err_g @ w_mat.unsqueeze(0) @ err_g.transpose(1, 2)
724
+ goal_costs = goal_costs.squeeze()
725
+ costs[i] += goal_costs
726
+ costs = costs.flatten()
727
+ return costs
728
+
729
+ def get_linear_system(self, trajs, **observation):
730
+ A, b, K = None, None, None
731
+ if self.multi_goal_states is not None:
732
+ npg = self.num_particles_per_goal
733
+ batch_size = npg * self.num_goals
734
+ x = trajs.reshape(self.num_goals, self.num_particles_per_goal, self.n_support_points, self.dim)
735
+ A = torch.zeros(batch_size, self.dim, self.dim * self.n_support_points, **self.tensor_args)
736
+ b = torch.zeros(batch_size, self.dim, 1, **self.tensor_args)
737
+ K = torch.zeros(batch_size, self.dim, self.dim, **self.tensor_args)
738
+ # TODO: remove this for loop
739
+ for i in range(self.num_goals):
740
+ err_g, H_g = self.multi_goal_prior[i].get_error(x[i, :, [-1]])
741
+ A[i * npg: (i + 1) * npg, :, -self.dim:] = H_g
742
+ b[i * npg: (i + 1) * npg] = err_g
743
+ K[i * npg: (i + 1) * npg] = self.multi_goal_prior[i].K
744
+
745
+ return A, b, K