Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- applications/multi_robot/README.md +32 -0
- applications/multi_robot/robotarium/README.md +114 -0
- applications/multi_robot/robotarium/batch.py +231 -0
- applications/multi_robot/robotarium/evaluate.py +208 -0
- applications/multi_robot/robotarium/figures.py +261 -0
- applications/multi_robot/robotarium/instances.py +185 -0
- applications/multi_robot/robotarium/projector.py +535 -0
- applications/multi_robot/robotarium/robotarium_baselines/.gitignore +6 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/.gitignore +43 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/LICENSE +28 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/README.md +209 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/analysis_config.yaml +63 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/analyze_mmd.yaml +59 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_conveyor.yaml +79 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_dropregion.yaml +79 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_empty.yaml +81 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/direct_highway.yaml +80 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_conveyor.yaml +90 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_dropregion.yaml +76 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_empty.yaml +77 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/configs/pcdiff_highway.yaml +76 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/LICENSE +21 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/MANIFEST.in +2 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/Makefile +14 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/README.md +111 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/basic/launch_test.py +75 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/basic/test.py +47 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/configs/config00.yaml +7 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/configs/config01.yaml +7 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/launch_test.py +65 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/config_files/test.py +49 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/multi_exp_same_gpu/launch_test.py +60 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/multi_exp_same_gpu/test.py +54 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/start_seed/launch_test.py +98 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/examples/start_seed/test.py +47 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/__init__.py +8 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/decorators.py +49 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/exceptions.py +10 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/launcher.py +495 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/experiment_launcher/utils.py +117 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/requirements.txt +5 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/experiment_launcher/setup.py +35 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/LICENSE +24 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/README.md +54 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/__init__.py +0 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/__init__.py +0 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/base.py +226 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/chomp.py +169 -0
- applications/multi_robot/robotarium/robotarium_baselines/DiRecT/deps/motion_planning_baselines/mp_baselines/planners/costs/__init__.py +0 -0
- 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 |
+

|
| 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
|