Skip to content

Commit 84a87a7

Browse files
committed
feat: add reachability benchmark tools
Signed-off-by: Hirokazu Ishida (SB Intuitions) <hirokazu.ishida@sbintuitions.co.jp>
1 parent ea31105 commit 84a87a7

10 files changed

Lines changed: 2212 additions & 0 deletions

File tree

reachability_bench/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/result

reachability_bench/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10

reachability_bench/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# OpenArm Reachability Benchmark
2+
3+
This tool benchmarks the reachability workspace of the OpenArm robot for different end effector orientations.
4+
This tool is designed to visualize the current reachability given the URDF file and leverage it for future improvements and customization of the OpenArm's mechanical structure.
5+
6+
## Usage
7+
Run a single benchmark by specifying height, direction
8+
```bash
9+
uv run bench.py --z 0.5 --direction forward1
10+
```
11+
z is the height of the target coordinates, and the reachability map is computed for the plane at height z.
12+
13+
Execute batch benchmarks across multiple heights and directions:
14+
```bash
15+
./batch_bench.sh
16+
```
17+
18+
Visualize results with animation support:
19+
```bash
20+
uv run visualize.py --direction forward1 --htable 0.29 --animate
21+
```

reachability_bench/activate.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
source .venv/bin/activate

reachability_bench/batch_bench.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
parallel --bar uv run bench.py --z {1} --direction {2} ::: 0.3 0.35 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 ::: forward1 forward2 down1 down2

reachability_bench/bench.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright 2025 SB Intuitions Corp.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from pathlib import Path
16+
from typing import Literal
17+
18+
import numpy as np
19+
import tqdm
20+
import tyro
21+
from plainmp.ik import solve_ik_srinv
22+
from plainmp.robot_spec import OpenArmV10RarmSpec
23+
from skrobot.coordinates import Coordinates
24+
from threadpoolctl import threadpool_limits
25+
26+
27+
class PlainmpIKSolver:
28+
def __init__(self):
29+
self.spec = OpenArmV10RarmSpec()
30+
31+
def solve(self, tcp_coords_ros: Coordinates, n_global_iter: int) -> np.ndarray | None:
32+
cst = self.spec.create_tcp_pose_const(tcp_coords_ros)
33+
lb, ub = self.spec.angle_bounds()
34+
ret = solve_ik_srinv(cst, lb, ub, q_seed=None, max_trial=n_global_iter)
35+
if ret.success:
36+
return ret.q
37+
return None
38+
39+
40+
def benchmark(
41+
z: float,
42+
direction: Literal["forward1", "forward2", "down1", "down2"] = "forward1",
43+
):
44+
if direction == "forward1":
45+
co_base = Coordinates()
46+
elif direction == "forward2":
47+
co_base = Coordinates()
48+
co_base.rotate(-np.pi * 0.5, "x")
49+
elif direction == "down1":
50+
co_base = Coordinates()
51+
co_base.rotate(np.pi * 0.5, "y")
52+
elif direction == "down2":
53+
co_base = Coordinates()
54+
co_base.rotate(np.pi * 0.5, "y")
55+
co_base.rotate(-np.pi * 0.5, "x")
56+
else:
57+
assert False
58+
59+
result_dir = Path(__file__).parent / "result"
60+
result_dir.mkdir(exist_ok=True)
61+
62+
xlin = np.linspace(0.0, 0.8, 20)
63+
ylin = np.linspace(-0.5, 0.5, 20)
64+
X, Y = np.meshgrid(xlin, ylin)
65+
points2d = np.stack([X.ravel(), Y.ravel()], axis=1)
66+
solver = PlainmpIKSolver()
67+
68+
with threadpool_limits(limits=1, user_api="blas"):
69+
qs_reachable = []
70+
labels = []
71+
for point2d in tqdm.tqdm(points2d):
72+
point3d = np.hstack([point2d, z])
73+
co = co_base.copy_worldcoords()
74+
co.translate(point3d, wrt="world")
75+
ret = solver.solve(co, 30)
76+
success = ret is not None
77+
labels.append(success)
78+
if success:
79+
qs_reachable.append(ret)
80+
is_reachables = np.array(labels)
81+
qs_reachable = np.array(qs_reachable)
82+
pts_reachable = points2d[is_reachables]
83+
84+
d = {"z": z, "pts": pts_reachable, "qs": qs_reachable}
85+
with (result_dir / f"{direction}-z-{round(z, 2)}.npz").open(mode="wb") as f:
86+
np.savez(f, d)
87+
88+
89+
if __name__ == "__main__":
90+
tyro.cli(benchmark)

