batches 0.11.0b1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. batches-0.11.0b1/AGENTS.md +130 -0
  2. batches-0.11.0b1/CHANGELOG.md +253 -0
  3. batches-0.11.0b1/CONTRIBUTING.md +121 -0
  4. batches-0.11.0b1/Cargo.lock +2293 -0
  5. batches-0.11.0b1/Cargo.toml +86 -0
  6. batches-0.11.0b1/LICENSE-APACHE +202 -0
  7. batches-0.11.0b1/LICENSE-MIT +21 -0
  8. batches-0.11.0b1/PKG-INFO +25 -0
  9. batches-0.11.0b1/PYPI_README.md +13 -0
  10. batches-0.11.0b1/README.md +330 -0
  11. batches-0.11.0b1/assets/README.md +35 -0
  12. batches-0.11.0b1/assets/franka/LICENSE +178 -0
  13. batches-0.11.0b1/assets/franka/franka_panda.urdf +318 -0
  14. batches-0.11.0b1/assets/franka/meshes/collision/finger.obj +152 -0
  15. batches-0.11.0b1/assets/franka/meshes/collision/hand.obj +622 -0
  16. batches-0.11.0b1/assets/franka/meshes/collision/link0.obj +568 -0
  17. batches-0.11.0b1/assets/franka/meshes/collision/link1.obj +712 -0
  18. batches-0.11.0b1/assets/franka/meshes/collision/link2.obj +716 -0
  19. batches-0.11.0b1/assets/franka/meshes/collision/link3.obj +702 -0
  20. batches-0.11.0b1/assets/franka/meshes/collision/link4.obj +702 -0
  21. batches-0.11.0b1/assets/franka/meshes/collision/link5.obj +728 -0
  22. batches-0.11.0b1/assets/franka/meshes/collision/link6.obj +502 -0
  23. batches-0.11.0b1/assets/franka/meshes/collision/link7.obj +576 -0
  24. batches-0.11.0b1/assets/franka/panda_collision.json +464 -0
  25. batches-0.11.0b1/assets/franka/usd/Payload/Contents.usda +20 -0
  26. batches-0.11.0b1/assets/franka/usd/Payload/Geometry.usda +408 -0
  27. batches-0.11.0b1/assets/franka/usd/Payload/GeometryLibrary.usdc +0 -0
  28. batches-0.11.0b1/assets/franka/usd/Payload/Physics.usda +450 -0
  29. batches-0.11.0b1/assets/franka/usd/panda.usda +30 -0
  30. batches-0.11.0b1/assets/menagerie/franka_emika_panda/LICENSE +176 -0
  31. batches-0.11.0b1/assets/menagerie/franka_emika_panda/panda.xml +287 -0
  32. batches-0.11.0b1/assets/menagerie/franka_emika_panda/scene.xml +21 -0
  33. batches-0.11.0b1/assets/menagerie/robotiq_2f85/2f85.xml +192 -0
  34. batches-0.11.0b1/assets/menagerie/robotiq_2f85/LICENSE +23 -0
  35. batches-0.11.0b1/assets/menagerie/universal_robots_ur5e/LICENSE +26 -0
  36. batches-0.11.0b1/assets/menagerie/universal_robots_ur5e/ur5e.xml +136 -0
  37. batches-0.11.0b1/assets/newton/robotiq_2f85_v4/LICENSE +23 -0
  38. batches-0.11.0b1/assets/newton/robotiq_2f85_v4/usd_structured/Dual_wrist_camera.usda +29 -0
  39. batches-0.11.0b1/assets/newton/robotiq_2f85_v4/usd_structured/Payload/Contents.usda +20 -0
  40. batches-0.11.0b1/assets/newton/robotiq_2f85_v4/usd_structured/Payload/Geometry.usda +662 -0
  41. batches-0.11.0b1/assets/newton/robotiq_2f85_v4/usd_structured/Payload/GeometryLibrary.usdc +0 -0
  42. batches-0.11.0b1/assets/newton/robotiq_2f85_v4/usd_structured/Payload/Materials.usda +150 -0
  43. batches-0.11.0b1/assets/newton/robotiq_2f85_v4/usd_structured/Payload/MaterialsLibrary.usdc +0 -0
  44. batches-0.11.0b1/assets/newton/robotiq_2f85_v4/usd_structured/Payload/Physics.usda +593 -0
  45. batches-0.11.0b1/assets/newton/universal_robots_ur5e/LICENSE +26 -0
  46. batches-0.11.0b1/assets/newton/universal_robots_ur5e/usd_structured/Payload/Contents.usda +20 -0
  47. batches-0.11.0b1/assets/newton/universal_robots_ur5e/usd_structured/Payload/Geometry.usda +687 -0
  48. batches-0.11.0b1/assets/newton/universal_robots_ur5e/usd_structured/Payload/Materials.usda +205 -0
  49. batches-0.11.0b1/assets/newton/universal_robots_ur5e/usd_structured/Payload/Physics.usda +405 -0
  50. batches-0.11.0b1/assets/newton/universal_robots_ur5e/usd_structured/ur5e.usda +29 -0
  51. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/LICENSE +29 -0
  52. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/left_finger.stl +0 -0
  53. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/left_finger_tip.stl +0 -0
  54. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/left_inner_knuckle.stl +0 -0
  55. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/left_knuckle.stl +0 -0
  56. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/right_finger.stl +0 -0
  57. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/right_finger_tip.stl +0 -0
  58. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/right_inner_knuckle.stl +0 -0
  59. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/right_knuckle.stl +0 -0
  60. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/robotiq_base.stl +0 -0
  61. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/meshes/collision/2f_85/ur_to_robotiq_adapter.stl +0 -0
  62. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/package.xml +34 -0
  63. batches-0.11.0b1/assets/robotiq_2f85/robotiq_description/urdf/robotiq_2f_85.urdf +302 -0
  64. batches-0.11.0b1/assets/so101/LICENSE +201 -0
  65. batches-0.11.0b1/assets/so101/assets/base_motor_holder_so101_v1_hull.stl +0 -0
  66. batches-0.11.0b1/assets/so101/assets/base_so101_v2_hull.stl +0 -0
  67. batches-0.11.0b1/assets/so101/assets/motor_holder_so101_base_v1_hull.stl +0 -0
  68. batches-0.11.0b1/assets/so101/assets/motor_holder_so101_wrist_v1_hull.stl +0 -0
  69. batches-0.11.0b1/assets/so101/assets/moving_jaw_so101_v1_hull.stl +0 -0
  70. batches-0.11.0b1/assets/so101/assets/rotation_pitch_so101_v1_hull.stl +0 -0
  71. batches-0.11.0b1/assets/so101/assets/sts3215_03a_no_horn_v1_hull.stl +0 -0
  72. batches-0.11.0b1/assets/so101/assets/sts3215_03a_v1_hull.stl +0 -0
  73. batches-0.11.0b1/assets/so101/assets/under_arm_so101_v1_hull.stl +0 -0
  74. batches-0.11.0b1/assets/so101/assets/upper_arm_so101_v1_hull.stl +0 -0
  75. batches-0.11.0b1/assets/so101/assets/waveshare_mounting_plate_so101_v2_hull.stl +0 -0
  76. batches-0.11.0b1/assets/so101/assets/wrist_roll_follower_so101_v1_hull.stl +0 -0
  77. batches-0.11.0b1/assets/so101/assets/wrist_roll_pitch_so101_v2_hull.stl +0 -0
  78. batches-0.11.0b1/assets/so101/so101_new_calib.urdf +453 -0
  79. batches-0.11.0b1/assets/ur5e/ur_description/LICENSE +25 -0
  80. batches-0.11.0b1/assets/ur5e/ur_description/meshes/ur5e/collision/base.stl +0 -0
  81. batches-0.11.0b1/assets/ur5e/ur_description/meshes/ur5e/collision/forearm.stl +0 -0
  82. batches-0.11.0b1/assets/ur5e/ur_description/meshes/ur5e/collision/shoulder.stl +0 -0
  83. batches-0.11.0b1/assets/ur5e/ur_description/meshes/ur5e/collision/upperarm.stl +0 -0
  84. batches-0.11.0b1/assets/ur5e/ur_description/meshes/ur5e/collision/wrist1.stl +0 -0
  85. batches-0.11.0b1/assets/ur5e/ur_description/meshes/ur5e/collision/wrist2.stl +0 -0
  86. batches-0.11.0b1/assets/ur5e/ur_description/meshes/ur5e/collision/wrist3.stl +0 -0
  87. batches-0.11.0b1/assets/ur5e/ur_description/package.xml +61 -0
  88. batches-0.11.0b1/assets/ur5e/ur_description/urdf/ur5e.urdf +297 -0
  89. batches-0.11.0b1/assets/usd/arm.usda +28 -0
  90. batches-0.11.0b1/assets/usd/arm_parts.usda +138 -0
  91. batches-0.11.0b1/assets/usd/scene.usda +48 -0
  92. batches-0.11.0b1/batchplan/__init__.py +141 -0
  93. batches-0.11.0b1/clippy.toml +6 -0
  94. batches-0.11.0b1/docs/README.md +26 -0
  95. batches-0.11.0b1/docs/api.md +146 -0
  96. batches-0.11.0b1/docs/architecture.md +150 -0
  97. batches-0.11.0b1/docs/benchmarks.md +771 -0
  98. batches-0.11.0b1/docs/devices.md +69 -0
  99. batches-0.11.0b1/docs/perception.md +151 -0
  100. batches-0.11.0b1/docs/planning.md +453 -0
  101. batches-0.11.0b1/docs/python.md +100 -0
  102. batches-0.11.0b1/docs/robots.md +172 -0
  103. batches-0.11.0b1/docs/training-data.md +227 -0
  104. batches-0.11.0b1/docs/verification.md +163 -0
  105. batches-0.11.0b1/examples/bench.rs +81 -0
  106. batches-0.11.0b1/examples/benchmark.rs +862 -0
  107. batches-0.11.0b1/examples/common/mod.rs +512 -0
  108. batches-0.11.0b1/examples/compare.rs +719 -0
  109. batches-0.11.0b1/examples/constraints.rs +185 -0
  110. batches-0.11.0b1/examples/control_loop.rs +120 -0
  111. batches-0.11.0b1/examples/datagen.rs +133 -0
  112. batches-0.11.0b1/examples/depth.rs +151 -0
  113. batches-0.11.0b1/examples/handover/main.rs +206 -0
  114. batches-0.11.0b1/examples/handover/rebot.rs +265 -0
  115. batches-0.11.0b1/examples/join.rs +170 -0
  116. batches-0.11.0b1/examples/so101_tracking.rs +231 -0
  117. batches-0.11.0b1/examples/tasks.rs +184 -0
  118. batches-0.11.0b1/examples/timing.rs +156 -0
  119. batches-0.11.0b1/examples/tool_paths.rs +86 -0
  120. batches-0.11.0b1/examples/usd_fk.rs +61 -0
  121. batches-0.11.0b1/pyproject.toml +53 -0
  122. batches-0.11.0b1/python/Cargo.lock +1789 -0
  123. batches-0.11.0b1/python/Cargo.toml +46 -0
  124. batches-0.11.0b1/python/LICENSE-APACHE +202 -0
  125. batches-0.11.0b1/python/LICENSE-MIT +21 -0
  126. batches-0.11.0b1/python/PYPI_README.md +13 -0
  127. batches-0.11.0b1/python/src/device.rs +224 -0
  128. batches-0.11.0b1/python/src/error.rs +84 -0
  129. batches-0.11.0b1/python/src/field.rs +361 -0
  130. batches-0.11.0b1/python/src/held.rs +125 -0
  131. batches-0.11.0b1/python/src/ik.rs +339 -0
  132. batches-0.11.0b1/python/src/lib.rs +46 -0
  133. batches-0.11.0b1/python/src/plan.rs +466 -0
  134. batches-0.11.0b1/python/src/robot.rs +475 -0
  135. batches-0.11.0b1/python/src/solved.rs +48 -0
  136. batches-0.11.0b1/python/src/task.rs +501 -0
  137. batches-0.11.0b1/python/src/timing.rs +282 -0
  138. batches-0.11.0b1/python/src/tool_path.rs +241 -0
  139. batches-0.11.0b1/python/src/world.rs +210 -0
  140. batches-0.11.0b1/python/tests/conftest.py +63 -0
  141. batches-0.11.0b1/python/tests/test_docs.py +27 -0
  142. batches-0.11.0b1/python/tests/test_planning.py +568 -0
  143. batches-0.11.0b1/python/tests/test_tasks.py +194 -0
  144. batches-0.11.0b1/python/tests/test_values.py +135 -0
  145. batches-0.11.0b1/python/uv.lock +436 -0
  146. batches-0.11.0b1/rustfmt.toml +4 -0
  147. batches-0.11.0b1/scripts/bench_model.py +364 -0
  148. batches-0.11.0b1/scripts/build_compare_cpp.sh +27 -0
  149. batches-0.11.0b1/scripts/check.sh +29 -0
  150. batches-0.11.0b1/scripts/compare.py +596 -0
  151. batches-0.11.0b1/scripts/compare_cpp.cc +378 -0
  152. batches-0.11.0b1/scripts/compare_planners.py +564 -0
  153. batches-0.11.0b1/scripts/cpp_planners.cc +298 -0
  154. batches-0.11.0b1/scripts/fetch_benchmark.sh +36 -0
  155. batches-0.11.0b1/scripts/fetch_panda_meshes.sh +46 -0
  156. batches-0.11.0b1/scripts/fetch_rebot.sh +86 -0
  157. batches-0.11.0b1/scripts/parity.py +334 -0
  158. batches-0.11.0b1/scripts/replay_mujoco.py +207 -0
  159. batches-0.11.0b1/scripts/report.py +219 -0
  160. batches-0.11.0b1/scripts/report_suite.sh +27 -0
  161. batches-0.11.0b1/scripts/robokit/batchplan_ik.py +130 -0
  162. batches-0.11.0b1/scripts/robokit/batchplan_mp.py +57 -0
  163. batches-0.11.0b1/scripts/robokit/batchplan_plugin.py +145 -0
  164. batches-0.11.0b1/scripts/run_comparison.sh +68 -0
  165. batches-0.11.0b1/scripts/run_suite.sh +29 -0
  166. batches-0.11.0b1/scripts/so101_tracking.py +153 -0
  167. batches-0.11.0b1/scripts/sync.sh +15 -0
  168. batches-0.11.0b1/scripts/test_bench.py +190 -0
  169. batches-0.11.0b1/scripts/validate.py +197 -0
  170. batches-0.11.0b1/scripts/validate_lerobot.py +68 -0
  171. batches-0.11.0b1/scripts/validate_usd.py +199 -0
  172. batches-0.11.0b1/src/constraint.rs +995 -0
  173. batches-0.11.0b1/src/cpu.rs +1999 -0
  174. batches-0.11.0b1/src/datagen.rs +1547 -0
  175. batches-0.11.0b1/src/description.rs +577 -0
  176. batches-0.11.0b1/src/device.rs +636 -0
  177. batches-0.11.0b1/src/edges.rs +604 -0
  178. batches-0.11.0b1/src/error.rs +70 -0
  179. batches-0.11.0b1/src/gpu.rs +2233 -0
  180. batches-0.11.0b1/src/grid.wgsl +44 -0
  181. batches-0.11.0b1/src/grids.wgsl +343 -0
  182. batches-0.11.0b1/src/held.rs +385 -0
  183. batches-0.11.0b1/src/ik.rs +507 -0
  184. batches-0.11.0b1/src/join.rs +813 -0
  185. batches-0.11.0b1/src/kernels.wgsl +1148 -0
  186. batches-0.11.0b1/src/lanes.rs +1347 -0
  187. batches-0.11.0b1/src/lerobot.rs +866 -0
  188. batches-0.11.0b1/src/lib.rs +120 -0
  189. batches-0.11.0b1/src/live.rs +809 -0
  190. batches-0.11.0b1/src/loops.rs +351 -0
  191. batches-0.11.0b1/src/meshes.rs +355 -0
  192. batches-0.11.0b1/src/mjcf.rs +658 -0
  193. batches-0.11.0b1/src/npy.rs +213 -0
  194. batches-0.11.0b1/src/rng.rs +51 -0
  195. batches-0.11.0b1/src/robot.rs +2058 -0
  196. batches-0.11.0b1/src/rrt/ahead.rs +1125 -0
  197. batches-0.11.0b1/src/rrt/connect.rs +1628 -0
  198. batches-0.11.0b1/src/rrt.rs +26 -0
  199. batches-0.11.0b1/src/sdf.rs +1221 -0
  200. batches-0.11.0b1/src/shortcut.rs +606 -0
  201. batches-0.11.0b1/src/spheres.rs +1014 -0
  202. batches-0.11.0b1/src/spline.rs +236 -0
  203. batches-0.11.0b1/src/task.rs +1534 -0
  204. batches-0.11.0b1/src/timing.rs +844 -0
  205. batches-0.11.0b1/src/tool_path.rs +783 -0
  206. batches-0.11.0b1/src/topp.rs +823 -0
  207. batches-0.11.0b1/src/trajopt.rs +2267 -0
  208. batches-0.11.0b1/src/types.rs +148 -0
  209. batches-0.11.0b1/src/urdf.rs +213 -0
  210. batches-0.11.0b1/src/usd.rs +812 -0
  211. batches-0.11.0b1/src/world.rs +664 -0
  212. batches-0.11.0b1/tests/allocations.rs +131 -0
  213. batches-0.11.0b1/tests/attach.rs +370 -0
  214. batches-0.11.0b1/tests/budget.rs +151 -0
  215. batches-0.11.0b1/tests/constraints.rs +820 -0
  216. batches-0.11.0b1/tests/cpu.rs +297 -0
  217. batches-0.11.0b1/tests/device.rs +1604 -0
  218. batches-0.11.0b1/tests/export.rs +638 -0
  219. batches-0.11.0b1/tests/failures.rs +224 -0
  220. batches-0.11.0b1/tests/goals.rs +409 -0
  221. batches-0.11.0b1/tests/gpu.rs +954 -0
  222. batches-0.11.0b1/tests/hands.rs +607 -0
  223. batches-0.11.0b1/tests/held.rs +432 -0
  224. batches-0.11.0b1/tests/join.rs +629 -0
  225. batches-0.11.0b1/tests/live.rs +1201 -0
  226. batches-0.11.0b1/tests/meshes.rs +366 -0
  227. batches-0.11.0b1/tests/mjcf.rs +478 -0
  228. batches-0.11.0b1/tests/pick_place.rs +932 -0
  229. batches-0.11.0b1/tests/robot.rs +1788 -0
  230. batches-0.11.0b1/tests/rrt.rs +792 -0
  231. batches-0.11.0b1/tests/sdf.rs +1246 -0
  232. batches-0.11.0b1/tests/task.rs +1506 -0
  233. batches-0.11.0b1/tests/threads.rs +107 -0
  234. batches-0.11.0b1/tests/tool_path.rs +595 -0
  235. batches-0.11.0b1/tests/touch.rs +414 -0
  236. batches-0.11.0b1/tests/trajectory.rs +1100 -0
  237. batches-0.11.0b1/tests/usd.rs +383 -0
