Skip to content

Commit 7cdbdb1

Browse files
Multi robot planner (#17)
Restore unnecessary modification Changing folder Name chore: ignore local scenegraph JSON data Modify multi-robot pddl domain so that visited- predicates doesn't need to be defined with specific robots. Define MultiRobotPddlDomain for ground_problem for multi-robot. Now,omniplanner.py is also restored to the original version except the import dsg_pddl.dsg_pddl_planning formatted Add MultiRobotPddlDomain to pddl_grounding.py, Remove unnecessary functions and case for goto-object-domain in dsg_pddl_grounding_Multirobot_Better.py, Fix format using pre-commit implement compile_plan for multirobot planning ROS for multirobotplan jaeyoun local Changing ROS for Multi-robot planner Cleanup by following Aaron's feedback I think that planning should work for single-robot and multi-robot domains Multi-robot domains currently must have a name that contains the string Fix import location Change name to dsg_pddl_grounding_multirobot.py Change name to dsg_pddl_grounding_multirobot at multirobot_ros.py Remove unused code Modify generate_place_containment.py for place-in-region issue Modify generate_place_containment.py for place-in-region issue Debugging for multi-robot action Co-authored-by: Jaeyoun <[email protected]>
1 parent 4367a3b commit 7cdbdb1

16 files changed

+1038
-72
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
*__pycache__*
33
*.pyc
44
*.egg-info*
5+
6+
# Local scenegraph data
7+
omniplanner/examples/scenegraph/*.json

omniplanner/examples/pddl_example.py

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import logging
2-
from importlib.resources import as_file, files
32

4-
import dsg_pddl.domains
53
import numpy as np
64
import spark_dsg
75
from dsg_pddl.pddl_grounding import PddlDomain, PddlGoal
86
from ruamel.yaml import YAML
9-
from utils import DummyRobotPlanningAdaptor
7+
from utils import DummyRobotPlanningAdaptor, load_omniplanner_pddl_domain
108

119
from omniplanner.compile_plan import collect_plans
1210
from omniplanner.omniplanner import PlanRequest, full_planning_pipeline
@@ -43,10 +41,7 @@
4341
)
4442

4543
# Load the PDDL domain you want to use
46-
with as_file(files(dsg_pddl.domains).joinpath("GotoObjectDomain.pddl")) as path:
47-
print(f"Loading domain {path}")
48-
with open(str(path), "r") as fo:
49-
domain = PddlDomain(fo.read())
44+
domain = PddlDomain(load_omniplanner_pddl_domain("GotoObjectDomain.pddl"))
5045

5146

5247
# Build the plan request
@@ -62,7 +57,7 @@
6257
print("Plan from planning domain:")
6358
print(plan)
6459

65-
compiled_plan = compile_plan(adaptors, plan)
60+
compiled_plan = compile_plan(adaptors, "map", plan)
6661
print(compiled_plan)
6762

6863
collected_plans = collect_plans(compiled_plan)
@@ -77,12 +72,7 @@
7772
goal = PddlGoal(robot_id="euclid", pddl_goal="(and (object-in-place o94 p2157))")
7873

7974
# Load the PDDL domain you want to use
80-
with as_file(
81-
files(dsg_pddl.domains).joinpath("ObjectRearrangementDomain.pddl")
82-
) as path:
83-
print(f"Loading domain {path}")
84-
with open(str(path), "r") as fo:
85-
domain = PddlDomain(fo.read())
75+
domain = PddlDomain(load_omniplanner_pddl_domain("ObjectRearrangementDomain.pddl"))
8676

8777

8878
# Build the plan request
@@ -98,7 +88,7 @@
9888
print("Plan from planning domain:")
9989
print(plan)
10090

101-
collected_plan = collect_plans(compile_plan(adaptors, plan))
91+
collected_plan = collect_plans(compile_plan(adaptors, "map", plan))
10292
print("collected plan: ", collected_plan)
10393

10494

@@ -117,12 +107,9 @@
117107
)
118108

119109
# Load the PDDL domain you want to use
120-
with as_file(
121-
files(dsg_pddl.domains).joinpath("RegionObjectRearrangementDomain.pddl")
122-
) as path:
123-
print(f"Loading domain {path}")
124-
with open(str(path), "r") as fo:
125-
domain = PddlDomain(fo.read())
110+
domain = PddlDomain(
111+
load_omniplanner_pddl_domain("RegionObjectRearrangementDomain.pddl")
112+
)
126113

127114

128115
# Build the plan request
@@ -139,5 +126,5 @@
139126
print(plan)
140127

141128

142-
collected_plan = collect_plans(compile_plan(adaptors, plan))
129+
collected_plan = collect_plans(compile_plan(adaptors, "map", plan))
143130
print("collected plan: ", collected_plan)
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Multi-Robot Fast Downward test with real scene graph
4+
Follows the exact format you wanted using existing infrastructure
5+
"""
6+
7+
import logging
8+
9+
import numpy as np
10+
import spark_dsg
11+
from dsg_pddl.pddl_grounding import MultiRobotPddlDomain, PddlGoal
12+
from utils import DummyRobotPlanningAdaptor, load_omniplanner_pddl_domain
13+
14+
from omniplanner.compile_plan import collect_plans
15+
from omniplanner.omniplanner import PlanRequest, full_planning_pipeline
16+
from omniplanner_ros.pddl_planner_ros import compile_plan
17+
18+
logging.basicConfig(level=logging.DEBUG, force=True)
19+
20+
21+
# multi_robot_path = (
22+
# "/home/ubuntu/lxc_datashare/heracles_evaluation_dsg_data/"
23+
# )
24+
# if multi_robot_path not in sys.path:
25+
# sys.path.insert(0, multi_robot_path)
26+
27+
28+
def extract_plan_from_wrapper(plan):
29+
"""Extract the actual plan from OmniPlanner wrappers"""
30+
# Handle SymbolicContext wrapper
31+
if hasattr(plan, "value"):
32+
inner_plan = plan.value
33+
# Handle RobotWrapper
34+
if hasattr(inner_plan, "value"):
35+
return inner_plan.value
36+
return inner_plan
37+
return plan
38+
39+
40+
def calculate_geometric_cost_from_plan(plan_lines, symbols, robot_poses):
41+
"""Calculate geometric path cost from multi-robot plan actions"""
42+
total_cost = 0.0
43+
action_costs = []
44+
45+
for action_line in plan_lines:
46+
if action_line.startswith("(") and action_line.endswith(")"):
47+
action_content = action_line[1:-1]
48+
parts = action_content.split()
49+
50+
if len(parts) >= 4 and parts[0] == "goto-poi":
51+
# robot_id = parts[1]
52+
from_place = parts[2]
53+
to_place = parts[3]
54+
55+
# Get positions from symbols
56+
if from_place in symbols and to_place in symbols:
57+
from_pos = symbols[from_place].position[
58+
:2
59+
] # Take only x,y coordinates
60+
to_pos = symbols[to_place].position[:2]
61+
62+
# Calculate Euclidean distance
63+
distance = np.linalg.norm(np.array(to_pos) - np.array(from_pos))
64+
total_cost += distance
65+
action_costs.append((action_line, distance))
66+
else:
67+
# If symbols not found, assume unit cost
68+
total_cost += 1.0
69+
action_costs.append((action_line, 1.0))
70+
else:
71+
total_cost += 0.1
72+
action_costs.append((action_line, 0.1))
73+
74+
return total_cost, action_costs
75+
76+
77+
def examine_scene_graph_coordinates(G):
78+
"""Examine and display coordinate information from the scene graph"""
79+
print("\n=== Scene Graph Coordinate Information ===")
80+
81+
# Get all nodes by layer
82+
place_nodes = list(G.get_layer(spark_dsg.DsgLayers.PLACES).nodes)
83+
object_nodes = list(G.get_layer(spark_dsg.DsgLayers.OBJECTS).nodes)
84+
85+
print(f"Total places: {len(place_nodes)}")
86+
print(f"Total objects: {len(object_nodes)}")
87+
88+
print("\n--- Sample Places with Coordinates ---")
89+
for i, node in enumerate(place_nodes):
90+
if i >= 100:
91+
break
92+
position = node.attributes.position
93+
print(
94+
f" Place {node.id}: position = [{position[0]:.2f}, {position[1]:.2f}, {position[2]:.2f}]"
95+
)
96+
97+
print("\n--- Sample Objects with Coordinates ---")
98+
for i, node in enumerate(object_nodes):
99+
if i >= 100:
100+
break
101+
position = node.attributes.position
102+
print(
103+
f" Object {node.id}: position = [{position[0]:.2f}, {position[1]:.2f}, {position[2]:.2f}]"
104+
)
105+
106+
107+
def print_region_kinds(G):
108+
"""Print unique region semantic categories present in the scene graph."""
109+
try:
110+
regions_layer = G.get_layer(spark_dsg.DsgLayers.ROOMS)
111+
region_nodes = list(regions_layer.nodes)
112+
# Default ROOM layer id is 4, partition 0
113+
labelspace = G.get_labelspace(4, 0)
114+
kinds_counts = {}
115+
for node in region_nodes:
116+
if hasattr(node, "attributes") and hasattr(
117+
node.attributes, "semantic_label"
118+
):
119+
category = labelspace.get_category(node.attributes.semantic_label)
120+
kinds_counts[category] = kinds_counts.get(category, 0) + 1
121+
print("\n--- Region kinds present ---")
122+
if kinds_counts:
123+
for category, count in sorted(
124+
kinds_counts.items(), key=lambda kv: (-kv[1], kv[0])
125+
):
126+
print(f" {category}: {count}")
127+
else:
128+
print(" (none)")
129+
except Exception as e:
130+
print(f" Region kinds error: {e}")
131+
132+
133+
def print_region_names(G):
134+
"""Print names of all regions present in the scene graph."""
135+
try:
136+
regions_layer = G.get_layer(spark_dsg.DsgLayers.ROOMS)
137+
region_nodes = list(regions_layer.nodes)
138+
print("\n--- Region names ---")
139+
if not region_nodes:
140+
print(" (none)")
141+
return
142+
# Try to use semantic labelspace as fallback if name is empty
143+
try:
144+
labelspace = G.get_labelspace(4, 0)
145+
except Exception:
146+
labelspace = None
147+
for node in region_nodes:
148+
name = getattr(node.attributes, "name", "")
149+
if (not name) and labelspace and hasattr(node.attributes, "semantic_label"):
150+
try:
151+
name = labelspace.get_category(node.attributes.semantic_label)
152+
except Exception:
153+
pass
154+
print(f" {node.id}: {name}")
155+
except Exception as e:
156+
print(f" Region names error: {e}")
157+
158+
159+
def print_places_in_region(G, region_symbol: str):
160+
"""Print all places contained in the given region symbol (e.g., 'r68')."""
161+
try:
162+
# Resolve region node by matching its canonical string form (e.g., R(68)) to input (lowercased)
163+
target = region_symbol.lower()
164+
regions_layer = G.get_layer(spark_dsg.DsgLayers.ROOMS)
165+
region_node = None
166+
for node in regions_layer.nodes:
167+
try:
168+
if node.id.str(True).lower() == target:
169+
region_node = node
170+
break
171+
except Exception:
172+
continue
173+
if region_node is None:
174+
print(f"\n--- Places in {region_symbol} ---\n Region not found")
175+
return
176+
177+
# Use the same place layer as PDDL (MESH_PLACES or fallback to numeric 20)
178+
try:
179+
mesh_places_layer = G.get_layer(spark_dsg.DsgLayers.MESH_PLACES)
180+
except Exception:
181+
mesh_places_layer = G.get_layer(20)
182+
183+
# Map mesh places to regions via nearest 3D place's parent (matches PDDL generation)
184+
places_layer_3d = G.get_layer(spark_dsg.DsgLayers.PLACES)
185+
place_centers = []
186+
place_nodes = []
187+
for n in places_layer_3d.nodes:
188+
place_centers.append(n.attributes.position)
189+
place_nodes.append(n)
190+
if len(place_centers) == 0:
191+
print(f"\n--- Places in {region_symbol} ---\n No 3D places available")
192+
return
193+
place_centers = np.array(place_centers)
194+
195+
places_in_region = []
196+
for mesh_place in mesh_places_layer.nodes:
197+
try:
198+
mp = mesh_place.attributes.position
199+
# Find nearest 3D place
200+
idx = int(np.argmin(np.linalg.norm(place_centers - mp, axis=1)))
201+
nearest_place = place_nodes[idx]
202+
parent_region_id = nearest_place.get_parent()
203+
if parent_region_id and parent_region_id == region_node.id:
204+
places_in_region.append(mesh_place)
205+
except Exception:
206+
continue
207+
208+
print(f"\n--- Places in {region_symbol} ---")
209+
print(f" Count: {len(places_in_region)}")
210+
for p in places_in_region[:200]: # cap to reasonable number
211+
pos = getattr(p.attributes, "position", None)
212+
if pos is not None:
213+
# Print up to 3 components if available
214+
if len(pos) >= 3:
215+
print(f" {p.id}: [{pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f}]")
216+
else:
217+
print(f" {p.id}: [{pos[0]:.2f}, {pos[1]:.2f}]")
218+
else:
219+
print(f" {p.id}")
220+
except Exception as e:
221+
print(f" Error listing places for {region_symbol}: {e}")
222+
223+
224+
"""Main test function following your exact format"""
225+
print("Multi-Robot Fast Downward Test with Real Scene Graph")
226+
print("=" * 80)
227+
228+
# Configuration
229+
# scene_graph_path = "./src/awesome_dcist_t4/omniplanner/omniplanner/examples/scenegraph/west_point_fused_map_wregions_labelspace.json"
230+
scene_graph_path = "/home/ubuntu/lxc_datashare/heracles_evaluation_dsg_data/b45_clip_final_connected_rooms_and_labelspace_fix.json"
231+
# scene_graph_path = "/home/jaeyoun-choi/colcon_ws/assets/west_point_fused_map_wregions_labelspace.json"
232+
# robot_ids = ["robot1","robot2"]
233+
robot_ids = ["robot1", "robot2", "robot3"]
234+
235+
print(f"Scene graph: {scene_graph_path}")
236+
print(f"Robots: {robot_ids}")
237+
238+
# Load scene graph
239+
print(f"Loading scene graph from: {scene_graph_path}")
240+
G = spark_dsg.DynamicSceneGraph.load(scene_graph_path)
241+
print(f"✓ Scene graph loaded: {G.num_nodes()} total nodes")
242+
243+
# Examine scene graph coordinates
244+
examine_scene_graph_coordinates(G)
245+
# Print existing region kinds
246+
247+
print_region_kinds(G)
248+
# Print region names
249+
print_region_names(G)
250+
# Print places included in specific regions of interest
251+
# print_places_in_region(G, "r68")
252+
print_places_in_region(G, "r3")
253+
print_places_in_region(G, "r4")
254+
255+
# Create robot poses (following your format)
256+
robot_poses = {
257+
"ROBOT1": np.array([-15.0, -15.1]),
258+
"ROBOT2": np.array([-15.0, 0.1]),
259+
"ROBOT3": np.array([0.0, 6.0]),
260+
}
261+
262+
print("=========================================")
263+
print("==== PDDL region Domain (Multi-Robot) ====")
264+
print("=========================================")
265+
print("")
266+
267+
# goal_string ="(and (safe o2)(safe o3))"
268+
goal_string = "(and (explored-region r1)(explored-region r2)(visited-object o2)(visited-object o9)(visited-place p22543)(visited-place p6255))"
269+
goal_string = "(and (visited-object o79)(visited-object o285)(visited-object o43)(safe o79)(explored-region r2)(visited-object o2)(visited-object o9)(visited-place p22543)(visited-place p6255))"
270+
# goal_string ="(and (visited-poi o27))"
271+
# goal_string ="(and (object-in-place o5 p91) (object-in-place o85 p118) )"
272+
# goal_string ="(and (object-in-place o5 p91) (object-in-place o94 p2157))"
273+
# goal_string ="(and (safe o2))"
274+
# goal_string ="(and (object-in-place o5 p91) (object-in-place o85 p118) (object-in-place o94 p2157))"
275+
goal = PddlGoal(robot_id="robot1", pddl_goal=goal_string)
276+
277+
# Load the multi-robot domain
278+
domain = MultiRobotPddlDomain(
279+
load_omniplanner_pddl_domain(
280+
"RegionObjectRearrangementDomain_MultiRobot_FD_Explore.pddl"
281+
)
282+
)
283+
284+
285+
# print(f"Loading domain {domain_path}")
286+
# print(f"Domain name: {domain.domain_name}")
287+
req = PlanRequest(
288+
domain=domain,
289+
goal=goal,
290+
robot_states=robot_poses,
291+
)
292+
# Automatically generate a multi-robot PDDL problem from DSG (first pass to discover objects)
293+
# pddl_start_time = time.time()
294+
plan = full_planning_pipeline(req, G)
295+
print("Symbolic Plan:")
296+
for a in plan.value.value.symbolic_actions:
297+
print(a)
298+
299+
adaptor1 = DummyRobotPlanningAdaptor("euclid", "spot", "map", "euclid/body")
300+
adaptor2 = DummyRobotPlanningAdaptor("hamilton", "spot", "map", "hamilton/body")
301+
adaptor3 = DummyRobotPlanningAdaptor("gauss", "husky", "map", "husky/body")
302+
adaptors = {"ROBOT1": adaptor1, "ROBOT2": adaptor2, "ROBOT3": adaptor3}
303+
304+
compiled_plans = compile_plan(adaptors, "map", plan)
305+
306+
collected_plan = collect_plans(compile_plan(adaptors, "map", plan))
307+
print("collected plan: ", collected_plan)

0 commit comments

Comments
 (0)