reachability_bench/format.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ruff check . --fix
2+
ruff format .

reachability_bench/pyproject.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[project]
2+
name = "reachability-test"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
dependencies = [
8+
"matplotlib>=3.10.7",
9+
"plainmp>=0.3.4",
10+
"plotly>=6.5.0",
11+
"scikit-robot>=0.2.35",
12+
"threadpoolctl>=3.6.0",
13+
"tqdm>=4.67.1",
14+
"tyro>=0.9.35",
15+
]
16+
17+
[tool.ruff]
18+
ignore = ["E731"]
19+
lint.select = ["E", "F", "I"]
20+
line-length = 100
21+
22+
[dependency-groups]
23+
dev = [
24+
"pudb>=2025.1.3",
25+
"ruff>=0.14.6",
26+
]

reachability_bench/uv.lock

Lines changed: 1980 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

reachability_bench/visualize.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright 2025 SB Intuitions Corp.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import time
16+
from pathlib import Path
17+
from typing import Literal
18+
19+
import matplotlib.pyplot as plt
20+
import numpy as np
21+
import tyro
22+
from plainmp.robot_spec import OpenArmV10RarmSpec
23+
from skrobot.model.primitives import Box
24+
from skrobot.viewers import PyrenderViewer
25+
26+
27+
def main(
28+
htable: float = 0.29,
29+
hmin: float | None = None,
30+
hmax: float | None = None,
31+
animate: bool = False,
32+
direction: Literal["forward1", "forward2", "down1", "down2"] = "forward1",
33+
):
34+
feasible_pointss = []
35+
qss = []
36+
if hmin is None:
37+
hmin = -np.inf
38+
if hmax is None:
39+
hmax = np.inf
40+
z_point_max = -np.inf
41+
for result_path in Path("result").iterdir():
42+
if not str(result_path.stem).startswith(direction):
43+
continue
44+
npz = np.load(result_path, allow_pickle=True)
45+
dic = npz["arr_0"].item()
46+
z = dic["z"]
47+
bools = dic["pts"]
48+
qs = dic["qs"]
49+
z_point_max = max(z_point_max, z)
50+
if z > htable and z > hmin and z < hmax:
51+
if len(bools) > 0:
52+
feasible_pointss.append([z, bools])
53+
qss.append(qs)
54+
55+
qs = np.vstack(qss)
56+
57+
v = PyrenderViewer()
58+
spec = OpenArmV10RarmSpec()
59+
model = spec.get_robot_model(with_mesh=True)
60+
v.add(model)
61+
table_thickness = 0.03
62+
table = Box([0.4, 1.0, table_thickness], face_colors=[0.65, 0.45, 0.0, 1.0])
63+
table.translate([0.3, 0.0, htable - table_thickness * 0.5])
64+
v.add(table)
65+
66+
cmap = plt.get_cmap("jet")
67+
68+
for z, points in feasible_pointss:
69+
for p_2d in points:
70+
p_3d = np.hstack([p_2d, z])
71+
size = 0.01
72+
s = (z - htable) / (z_point_max - htable)
73+
color = cmap(s)
74+
box = Box([size, size, size], face_colors=color)
75+
box.translate(p_3d)
76+
v.add(box)
77+
v.show()
78+
79+
if animate:
80+
input("press enter to start animate")
81+
for q in qs:
82+
spec.set_skrobot_model_state(model, q)
83+
v.redraw()
84+
time.sleep(0.5)
85+
time.sleep(1000)
86+
87+
88+
if __name__ == "__main__":
89+
tyro.cli(main)

0 commit comments

Comments
 (0)