@@ -0,0 +1,130 @@
1
+ # Agent instructions
2
+
3
+ This file is for coding agents that use batchplan in a program. To change batchplan itself, read [`CLAUDE.md`](CLAUDE.md): its design rules, invariants and working rules. The guides under [`docs/`](docs/README.md) explain each topic in full; this page gathers what an agent needs first and links to them. From Python, the `batchplan` package makes the same calls by the same names; [Python](docs/python.md) says how they map and what it binds so far.
4
+
5
+ ## Use batchplan: make once, warm up, reuse
6
+
7
+ Build the robot and its device once, warm the device, upload each scene once, and reuse all three for every request. Results are per problem: read each one's answer or its reason, never assume a batch succeeded together.
8
+
9
+ ```rust,no_run
10
+ use batchplan::glam::{Quat, Vec3};
11
+ use batchplan::{
12
+ Device, Error, Obstacle, PlanFailure, PlanOptions, PlanProblem, Pose, Robot, RobotOptions,
13
+ Trajectory, World,
14
+ };
15
+
16
+ fn main() -> Result<(), Error> {
17
+ // Once per robot: load it, check its spheres, make its device and warm it up.
18
+ let options = RobotOptions {
19
+ ee_links: vec!["tool0".into()],
20
+ ..Default::default()
21
+ };
22
+ let robot = Robot::load("assets/ur5e/ur_description/urdf/ur5e.urdf", &options)?;
23
+ for link in robot.qualify()?.links {
24
+ println!("{}: spheres reach {:.1} mm past it", link.link, link.overhang * 1e3);
25
+ }
26
+ let device = Device::gpu(&robot).or_else(|_| Device::cpu(&robot))?;
27
+ device.warmup()?;
28
+
29
+ // Once per scene, and again whenever it changes: obstacles in the robot's frame, with the
30
+ // links that always touch the table it stands on allowed to.
31
+ let table = Obstacle::cuboid(Vec3::new(0.3, 0.0, -0.02), Vec3::new(0.6, 0.6, 0.02));
32
+ let mut world = World::new(vec![table]);
33
+ let mounted = world.always_touching(&robot);
34
+ world.touch.extend(mounted);
35
+ let worlds = device.upload(&[world])?;
36
+
37
+ // Per request: a pose in the robot's frame (quaternions x, y, z, w), from the measured state
38
+ // in `robot.joint_names()` order.
39
+ let target = Pose::new(Vec3::new(0.4, 0.2, 0.3), Quat::from_xyzw(1.0, 0.0, 0.0, 0.0));
40
+ let start = robot.default_q();
41
+ // IK from the start finds where the path ends: the nearest collision-free solution.
42
+ let problem = PlanProblem::new(0, start, target);
43
+ let plans = batchplan::plan(&device, &worlds, &[problem], &PlanOptions::default())?;
44
+ match (plans.best(0), &plans.failures[0]) {
45
+ (Some(path), _) => {
46
+ let trajectory = Trajectory::try_new(&robot, path, 1.0)?;
47
+ trajectory.check(&robot)?;
48
+ let samples = trajectory.sample(100.0)?; // positions, velocities, accelerations
49
+ println!("{} samples over {:.2} s", samples.len(), trajectory.duration());
50
+ }
51
+ (None, Some(PlanFailure::StartCollides { contacts })) => {
52
+ println!("the start collides: {contacts:?}");
53
+ }
54
+ (None, Some(PlanFailure::GoalUnreachable)) => println!("no IK seed reaches the pose"),
55
+ (None, why) => println!("no plan: {why:?}"),
56
+ }
57
+ Ok(())
58
+ }
59
+ ```
60
+
61
+ ### Conventions
62
+
63
+ - **Units.** Meters, radians (meters for prismatic joints), seconds, `f32` throughout.
64
+ - **Poses** are `Pose { position, rotation }` with [glam](https://docs.rs/glam) types, re-exported as `batchplan::glam`. glam's quaternions are x, y, z, w (`Quat::from_xyzw`), as ROS messages are; MuJoCo and Eigen's constructor are w, x, y, z. Rotations must be unit quaternions.
65
+ - **Frames.** Targets, obstacles and depth-camera poses are all in the robot's frame: its description's root link, or `world` for a robot made with `Robot::assemble`. Transform camera and map data into it first.
66
+ - **Joint order** is `robot.joint_names()`: the actuated joints after locking, numbered breadth first, so two arms' joints interleave. Map a controller's names to it; never assume the description's order. Planar, ball and floating joints become one-axis joints named `<joint>_x`, `_theta` and so on ([robots](docs/robots.md#kinematics)).
67
+ - **Hands** are the IK frames of `RobotOptions::ee_links`, numbered in that order. For a gripper, use a frame between the fingertips, z out of the hand and y along the closing direction: what `grasps` assumes.
68
+
69
+ ### Lifecycle
70
+
71
+ - **Robot.** `Robot::load` fits collision spheres unless given a model: a fraction of a second for an arm at the default budget, seconds for thousands of spheres; save `robot.collision_model()` and pass it back as `RobotOptions::collision_model` to load the same spheres every time.
72
+ - **Device.** One per robot (`Device::gpu`, falling back to `Device::cpu`), made once: never inside a control loop or per request. `device.warmup()` before timing anything ([devices](docs/devices.md#make-once-warm-up-reuse)).
73
+ - **Worlds.** `device.upload(&worlds)` once per scene. Uploads are cheap (0.1–0.2 ms for 512 tabletop worlds), so a scene that changes is uploaded again; there is no partial update ([changing a scene](docs/perception.md#changing-a-scene)).
74
+ - **Locked joints that move**, as a torso or head the robot plans without: `robot.relock(&values)` then `device.with_robot(&relocked)`, which compiles nothing and keeps the uploaded worlds. Fit the collision model with those joints free and load with it: a model records the joints it was fitted with locked, whose pairs that meet only at other values would go unchecked, and `relock` refuses to move them ([robots](docs/robots.md#kinematics)).
75
+ - **Held objects.** `holding: Some(held)` on a `PlanProblem`, `IkProblem` or `ToolPath`, with the device without the object: one call takes problems holding different objects or grasps, a batch per hold ([holding objects](docs/robots.md#holding-objects)). On a GPU, warm up a device holding any object in each hand you will hold in: the first call holding there builds its kernels. For a device that holds one, `robot.attach(&AttachedObject)` then `device.with_robot(&held)`: the same GPU or threads, and the uploaded worlds still work. The held robot's GPU kernels are its own: warm its device up too before timing it. `robot.detach(name)` reverses it. Attachment is collision only: there is no payload mass or inertia, so timing a heavy carry ignores the load; slow it with a speed scale ([trajectories](docs/planning.md#executable-trajectories)).
76
+ - **Live loops** call `Follower::tick` or `servo` each period and nothing that builds: pass the measured state to `tick` so a robot held back or pushed is followed from where it is (held there, `Blocked`, with `FollowerOptions::replan` off). A controller in another process takes each replan from `FollowStatus::Replanned { at, trajectory }`, with `FollowerOptions::lead` for the time sending it takes. `Stopping { contacts }` and `Collides { at, contacts }` name what is in the way, links and obstacles as `PlanFailure::StartCollides` names them ([live motion](docs/planning.md#live-motion)).
77
+
78
+ ### Choose the call
79
+
80
+ | Task | Call | Guide |
81
+ |---|---|---|
82
+ | Forward kinematics | `robot.ee_pose(q, hand)`, `robot.link_pose(q, link)`, `robot.spheres(q)` | [robots](docs/robots.md) |
83
+ | Configurations for hand poses | `solve_ik`; `ik::failures` for why one has none | [IK](docs/planning.md#inverse-kinematics) |
84
+ | A collision-free path to a configuration or a hand pose | `plan`, then `Trajectory::try_new` | [planning](docs/planning.md#planning-a-path) |
85
+ | The hand along straight lines | `follow` | [tool paths](docs/planning.md#moving-the-hand-along-a-path) |
86
+ | A hand kept upright, in a box or relative to another hand | `HandConstraint` on the problem | [constraints](docs/planning.md#constraints-along-the-path) |
87
+ | Picking up and setting down | `grasps`, `pick`, `place` | [tasks](docs/planning.md#picking-up-and-setting-down) |
88
+ | Motions run without stopping between them | `join` | [joins](docs/planning.md#joining-motions) |
89
+ | Running a motion while the world changes | `Follower`, `servo` | [live motion](docs/planning.md#live-motion) |
90
+ | Obstacles from depth images or point clouds | `SdfGrid`, `OccupancyMap`, `DepthImage::robot_mask` | [perception](docs/perception.md) |
91
+ | Training data | `datagen`, `npy::export`, `lerobot::export` | [training data](docs/training-data.md) |
92
+
93
+ IK finds configurations; a plan finds the path to one, or to a pose, solving IK from the start itself. Solve IK first only to choose among its solutions. Every call takes many problems, each naming its world: batch them rather than calling once per problem, since each GPU call has a fixed cost.
94
+
95
+ ### Qualify the robot
96
+
97
+ A robot that loads may still have spheres unfit to plan with. Before planning with a new robot or a hand-tuned collision model ([qualifying a robot](docs/robots.md#qualifying-a-robot)):
98
+
99
+ 1. `robot.qualify()`: each link's `overhang` past its geometry, the `uncovered` share of its surface and its `thickness`, and each hand's `tips`. Fingers whose overhang or tip is a large part of the smallest object you will grasp will collide with what it stands on; give them a tighter tolerance (`RobotOptions::sphere_tolerances`) or a finer `SphereOptions::voxel_size`.
100
+ 2. `world.always_touching(&robot)` with the table the robot stands on: links that touch it in every configuration need a touch, or every plan's start collides.
101
+ 3. Fingers that are part of the palm's link, as the SO-101's fixed jaw: name them in `RobotOptions::finger_links`, or grasps where they meet the object fail.
102
+ 4. `device.contacts(&worlds, &item_world, &q, 0.0)` at the default configuration, the SRDF's named states and a few others: each should collide with nothing.
103
+
104
+ ### Read results
105
+
106
+ - `ik.best(i)`, `plans.best(i)` and `solved()` return only valid answers; `IkResult::success` and `PlanResult::valid` are per seed, problem-major (`problem * seeds + seed`).
107
+ - A plan is B-spline control points, not a trajectory: time it with `Trajectory::try_new` (`Trajectory::moving` from a moving start), sample it with `at` or `sample`, and `check` it before it runs on hardware. Never send control points to a controller.
108
+ - Plans do not depend on the batch or the device; with a `time_budget`, they depend on the machine's speed. IK starts every problem from the same seeds in any batch, but a GPU rounds differently from the CPU, and differently on either side of where it switches how many invocations share a configuration (4,096 configurations, more for robots with many spheres), so solutions there can differ slightly, and so can plans to poses, which end where IK solves them.
109
+
110
+ ### Diagnose a failed request, in order
111
+
112
+ Check these before raising budgets:
113
+
114
+ 1. **The input.** An `Error::Input` names what was refused and the value given: shapes, joint order, unit quaternions, joint limits (starts and goals must be within them), frames.
115
+ 2. **The start.** `PlanFailure::StartCollides { contacts }` names each link and obstacle, or pair of links. A link touching the table in every configuration needs a touch (`always_touching`); a start inside a held object's spheres needs the object attached.
116
+ 3. **The goal.** `PlanFailure::GoalCollides`, and for a pose goal `GoalUnreachable`, which more `PlanOptions::goal_ik` iterations may fix; for IK, `ik::failures` tells `Unreachable` from `Collides { contacts }`.
117
+ 4. **The search.** `NoPath`: more `rrt.max_iterations`, or the way is blocked. `NoSpline`: more `control_points`, or a slower start motion. `OutOfTime`: a longer `time_budget`.
118
+ 5. **Tasks.** A `TaskFailure` prints where and why every candidate stopped; each `CandidateStop` has the step, the reason and the contacts, whose obstacles are numbered as in the task's world; in a place, `world.obstacles.len()` is the object where it is set down. `grasps` says why it found none. Grasps near a support need `GraspOptions::tip` and `floor`, which keeps the open fingers over it too; from above, they keep at least `overlap` of the fingers (1 cm) in the object, so an object whose top is less than that over the floor has none.
119
+
120
+ ### Footguns
121
+
122
+ - glam's quaternion order is x, y, z, w; a w-first source passed straight through turns the hand.
123
+ - `PlanOptions::seeds` is 0: no trajectory optimization unless asked for.
124
+ - A world's touches name links: worlds where a held object may touch something are uploaded to the device of the robot holding it.
125
+ - `holding` takes the device without the object: one already holding a `Held` in that hand is an `Error::Input`.
126
+ - `grasps` returns poses in the object's frame; with a `floor`, pass the obstacle as it stands in the world.
127
+ - `Robot::lower` and `upper` are ranges; continuous joints turn without limit and wrap.
128
+ - `Follower::tick` with `None` trusts the trajectory; on hardware, pass the measured state.
129
+ - `FollowStatus::Stopping { contacts }` numbers obstacles as in the worlds of the tick the stop began at, on every tick of the stop: map them through those worlds, not the current ones.
130
+ - A `Follower` on a tool path (`follow`) needs `FollowerOptions::replan` off: a replan leaves the path for a free-space motion to its end. Off, a collision ahead stops it on the path and a robot past `tolerance` is held where it is, both `Blocked` at rest ([live motion](docs/planning.md#live-motion)).
@@ -0,0 +1,253 @@
1
+ # Changelog
2
+
3
+ What changed in each release, for people upgrading, in the format of
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow
5
+ [Cargo's semver](https://doc.rust-lang.org/cargo/reference/semver.html): before 1.0, a new minor
6
+ version (0.9 → 0.10) may break the interface, and a patch (0.9.0 → 0.9.1) breaks nothing.
7
+ [Contributing](CONTRIBUTING.md#releases) says how releases are made.
8
+
9
+ ## [Unreleased]
10
+
11
+ ### Added
12
+
13
+ - **Breaking:** `plan` takes the hands' poses as a goal: `PlanProblem::goal` is a `Goal`, a
14
+ configuration (`Goal::Joints`) or a pose or `None` per hand (`Goal::Poses`), and
15
+ `PlanProblem::new` takes either, a `Pose` for a one-handed robot. `plan` solves IK for every
16
+ pose goal of the call in one batch, seeded from each start and keeping its held joints, what it
17
+ holds and its constraints, and plans to the collision-free solution nearest the start up to
18
+ whole turns. Poses no seed reaches collision-free are solved again with four times the seeds, up
19
+ to sixteen times, while a `time_budget` lasts; those still unreached fail as
20
+ `PlanFailure::GoalUnreachable`, or `GoalCollides` where IK reaches them only in collision.
21
+ `PlanOptions::goal_ik` sets that IK. `PlanProblem::new` takes a configuration as a `Vec<f32>`,
22
+ `&[f32]`, `&Vec<f32>`, `[f32; N]` or `&[f32; N]`; convert any other form with `to_vec()`. Code
23
+ reading a goal as a slice reads `goal.joints()`, a struct literal wraps a configuration in
24
+ `Goal::Joints`, and code that writes out every field of `PlanOptions` needs the new one. The benchmark's ik+plan mode plans to the
25
+ goal pose this way. In Python, `PlanProblem` takes a `Pose`, or a `Pose` or `None` per hand, as
26
+ its goal, beside `PlanOptions.goal_ik` and `PlanFailure.GoalUnreachable`.
27
+ - Python bindings, in `python/`: the `batchplan` package, built with maturin
28
+ ([Python](docs/python.md)). Robots, assembled ones among them, obstacles and worlds, devices, IK
29
+ (`solve_ik`, `ik_failures`), planning with hand constraints (`plan`), timing (`Trajectory`),
30
+ tool paths (`follow`), whole tasks (`pick`, `place`, `grasps`, `place_poses`) and held objects,
31
+ with NumPy arrays in and out, `batchplan::Error`'s kinds as exception classes, and the
32
+ interpreter released while each call works. Values are frozen and changed with
33
+ `replace(**changes)`, as Rust's struct update syntax does. Install it from the repository with
34
+ `pip install "git+https://github.com/natsuku-lab/batchplan#subdirectory=python"`, which needs a
35
+ Rust toolchain.
36
+ - **Breaking:** `PlanProblem::holding`, `IkProblem::holding` and `ToolPath::holding`: an object a
37
+ hand holds (a `Held`, as `TaskPlan::held` gives it), per problem. One `plan`, `solve_ik` or
38
+ `follow` call takes problems holding nothing, different objects, or one object by different
39
+ grasps, and gives each what a device holding its object alone gives: it groups them by what
40
+ they hold and runs a batch per group, in parallel, on the device's GPU or threads. Holding per
41
+ problem took a device and a call per hold. Pass the device without the object; one already
42
+ holding a `Held` in that hand is an `Error::Input`. Code that writes out every field of
43
+ `IkProblem`, `PlanProblem` or `ToolPath` needs the new one (`holding: None`).
44
+ - `SphereOptions` implements `PartialEq`.
45
+ - **Breaking:** `GraspOptions::overlap`, 1 cm by default: the least the fingers reach into the
46
+ object in a grasp from above over a `floor`. An object flatter than `tip` and the floor's
47
+ clearance is now held from above with the fingertips at the floor and the frame over its top,
48
+ as long as its top is at least `overlap` over the floor. `grasps` left out every grasp from
49
+ above of such an object: an 8.8 x 4.8 x 1.85 cm package got only grasps from the sides, which
50
+ put the hand into the table. Every grasp from above over a floor now keeps at least `overlap`
51
+ of the fingers in the object, where it kept the frame inside it, so with `tip` under 1 cm (the
52
+ default 0 among them) some it returned are left out. Code that writes out every field of
53
+ `GraspOptions` needs the new one.
54
+ - **Breaking:** `FollowerOptions::replan`, on by default. Off, a `Follower` keeps its
55
+ trajectory's path, as a motion along a tool path must: where the rest of the motion collides,
56
+ it stops on the path, and where the robot is measured past `tolerance`, it holds it there,
57
+ `Blocked`. In either case it used to plan a free-space motion to the goal, off the path. Code
58
+ that writes out every field of `FollowerOptions` needs the new one.
59
+
60
+ ### Changed
61
+
62
+ - Batched IK spends less time around the solve: a problem's seeds share its targets instead
63
+ of each carrying a copy, large batches are seeded on the device's threads, buffers go to the
64
+ GPU without an extra copy, and a robot without collision spheres skips the collision push,
65
+ which is zero for it. The same results, bit for bit. In RoboKit's IK benchmark on an RTX 4090
66
+ (a Panda, collision off, 64 seeds of 30 steps), 1,000 targets take 4.5 ms where they took
67
+ 10.8, 2,000 take 9.1 ms where 23.2, and one takes 0.9 ms where 1.26
68
+ ([Benchmarks](docs/benchmarks.md#against-robokit)).
69
+ - **Breaking:** `IkOptions::collision_step` must be finite: an infinite or NaN one is an input
70
+ error. NaN and negative infinity used to mean no push, and positive infinity solved nothing
71
+ (the CPU left every seed where it started).
72
+ - IK on a GPU takes 10-28% less time: the kernels are built without the counter naga adds to
73
+ every loop to rule out infinite ones, which theirs cannot be (the crate's one `unsafe`). One
74
+ target's 64 seeds of 30 steps take 1.01 ms where they took 1.27 on an M4 Pro for a Panda
75
+ without collision spheres (2.83 ms where 3.33 with them), and 0.53 ms where 0.74 on an RTX
76
+ 4090 (about 1.2 ms where 1.33). A GPU's results round differently for it, as they do from
77
+ driver to driver ([Benchmarks](docs/benchmarks.md#how-the-speed-came)).
78
+ - Trajectories are sooner: time-optimal timing uses the full velocity and acceleration limits,
79
+ and where its smoothed time map overshoots a limit, slows the profile there instead of
80
+ stretching the whole motion for it. Planned Panda motions take 6.6% less time on average, the
81
+ hand's straight moves 3.0%, replans from a moving start 5.7%, and the 2,600 benchmark
82
+ problems' motions 5.0%, within the same limits (`Trajectory::check`); more joins are worth
83
+ blending. Timing costs less than it did (a Ryzen thread): a path from rest 9% less, and one
84
+ from a moving start 59% less (475 µs where 1,172), timed once with its start at the full
85
+ limits, its candidate time maps turned down at a quarter of the checking density, and each
86
+ map measured over the joints in contiguous rows. A moving start's timing and a stop's are
87
+ also held to their limits either side of where they cross the path's knots, between the
88
+ samples checked, where jerk could pass its limit by up to 12%, and in a stop acceleration
89
+ by up to 20% and jerk by 19%: of 960 stops asked early in planned Panda motions 52 are
90
+ refused where 40 were. Replans from a moving start take 11% less time one at a time on a
91
+ Panda and 70% less on an SO-101; a batch of 96 Panda replans takes 65 ms where it took 61
92
+ (within the runs' spread), since a moving start no timing fits costs more to turn down
93
+ (2.0 ms where 1.6). A spline that cannot follow the start motion is then tried again,
94
+ stopping at every waypoint, with less of that timing, which takes a further quarter off a
95
+ Panda's replans one at a time and a sixth off the SO-101's (an M4 Pro)
96
+ ([Benchmarks](docs/benchmarks.md#timing-up-to-the-limits)).
97
+ - **Breaking:** `IkOptions::default()` is 64 seeds of 30 steps of at most 1 rad, where it was 32
98
+ seeds of 60 steps of at most 0.3 rad: at 0.3 rad, about half the seeds ran out of steps before
99
+ converging. The same problems solve. Results carry 64 seeds per problem, and `TaskOptions`,
100
+ `DemoOptions`, `PickPlaceOptions` and `PlanOptions::goal_ik` inherit the new IK; pass the old
101
+ values to keep the old results.
102
+ - **Breaking:** An IK step that would move no joint by more than 1e-6 rad is not taken, on the
103
+ CPU and the GPU alike: a seed that has come to rest stays exactly there, and CPU threads skip
104
+ its remaining steps, which makes their IK about a fifth faster. Seeds can differ from before
105
+ by a few microradians. Together with the new defaults, the 2,600 benchmark problems plan to
106
+ pose goals 1.8 times as fast in batches on a Radeon 8060S (6,225 against 3,489 per second, and
107
+ in 5 ms instead of 9 for one) and 1.18 times as fast on 32 CPU threads (3,344 against 2,836);
108
+ IK for 512 tabletop targets takes 71 ms instead of 84 on those threads.
109
+ - `solve_ik` on a GPU checks the solved configurations' clearances in the same submission, from
110
+ the configurations already on the GPU, and reads everything back at once: one wait per 65,536
111
+ seeds instead of three, about 0.4 ms less per call.
112
+ - **Breaking:** `Robot::load`, `Robot::from_urdf` and `Robot::assemble` refuse a collision
113
+ model whose spheres have a non-finite center or radius or a negative one, or whose
114
+ self-collision buffer for a link is not finite or would shrink one of its spheres below nothing,
115
+ naming the link and the value (`Error::Load`; `Error::Input` from `from_urdf`). Spheres fitted to
116
+ geometry that is not finite, or so large it overflows, are refused alike. Such models loaded
117
+ before, and a sphere smaller than nothing checked as farther from what it touched than it was.
118
+ - **Breaking:** `Held` is `batchplan::Held`, no longer also `batchplan::task::Held`.
119
+ - A device keeps the spheres it fits to held objects, the latest 64, and shares them with the
120
+ devices `with_robot` makes from it. `pick`, `place` and every call holding a cylinder or a
121
+ capsule fitted it again, which takes milliseconds; a box or a ball is covered in microseconds.
122
+ - **Breaking:** `FollowStatus::Stopping { contacts }` and `FollowStatus::Collides { at, contacts }`
123
+ name what is in the way, as `Device::contacts` lists it: what the rest of the motion collided
124
+ with where it was first found to, numbered as in the worlds of the tick the stop began at and
125
+ repeated on every tick of the stop while it is clear, and what the robot collides with at
126
+ `at`. A graze a GPU device's check finds just outside what its CPU threads list names the
127
+ nearest within 0.1 mm. They said only that something collided. `Stopping` was a unit variant.
128
+ - **Breaking:** `grasps` keeps the open fingers above `GraspOptions::floor` too, `opening` apart
129
+ and reaching `tip` past the frame, where it kept only the frame and the point between the
130
+ fingertips. A grasp tilted about a bar over a support, as a drawer's handle, swung one open
131
+ finger into it: of a bar whose top is 3.7 cm over a plate, every one of the 12 grasps tilted
132
+ from above at 16 turns put a Panda's finger into the plate. They are now held as far out as
133
+ keeps the finger over the floor, and left out where that holds less than `overlap`: the 4 left
134
+ all pick it. Grasps from the side that close upright on an object standing on the table, their
135
+ lower finger below the table's top, are left out too, so an object only those fit, such as a
136
+ box too wide across its top to grasp from above, gets an `Error::Input` where it got them. An
137
+ infinite `opening` is an `Error::Input`.
138
+
139
+ ### Fixed
140
+
141
+ - A plan from a moving start is optimized (`PlanOptions::seeds`) unless its path is within 2% of
142
+ the straight line from the start: the line was measured from the path's first control point,
143
+ which lies behind a moving start, so paths that bent by half as much again were left
144
+ unoptimized.
145
+ - `Trajectory::stop` stops a motion from more of the states it passes through, and sooner. It
146
+ refused where a joint ran within 0.6% of its velocity limit, since a stop's velocity was held
147
+ to the margin under the limit even where it starts, and often while a motion still gathered
148
+ speed, since braking at once breaks the jerk limit there, or stopped only by creeping to the
149
+ path's end. It now also tries carrying on for one span with the motion's jerk, half of it or
150
+ none before braking, and keeps the stop that ends soonest along the path. Trajectories timed
151
+ up to the limits pass through such states more often. Through the first fifth of 48 planned
152
+ Panda motions, 52 of 960 stops are refused where 153 were and one takes longer than going on
153
+ where 66 did; of 19 states at a velocity limit, 9 stop where none did. A quarter of the way
154
+ along 438 motions, stops last 0.633 s where they lasted 0.646, and take 0.62 ms to find where
155
+ they took 0.73 (`examples/timing.rs`, which now times them, on an M4 Pro). A `Follower` that
156
+ must stop where none is found keeps to its trajectory, which comes to rest at the path's end.
157
+ - Optimized seeds (`PlanOptions::seeds`) are validated as searched splines are checked: at samples no
158
+ farther apart than `shortcut.resolution`, keeping the margin from the world searched splines keep
159
+ away from their ends (2 mm, or `shortcut.clearance` if less). Validation kept no margin and
160
+ sampled each span `validate_substeps` times however far it moved, so optimization, which pulls
161
+ paths taut against obstacles, returned valid paths that collided between samples, as on picks
162
+ under a table. Their lengths, which `best` compares, are measured at those samples too.
163
+ - `plan` refuses a start or a joint goal that is not finite. A continuous joint's range has no
164
+ limits, so a start at infinity there passed the limit check and planned a path of infinite
165
+ control points marked valid.
166
+ - `Trajectory::at` and `Trajectory::sample` keep each position between the control points it
167
+ blends, so a path that runs along a joint limit never samples past it. Rounding left samples a
168
+ unit or two in the last place past (up to 4.8e-7 rad), which limit checks with no tolerance
169
+ refuse.
170
+ - `pick` builds each holding device where it moves with it again, so building one overlaps the
171
+ other grasps' moves. 0.10.0 built them all first, which cost picks on a GPU device about 1.5%:
172
+ 64 picks of the tasks example (`cargo run --release --example tasks -- 64`) took 185.5 ms on
173
+ an M4 Pro and take 183.2 ms now, against 181.5 ms in 0.9.0 (medians of seven alternated runs).
174
+ The rest of that gap is in grasping and moving in, whose code did not change.
175
+ - `grasps` refuses an object whose size is negative or not finite, or whose rotation is not a
176
+ unit quaternion, naming it, as uploading a world does.
177
+
178
+ ## [0.10.0] - 2026-11-07
179
+
180
+ ### Changed
181
+
182
+ - **Breaking:** `Robot::attach` refuses a shape whose size is negative or not finite, or whose
183
+ rotation is not a unit quaternion to within 1e-3, as uploading a world does, naming the shape.
184
+ 0.9.0 attached such shapes with spheres that did not cover them. Normalize a rotation read
185
+ with few digits, such as (0, 0, 0.71, 0.71), before attaching it.
186
+ - The model's `source` also gives the largest of each link's reach over its own tolerance.
187
+ - `Robot::attach` fits a single shape in its own frame and places its spheres by the shape's
188
+ pose, so where it sits on its link no longer changes them, and `pick`, `place` and attaching
189
+ `Held::attachment` give a held object the same spheres.
190
+ - `pick` and `place` fit each held object once and place its spheres by each grasp, where they
191
+ attached it at each grasp and fitted it again: picks of cylinders and capsules spend less of
192
+ their time attaching.
193
+
194
+ ### Fixed
195
+
196
+ - `place` refuses a held object whose size is not finite, as an input error naming its task.
197
+ - A pick's contacts where it lifts the object name obstacles as the task's world numbers them:
198
+ every obstacle after the picked one came back one index too low.
199
+ - Where the budget reaches every tolerance but cannot hold each link within its own, what it has
200
+ left holds the spheres tighter. It used to go unspent, with spheres reaching up to twice their
201
+ link's tolerance. A stand of two boxes held to 2 cm (`tests/robot.rs`) reached 36.6 mm with 379
202
+ of 512 spheres, and now 24.1 mm with 502; the Panda at 1,200 spheres reached 9.5 mm against
203
+ 5 mm, and now 5.8 mm. The search for the least growth makes such fits two to three times as
204
+ long: the Panda's at 1,200 spheres takes 0.20 s, where it took 0.10 s, and the stand's at 512
205
+ 0.13 s, where it took 0.05 s.
206
+ - Covers are refined until they reach every surface sample, where they stopped after 8 rounds,
207
+ so large boxes keep to their tolerance where the budget allows: the stand reached 25.7 mm at
208
+ 1,024 spheres, and now 20.0 mm.
209
+
210
+ ## [0.9.0] - 2026-11-06
211
+
212
+ ### Changed
213
+
214
+ - Fitted spheres reach less far past thin parts. Each link's spheres reach at most twice its
215
+ tolerance, scaled by what the budget allows, and less as the budget grows. Where the budget
216
+ affords it, each closed link is held within its own tolerance. Parts thinner than 0.1 mm and
217
+ vertices whose averaged normal leads out of the geometry may reach a little farther: at 2,048
218
+ spheres the Panda's base reaches 5.18 mm against 5 mm. The model's `source` names the scale.
219
+ Fitted models change, and saved models load as before.
220
+ - Spheres held that tight leave slivers of surface about a millimeter deep outside them:
221
+ `LinkQualification::uncovered` reaches about 2% of a link, where it was a fraction of a
222
+ percent.
223
+ - `Robot::qualify` measures on samples the fit did not draw.
224
+
225
+ ## [0.8.0] - 2026-11-06
226
+
227
+ ### Added
228
+
229
+ - `Robot::relock` holds joints locked at load at new values, and `Device::with_robot` takes the
230
+ relocked robot without compiling kernels or uploading worlds again.
231
+ - `FollowerOptions::lead`: time after the planning budget for a replan to reach the robot.
232
+
233
+ ### Changed
234
+
235
+ - **Breaking:** `FollowStatus::Replanned` carries the new trajectory: `Replanned { at, trajectory }`.
236
+ - **Breaking:** `FollowerOptions` gains `lead`, and `CollisionModel` gains `locked_joints`, the
237
+ joints locked while fitting. A model fitted with locked joints must be loaded with them locked
238
+ at the same values, and `relock` refuses to move them. Models saved before 0.8.0 load as before.
239
+
240
+ ### Fixed
241
+
242
+ - `Device::evaluate` refuses configurations that are not finite, and `Robot::load` lock values
243
+ that are not finite.
244
+ - GPU picks at small batches are as fast as in 0.6.1 again: IK that does not push away from
245
+ obstacles no longer splits each configuration's work across GPU invocations, and robots with
246
+ many spheres keep the split past 4,096 configurations.
247
+
248
+ Earlier releases have no notes beyond their commits.
249
+
250
+ [Unreleased]: https://github.com/natsuku-lab/batchplan/compare/v0.10.0...HEAD
251
+ [0.10.0]: https://github.com/natsuku-lab/batchplan/compare/v0.9.0...v0.10.0
252
+ [0.9.0]: https://github.com/natsuku-lab/batchplan/compare/v0.8.0...v0.9.0
253
+ [0.8.0]: https://github.com/natsuku-lab/batchplan/compare/v0.7.0...v0.8.0
@@ -0,0 +1,121 @@
1
+ # Contributing
2
+
3
+ Thanks for helping. Issues and pull requests are welcome at
4
+ [natsuku-lab/batchplan](https://github.com/natsuku-lab/batchplan).
5
+
6
+ ## Building and testing
7
+
8
+ ```bash
9
+ cargo build --release --all-targets --features lerobot,usd
10
+ BATCHPLAN_REQUIRE_GPU=1 cargo test --release --features lerobot,usd # fails rather than skips the GPU tests without an adapter
11
+ cargo test --release --no-default-features # the CPU-only build
12
+ cargo clippy --release --all-targets --features lerobot,usd # zero warnings; CI denies them
13
+ cargo doc --no-deps --features lerobot,usd # zero warnings
14
+ (cd demos && cargo clippy --release --all-targets) # the Rerun demos, a crate of their own
15
+ (cd python && BATCHPLAN_REQUIRE_GPU=1 uv run pytest) # the Python bindings, built and tested through the package
16
+ scripts/check.sh # every lint CI runs, on every crate: run it before pushing
17
+ ```
18
+
19
+ CI runs the same on Linux, with the GPU kernels on Mesa's llvmpipe, and builds on macOS, Windows
20
+ and aarch64 Linux. [Verification](docs/verification.md) says what each test pins down.
21
+
22
+ ## Style
23
+
24
+ The code follows [Google's Rust style guide](https://google.github.io/styleguide/rustguide.html).
25
+ Format with stable rustfmt, then check the guide's options that only nightly rustfmt has:
26
+
27
+ ```bash
28
+ cargo fmt
29
+ rustup toolchain install nightly --profile minimal -c rustfmt
30
+ cargo +nightly fmt --check -- --config imports_granularity=Module,hex_literal_case=Lower,format_macro_matchers=true
31
+ ```
32
+
33
+ Clippy checks what it can of the guide (`[lints]` in `Cargo.toml`, and `src/lib.rs` for the
34
+ library). Where the guide leaves a choice to the codebase:
35
+
36
+ - **Imports.** No glob imports, except `use super::*` in test modules and the rayon and
37
+ fearless_simd preludes. Call free functions through their module (`world::box_distance`,
38
+ `datagen::handover`, `batchplan::plan`), and import types by name, unless the name would clash
39
+ or comes from wgpu or rerun.
40
+ - **Modules.** A module with children only declares and re-exports them.
41
+ - **API.** Document every public item; mark pure functions `#[must_use]`; name a constructor that
42
+ returns a `Result` `try_new`; return named fields rather than tuples; spell names out (the
43
+ field's own terms, such as `dof` and `q`, excepted). Results and enums that may grow are
44
+ `#[non_exhaustive]`; options and problems keep struct literals, so struct update syntax works.
45
+ - **Implementation.** `expect` says why it cannot fail, `for` loops replace sequential
46
+ `for_each`, iterator chains are named before a `for` loop, and assertions say why they hold.
47
+
48
+ ## Changes
49
+
50
+ - **A new test must fail on the unfixed code.** Plant the bug and watch the test fail.
51
+ - **The CPU and the GPU implement the same math.** A change to one (`src/cpu.rs`, the kernels in
52
+ `src/*.wgsl` and `gpu.rs`) lands in the other in the same change; the parity tests catch drift.
53
+ - **Performance claims need an A/B comparison on one machine.** Build the previous commit in a
54
+ `git worktree` with its own `CARGO_TARGET_DIR`, and alternate runs with yours after checking the
55
+ machine is idle. One run proves nothing.
56
+ - **Numbers in the docs come from runs you can name.** [Benchmarks](docs/benchmarks.md) says how
57
+ each was measured; rerun what a change moves and update every page that quotes it.
58
+
59
+ ## Review
60
+
61
+ Before pushing, run `scripts/check.sh`, then have the reviewer agent read the change. In
62
+ [Claude Code](https://claude.com/claude-code), ask for the `reviewer` agent on your branch
63
+ ([`.claude/agents/reviewer.md`](.claude/agents/reviewer.md)). It checks the change against the
64
+ rules in [CLAUDE.md](CLAUDE.md): correctness, the CPU/GPU twin and the other invariants,
65
+ deep-module design, the style guide, tests that fail on the unfixed code, measured performance and
66
+ docs that match the code. It reports each defect with the input that shows it. Fix what it finds,
67
+ or say in the pull request why a finding does not apply.
68
+
69
+ [CLAUDE.md](CLAUDE.md) holds the design rules, invariants and working rules, for people and coding
70
+ agents alike; [architecture.md](docs/architecture.md) says how the code fits together.
71
+
72
+ ## Releases
73
+
74
+ Merging is not releasing. Changes collect on `main` and ship together, so a version stands for a
75
+ batch of changes rather than for each one.
76
+
77
+ - **The changelog.** A change a user would notice adds a line to `## [Unreleased]` in
78
+ [CHANGELOG.md](CHANGELOG.md), under Keep a Changelog's headings (Added, Changed, Removed,
79
+ Fixed). Mark with **Breaking:** whatever
80
+ breaks code written against the last release.
81
+ - **Before a release.** A project that needs a merged change before it ships pins its commit,
82
+ `batchplan = { git = "https://github.com/natsuku-lab/batchplan", rev = "<commit>" }`, and goes
83
+ back to a version when the release is out. crates.io takes no crate with a git dependency, so
84
+ this is for applications and unpublished crates. To try the changes from crates.io, cut a
85
+ pre-release: `0.10.0-beta.1`, then `0.10.0-beta.2`. Cargo picks a pre-release only for a
86
+ requirement that names one, so users of `0.9` never get it. One beta may break the last, and a
87
+ requirement of one takes the next, so a project trying a beta pins it exactly
88
+ (`batchplan = "=0.10.0-beta.1"`).
89
+ - **Patch or minor.** `cargo semver-checks --release-type minor`
90
+ (`cargo install cargo-semver-checks --locked`) fails on a change to the interface that breaks
91
+ the latest release on crates.io. It calls such a change major, which before 1.0 means the next
92
+ minor version. A call that now behaves differently can break callers too, which only a reader
93
+ sees. A batch that breaks nothing is a patch, even one that adds to the interface. Breaking
94
+ changes wait on `main` for the next minor version.
95
+ - **When.** A patch whenever a fix should reach crates.io users. A minor release at most every
96
+ two weeks, once a beta of it has run in the projects that use batchplan,
97
+ [batchplan-ros](https://github.com/natsuku-lab/batchplan-ros) among them. A patch for a release
98
+ that `main` has moved past with breaking changes goes on a branch from that release's last tag
99
+ (`0.9.x` from `v0.9.0`). Changes reach the branch by pull request, so CI runs on them, and their
100
+ changelog lines go to `main` too.
101
+ - **Cutting one.**
102
+ 1. Set `version` in `Cargo.toml` and `python/Cargo.toml` (the Python package's). The next build
103
+ of each crate updates `Cargo.lock`, `demos/Cargo.lock` and `python/Cargo.lock`, and
104
+ `scripts/check.sh` builds all three.
105
+ 2. For a release, not a beta, move the lines under `## [Unreleased]` to a new
106
+ `## [0.10.0] - date`, and point the compare links at the end of the file at it.
107
+ 3. Commit it as `batchplan 0.10.0`, to `main` or by pull request to a release's branch, and
108
+ wait for CI.
109
+ 4. `git tag -a v0.10.0 -m "batchplan 0.10.0" && git push origin v0.10.0`. The release workflow
110
+ checks that the tag names the version and that `cargo semver-checks` passes, then publishes to
111
+ crates.io.
112
+ 5. After a release, move batchplan-ros to it.
113
+ - **1.0** waits for an interface we mean to keep. Two-digit minor versions until then are usual
114
+ for Rust crates: nalgebra, glam and rapier are past 0.30. One decision comes first: how options
115
+ and problems grow. Today a new field breaks callers' struct literals, and after 1.0 each one would
116
+ take a major version.
117
+
118
+ ## License
119
+
120
+ Contributions are dual-licensed under MIT or Apache-2.0, as the project is, unless you say
121
+ otherwise.