-
Notifications
You must be signed in to change notification settings - Fork 5
feat: Populate scene using batch spawning #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
d0670ae
a4b6147
63093a0
743de23
921216f
e699eeb
6f0b073
377d7a7
e8aa697
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,12 +32,15 @@ | |
| wait_for_ros2_services, | ||
| ) | ||
| from rosidl_runtime_py.convert import message_to_ordereddict | ||
| from simulation_interfaces.msg import EntityState | ||
| from simulation_interfaces.msg import EntityState, Result | ||
| from simulation_interfaces.msg import SpawnEntity as SpawnEntityMsg | ||
| from simulation_interfaces.srv import ( | ||
| GetEntities, | ||
| GetEntityState, | ||
| SetEntityState, | ||
| SpawnEntities, | ||
| SpawnEntity, | ||
| SpawnEntity_Request, | ||
| ) | ||
| from tf2_geometry_msgs import do_transform_pose | ||
| from tf_transformations import euler_from_quaternion, quaternion_from_euler | ||
|
|
@@ -59,6 +62,9 @@ class Collection(Enum): | |
| FREE = "t3" | ||
|
|
||
|
|
||
| SPAWN_ENTITY_REQUEST_TIMEOUT = 3.0 | ||
|
|
||
|
|
||
| class SceneManager: | ||
| def __init__( | ||
| self, | ||
|
|
@@ -192,50 +198,39 @@ def populate_scene( | |
| "Slots and object names must have the same length and items stored" | ||
| ) | ||
|
|
||
| simulation_names: list[str] = [] | ||
| for slot, object_name, item in tqdm( | ||
| zip(slots, object_names, items_stored), | ||
| desc="Spawning entities", | ||
| total=len(slots), | ||
| ): | ||
| if np.isclose(offset_yaw, 0.0): | ||
| should_rotate = random.random() < percent_of_rotated_objects | ||
| if should_rotate: | ||
| # rotate additional 90 degrees | ||
| offset_yaw = random.choice([1.57, -1.57, 3.14]) | ||
| else: | ||
| offset_yaw = 0.0 | ||
|
|
||
| simulation_name = self.spawn_on_spot( | ||
| reqs: list[SpawnEntityMsg] = [] | ||
| for slot, object_name, item in zip(slots, object_names, items_stored): | ||
| req = self.make_spawn_entity_msg_for_spot( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall we update the tests?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I left the old spawning function for spawning single objects, so the tests don't need updating - tests only spawned a small amount of objects for testing specific features, so they don't need it. |
||
| slot_name=slot, | ||
| object_name=object_name, | ||
| item_stored=item, | ||
| std_xy=std_xy, | ||
| std_yaw=std_yaw, | ||
| offset_yaw=offset_yaw, | ||
| rotate_90_degrees=not bool(np.isclose(0.0, percent_of_rotated_objects)), | ||
| rotate_90_degrees_percentage=percent_of_rotated_objects, | ||
| ) | ||
| self.logger.info(f"Simulation name: {simulation_name}") | ||
| simulation_names.append(simulation_name) | ||
| reqs.append(req) | ||
|
|
||
| simulation_names: list[str] = self.spawn_objects(reqs) | ||
| return simulation_names | ||
|
|
||
| def spawn_object( | ||
| def init_spawn_entity_name_and_pose( | ||
| self, | ||
| req: SpawnEntityMsg | SpawnEntity_Request, | ||
| pose: Pose, | ||
| object_name: str, | ||
| item_stored: Optional[str] = None, | ||
| frame: str = "odom", | ||
| ): | ||
| wait_for_ros2_services(self.connector, ["/spawn_entity"]) | ||
| # NOTE (jmatejcz) item stored will be added to name of object | ||
| # and that's how it will be distinguished | ||
| if item_stored: | ||
| name = object_name + f"__{item_stored}__" + str(uuid.uuid4())[:8] | ||
| else: | ||
| name = object_name + str(uuid.uuid4())[:8] | ||
|
|
||
| req = SpawnEntity.Request() | ||
| req.name = name | ||
| req.uri = self.spawnable_to_uri[object_name] | ||
| req.initial_pose.header.frame_id = frame | ||
| req.initial_pose.pose.position.x = pose.position.x | ||
| req.initial_pose.pose.position.y = pose.position.y | ||
|
|
@@ -245,28 +240,76 @@ def spawn_object( | |
| req.initial_pose.pose.orientation.z = pose.orientation.z | ||
| req.initial_pose.pose.orientation.w = pose.orientation.w | ||
|
|
||
| self.logger.debug(f"Spawning {name}") | ||
| return req | ||
|
|
||
| def make_spawn_entity_msg( | ||
| self, | ||
| pose: Pose, | ||
| object_name: str, | ||
| item_stored: Optional[str] = None, | ||
| frame: str = "odom", | ||
| ) -> SpawnEntityMsg: | ||
| req = SpawnEntityMsg() | ||
| self.init_spawn_entity_name_and_pose(req, pose, object_name, item_stored, frame) | ||
| req.entity_resource.uri = self.spawnable_to_uri[object_name] | ||
| return req | ||
|
|
||
| def spawn_object( | ||
| self, | ||
| pose: Pose, | ||
| object_name: str, | ||
| item_stored: Optional[str] = None, | ||
| frame: str = "odom", | ||
| ): | ||
| wait_for_ros2_services(self.connector, ["/spawn_entity"]) | ||
|
|
||
| req = SpawnEntity.Request() | ||
| self.init_spawn_entity_name_and_pose(req, pose, object_name, item_stored, frame) | ||
| req.uri = self.spawnable_to_uri[object_name] | ||
|
|
||
| self.logger.debug(f"Spawning {req.name}") | ||
| result = self.connector.call_service( | ||
| ROS2Message(payload=message_to_ordereddict(req)), | ||
| target="/spawn_entity", | ||
| msg_type="simulation_interfaces/srv/SpawnEntity", | ||
| timeout_sec=3.0, | ||
| timeout_sec=SPAWN_ENTITY_REQUEST_TIMEOUT, | ||
| reuse_client=True, | ||
| ).payload | ||
| result = cast(SpawnEntity.Response, result) | ||
| return name | ||
| return req.name | ||
|
|
||
| def spawn_objects(self, requests: list[SpawnEntityMsg]): | ||
| wait_for_ros2_services(self.connector, ["/spawn_entities"]) | ||
|
|
||
| def spawn_on_spot( | ||
| req = SpawnEntities.Request() | ||
| req.spawn_requests = requests | ||
|
|
||
| self.logger.debug(f"Spawning {[request.name for request in requests]}") | ||
| result = self.connector.call_service( | ||
| ROS2Message(payload=message_to_ordereddict(req)), | ||
| target="/spawn_entities", | ||
| msg_type="simulation_interfaces/srv/SpawnEntities", | ||
| timeout_sec=SPAWN_ENTITY_REQUEST_TIMEOUT, | ||
| reuse_client=True, | ||
| ).payload | ||
| result = cast(SpawnEntities.Response, result) | ||
| for res in result.results: | ||
| if res.result.result != Result.RESULT_OK: | ||
| self.logger.debug(f"ERROR: {res.result.error_message}") | ||
| return [res.entity_name for res in result.results] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is res.entity_name in case of failed spawn?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't expect this function to fail, so correct error handling was not implemented here - I just modified the implementation so that it's compatible with the previous version in the codebase. That print was only for debugging. If we wanted to handle those errors, we would have to refactor the code further and that should be in a seperate PR IMO. |
||
|
|
||
| def make_spawn_entity_msg_for_spot( | ||
| self, | ||
| slot_name: str, | ||
| object_name: str, | ||
| item_stored: Optional[str] = None, | ||
| std_xy: float = 0.0, | ||
| std_yaw: float = 0.0, | ||
| rotate_90_degrees: bool = False, | ||
| rotate_90_degrees_percentage: float = 0.1, | ||
| offset_yaw: float = 0.0, | ||
| frame: str = "odom", | ||
| ): | ||
| wait_for_ros2_services(self.connector, ["/spawn_entity"]) | ||
| ) -> SpawnEntityMsg: | ||
| pose: Pose = copy.deepcopy(self.slots[slot_name].origin_pose) | ||
| # Add Gaussian noise to x, y | ||
| pose.position.x += random.normalvariate(0, std_xy) | ||
|
|
@@ -278,6 +321,9 @@ def spawn_on_spot( | |
|
|
||
| # Add Gaussian noise to yaw | ||
| yaw += random.normalvariate(0, std_yaw) | ||
| if rotate_90_degrees: | ||
| if random.random() < rotate_90_degrees_percentage: | ||
| yaw += random.choice([-np.pi / 2, np.pi / 2]) | ||
| yaw += offset_yaw | ||
|
|
||
| # Convert back to quaternion | ||
|
|
@@ -287,9 +333,7 @@ def spawn_on_spot( | |
| pose.orientation.z = q_new[2] | ||
| pose.orientation.w = q_new[3] | ||
|
|
||
| return self.spawn_object( | ||
| pose=pose, object_name=object_name, item_stored=item_stored, frame=frame | ||
| ) | ||
| return self.make_spawn_entity_msg(pose, object_name, item_stored, frame) | ||
|
|
||
| def clear_scene(self): | ||
| wait_for_ros2_services(self.connector, ["/get_entities", "/delete_entity"]) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was the blanket solution necessary?
We could leave with that although its bit sad.
Notice sim/Gem/Source/* - this is fine for a single layer and will not recurse, right?