diff --git a/omnigibson/__init__.py b/omnigibson/__init__.py index a743984a7..b25689a46 100644 --- a/omnigibson/__init__.py +++ b/omnigibson/__init__.py @@ -24,7 +24,7 @@ import nest_asyncio nest_asyncio.apply() -__version__ = "0.2.0" +__version__ = "0.2.1" log.setLevel(logging.DEBUG if gm.DEBUG else logging.INFO) diff --git a/omnigibson/object_states/__init__.py b/omnigibson/object_states/__init__.py index 214a59183..f36cd7b61 100644 --- a/omnigibson/object_states/__init__.py +++ b/omnigibson/object_states/__init__.py @@ -16,7 +16,7 @@ from omnigibson.object_states.next_to import NextTo from omnigibson.object_states.on_fire import OnFire from omnigibson.object_states.on_top import OnTop -from omnigibson.object_states.open import Open +from omnigibson.object_states.open_state import Open from omnigibson.object_states.overlaid import Overlaid from omnigibson.object_states.particle_modifier import ParticleRemover, ParticleApplier from omnigibson.object_states.particle_source_or_sink import ParticleSource, ParticleSink diff --git a/omnigibson/object_states/contains.py b/omnigibson/object_states/contains.py index 64f127806..da32896af 100644 --- a/omnigibson/object_states/contains.py +++ b/omnigibson/object_states/contains.py @@ -109,6 +109,16 @@ def _get_value(self, system): # Grab value from Contains state; True if value is greater than 0 return self.obj.states[ContainedParticles].get_value(system=system).n_in_volume > 0 + def _set_value(self, system, new_value): + if new_value: + # Cannot set contains = True, only False + raise NotImplementedError(f"{self.__class__.__name__} does not support set_value(system, True)") + else: + # Remove all particles from inside the volume + system.remove_particles(idxs=self.obj.states[ContainedParticles].get_value(system).in_volume.nonzero()[0]) + + return True + @classmethod def get_dependencies(cls): deps = super().get_dependencies() diff --git a/omnigibson/object_states/filled.py b/omnigibson/object_states/filled.py index a22d38884..e3f4b50fc 100644 --- a/omnigibson/object_states/filled.py +++ b/omnigibson/object_states/filled.py @@ -34,7 +34,7 @@ def _get_value(self, system): return value def _set_value(self, system, new_value): - # Sanity check to manke sure system is valid + # Sanity check to make sure system is valid assert is_physical_particle_system(system_name=system.name), \ "Can only set Filled state with a valid PhysicalParticleSystem!" @@ -52,8 +52,8 @@ def _set_value(self, system, new_value): check_contact=True, ) else: - # Going from True --> False, remove all particles inside the volume - system.remove_particles(idxs=contained_particles_state.get_value(system).in_volume.nonzero()[0]) + # Cannot set False + raise NotImplementedError(f"{self.__class__.__name__} does not support set_value(system, False)") return True diff --git a/omnigibson/object_states/heat_source_or_sink.py b/omnigibson/object_states/heat_source_or_sink.py index 6e06ec6d6..f824d5fee 100644 --- a/omnigibson/object_states/heat_source_or_sink.py +++ b/omnigibson/object_states/heat_source_or_sink.py @@ -3,7 +3,7 @@ from omnigibson.object_states.inside import Inside from omnigibson.object_states.link_based_state_mixin import LinkBasedStateMixin from omnigibson.object_states.object_state_base import AbsoluteObjectState -from omnigibson.object_states.open import Open +from omnigibson.object_states.open_state import Open from omnigibson.object_states.toggle import ToggledOn from omnigibson.utils.python_utils import classproperty import omnigibson.utils.transform_utils as T diff --git a/omnigibson/object_states/inside.py b/omnigibson/object_states/inside.py index 40fe7e949..62373925e 100644 --- a/omnigibson/object_states/inside.py +++ b/omnigibson/object_states/inside.py @@ -5,6 +5,7 @@ from omnigibson.object_states.object_state_base import BooleanStateMixin, RelativeObjectState from omnigibson.utils.object_state_utils import sample_kinematics from omnigibson.utils.usd_utils import BoundingBoxAPI +from omnigibson.utils.object_state_utils import m as os_m class Inside(RelativeObjectState, KinematicsMixin, BooleanStateMixin): @@ -20,10 +21,11 @@ def _set_value(self, other, new_value): state = og.sim.dump_state(serialized=False) - if sample_kinematics("inside", self.obj, other) and self.get_value(other): - return True - else: - og.sim.load_state(state, serialized=False) + for _ in range(os_m.DEFAULT_HIGH_LEVEL_SAMPLING_ATTEMPTS): + if sample_kinematics("inside", self.obj, other) and self.get_value(other): + return True + else: + og.sim.load_state(state, serialized=False) return False diff --git a/omnigibson/object_states/on_top.py b/omnigibson/object_states/on_top.py index fa3f465d2..2ed8e3041 100644 --- a/omnigibson/object_states/on_top.py +++ b/omnigibson/object_states/on_top.py @@ -4,6 +4,7 @@ from omnigibson.object_states.object_state_base import BooleanStateMixin, RelativeObjectState from omnigibson.object_states.touching import Touching from omnigibson.utils.object_state_utils import sample_kinematics +from omnigibson.utils.object_state_utils import m as os_m class OnTop(KinematicsMixin, RelativeObjectState, BooleanStateMixin): @@ -20,10 +21,11 @@ def _set_value(self, other, new_value): state = og.sim.dump_state(serialized=False) - if sample_kinematics("onTop", self.obj, other) and self.get_value(other): - return True - else: - og.sim.load_state(state, serialized=False) + for _ in range(os_m.DEFAULT_HIGH_LEVEL_SAMPLING_ATTEMPTS): + if sample_kinematics("onTop", self.obj, other) and self.get_value(other): + return True + else: + og.sim.load_state(state, serialized=False) return False @@ -33,4 +35,5 @@ def _get_value(self, other): return False adjacency = self.obj.states[VerticalAdjacency].get_value() - return other in adjacency.negative_neighbors and other not in adjacency.positive_neighbors + other_adjacency = other.states[VerticalAdjacency].get_value() + return other in adjacency.negative_neighbors and other not in adjacency.positive_neighbors and self.obj not in other_adjacency.negative_neighbors diff --git a/omnigibson/object_states/open.py b/omnigibson/object_states/open_state.py similarity index 100% rename from omnigibson/object_states/open.py rename to omnigibson/object_states/open_state.py diff --git a/omnigibson/object_states/particle_modifier.py b/omnigibson/object_states/particle_modifier.py index 67dee3cbc..e12185855 100644 --- a/omnigibson/object_states/particle_modifier.py +++ b/omnigibson/object_states/particle_modifier.py @@ -524,11 +524,11 @@ def _update(self): # Check if all conditions are met if np.all([condition(self.obj) for condition in conditions]): system = get_system(system_name) - # Update saturation limit if it's not the desired one + # Update saturation limit if it's not specified yet limit = self.visual_particle_modification_limit \ if is_visual_particle_system(system_name=system.name) \ else self.physical_particle_modification_limit - if limit != self.obj.states[Saturated].get_limit(system=system): + if system not in self.obj.states[Saturated].limits: self.obj.states[Saturated].set_limit(system=system, limit=limit) # Sanity check for oversaturation if self.obj.states[Saturated].get_value(system=system): diff --git a/omnigibson/object_states/saturated.py b/omnigibson/object_states/saturated.py index 76cf88639..e6981bb87 100644 --- a/omnigibson/object_states/saturated.py +++ b/omnigibson/object_states/saturated.py @@ -64,7 +64,7 @@ def _dump_state(self): return state def _load_state(self, state): - self.particle_counts = {REGISTERED_SYSTEMS[system_name]: val for system_name, val in state.items() if system_name != "n_systems"} + self.particle_counts = {REGISTERED_SYSTEMS[system_name]: val for system_name, val in state.items() if system_name != "n_systems" and val > 0} def _serialize(self, state): state_flat = np.array([state["n_systems"]], dtype=float) @@ -107,6 +107,14 @@ def _initialize(self): # Set internal variables self._limits = dict() + @property + def limits(self): + """ + Returns: + dict: Maps system to limit count for that system, if it exists + """ + return self._limits + def get_limit(self, system): """ Grabs the internal particle limit for @system @@ -202,7 +210,8 @@ def _load_state(self, state): continue elif k == "default_limit": self._default_limit = v - else: + # TODO: Make this an else once fresh round of sampling occurs (i.e.: no more outdated systems stored) + elif k in REGISTERED_SYSTEMS: self._limits[REGISTERED_SYSTEMS[k]] = v def _serialize(self, state): diff --git a/omnigibson/object_states/toggle.py b/omnigibson/object_states/toggle.py index 1334af08d..d27cf88fa 100644 --- a/omnigibson/object_states/toggle.py +++ b/omnigibson/object_states/toggle.py @@ -69,6 +69,7 @@ def _initialize(self): self.visual_marker = VisualGeomPrim(prim_path=mesh_prim_path, name=f"{self.obj.name}_visual_marker") self.visual_marker.scale = self.scale self.visual_marker.initialize() + self.visual_marker.visible = True # Make sure the marker isn't translated at all self.visual_marker.set_local_pose(translation=np.zeros(3), orientation=np.array([0, 0, 0, 1.0])) diff --git a/omnigibson/object_states/touching.py b/omnigibson/object_states/touching.py index efb715f5a..85619872a 100644 --- a/omnigibson/object_states/touching.py +++ b/omnigibson/object_states/touching.py @@ -20,11 +20,12 @@ def _get_value(self, other): return self._check_contact(other, self.obj) elif other.prim_type == PrimType.CLOTH: return self._check_contact(self.obj, other) - elif not self.obj.kinematic_only and not other.kinematic_only: - # Use optimized check for rigid bodies - return RigidContactAPI.in_contact( - prim_paths_a=[link.prim_path for link in self.obj.links.values()], - prim_paths_b=[link.prim_path for link in other.links.values()], - ) + # elif not self.obj.kinematic_only and not other.kinematic_only: + # # Use optimized check for rigid bodies + # # TODO: Use once NVIDIA fixes their absolutely broken API + # return RigidContactAPI.in_contact( + # prim_paths_a=[link.prim_path for link in self.obj.links.values()], + # prim_paths_b=[link.prim_path for link in other.links.values()], + # ) else: return self._check_contact(other, self.obj) and self._check_contact(self.obj, other) diff --git a/omnigibson/object_states/under.py b/omnigibson/object_states/under.py index eee52bf12..800607ec8 100644 --- a/omnigibson/object_states/under.py +++ b/omnigibson/object_states/under.py @@ -3,6 +3,7 @@ from omnigibson.object_states.kinematics_mixin import KinematicsMixin from omnigibson.object_states.object_state_base import BooleanStateMixin, RelativeObjectState from omnigibson.utils.object_state_utils import sample_kinematics +from omnigibson.utils.object_state_utils import m as os_m class Under(RelativeObjectState, KinematicsMixin, BooleanStateMixin): @@ -18,13 +19,15 @@ def _set_value(self, other, new_value): state = og.sim.dump_state(serialized=False) - if sample_kinematics("under", self.obj, other) and self.get_value(other): - return True - else: - og.sim.load_state(state, serialized=False) + for _ in range(os_m.DEFAULT_HIGH_LEVEL_SAMPLING_ATTEMPTS): + if sample_kinematics("under", self.obj, other) and self.get_value(other): + return True + else: + og.sim.load_state(state, serialized=False) return False def _get_value(self, other): adjacency = self.obj.states[VerticalAdjacency].get_value() - return other not in adjacency.negative_neighbors and other in adjacency.positive_neighbors + other_adjacency = other.states[VerticalAdjacency].get_value() + return other not in adjacency.negative_neighbors and other in adjacency.positive_neighbors and self.obj not in other_adjacency.positive_neighbors diff --git a/omnigibson/objects/dataset_object.py b/omnigibson/objects/dataset_object.py index 77d4e9a69..aa536ce61 100644 --- a/omnigibson/objects/dataset_object.py +++ b/omnigibson/objects/dataset_object.py @@ -1,7 +1,6 @@ import itertools import math import os -import tempfile import stat import cv2 import numpy as np @@ -16,7 +15,7 @@ from omnigibson.utils.constants import AVERAGE_CATEGORY_SPECS, DEFAULT_JOINT_FRICTION, SPECIAL_JOINT_FRICTIONS, JointType import omnigibson.utils.transform_utils as T from omnigibson.utils.usd_utils import BoundingBoxAPI -from omnigibson.utils.asset_utils import decrypt_file, get_all_object_category_models +from omnigibson.utils.asset_utils import get_all_object_category_models from omnigibson.utils.constants import PrimType from omnigibson.macros import gm, create_module_macros from omnigibson.utils.ui_utils import create_module_logger @@ -128,6 +127,7 @@ def __init__( super().__init__( prim_path=prim_path, usd_path=usd_path, + encrypted=True, name=name, category=category, class_id=class_id, @@ -217,21 +217,6 @@ def recursive_light_update(child_prim): if joint.joint_type != JointType.JOINT_FIXED: joint.friction = friction - def _load(self): - # Create a temporary file to store the decrytped asset, load it, and then delete it. - original_usd_path = self._usd_path - encrypted_filename = original_usd_path.replace(".usd", ".encrypted.usd") - decrypted_fd, decrypted_filename = tempfile.mkstemp(os.path.basename(original_usd_path), dir=og.tempdir) - decrypt_file(encrypted_filename, decrypted_filename) - self._usd_path = decrypted_filename - prim = super()._load() - os.close(decrypted_fd) - # On Windows, Isaac Sim won't let go of the file until the prim is removed, so we can't delete it. - if os.name == "posix": - os.remove(decrypted_filename) - self._usd_path = original_usd_path - return prim - def _post_load(self): # We run this post loading first before any others because we're modifying the load config that will be used # downstream @@ -323,6 +308,7 @@ def _update_texture_change(self, object_state): # else: # print(f"Warning: DatasetObject [{self.prim_path}] does not have texture map: " # f"[{target_texture_path}]. Falling back to directly updating albedo value.") + self._update_albedo_value(object_state, material) def set_bbox_center_position_orientation(self, position=None, orientation=None): diff --git a/omnigibson/objects/light_object.py b/omnigibson/objects/light_object.py index 80f67066b..62d975bd4 100644 --- a/omnigibson/objects/light_object.py +++ b/omnigibson/objects/light_object.py @@ -7,6 +7,7 @@ from omnigibson.utils.python_utils import assert_valid_key from omnigibson.utils.constants import PrimType from omnigibson.utils.ui_utils import create_module_logger +import numpy as np # Create module logger log = create_module_logger(module_name=__name__) @@ -137,6 +138,13 @@ def _initialize(self): # Initialize light link self._light_link.initialize() + @property + def aabb(self): + # This is a virtual object (with no associated visual mesh), so omni returns an invalid AABB. + # Therefore we instead return a hardcoded small value + return np.ones(3) * -0.001, np.ones(3) * 0.001 + + @property def light_link(self): """ diff --git a/omnigibson/objects/stateful_object.py b/omnigibson/objects/stateful_object.py index 2b7f914d8..87ec940df 100644 --- a/omnigibson/objects/stateful_object.py +++ b/omnigibson/objects/stateful_object.py @@ -444,11 +444,16 @@ def _update_albedo_value(object_state, material): # Query the object state for the parameters albedo_add, diffuse_tint = object_state.get_texture_change_params() - if material.albedo_add != albedo_add: - material.albedo_add = albedo_add + if material.is_glass: + if not np.allclose(material.glass_color, diffuse_tint): + material.glass_color = diffuse_tint - if not np.allclose(material.diffuse_tint, diffuse_tint): - material.diffuse_tint = diffuse_tint + else: + if material.albedo_add != albedo_add: + material.albedo_add = albedo_add + + if not np.allclose(material.diffuse_tint, diffuse_tint): + material.diffuse_tint = diffuse_tint def remove(self): """ diff --git a/omnigibson/objects/usd_object.py b/omnigibson/objects/usd_object.py index 0a02a1594..052f368f3 100644 --- a/omnigibson/objects/usd_object.py +++ b/omnigibson/objects/usd_object.py @@ -1,6 +1,11 @@ +import os +import tempfile + +import omnigibson as og from omnigibson.objects.stateful_object import StatefulObject from omnigibson.utils.constants import PrimType from omnigibson.utils.usd_utils import add_asset_to_stage +from omnigibson.utils.asset_utils import decrypt_file class USDObject(StatefulObject): @@ -13,6 +18,7 @@ def __init__( self, name, usd_path, + encrypted=False, prim_path=None, category="object", class_id=None, @@ -32,6 +38,7 @@ def __init__( Args: name (str): Name for the object. Names need to be unique per scene usd_path (str): global path to the USD file to load + encrypted (bool): whether this file is encrypted (and should therefore be decrypted) or not prim_path (None or str): global path in the stage to this object. If not specified, will automatically be created at /World/ category (str): Category for the object. Defaults to "object". @@ -59,6 +66,7 @@ def __init__( that kwargs are only shared between all SUBclasses (children), not SUPERclasses (parents). """ self._usd_path = usd_path + self._encrypted = encrypted super().__init__( prim_path=prim_path, name=name, @@ -81,7 +89,22 @@ def _load(self): """ Load the object into pybullet and set it to the correct pose """ - return add_asset_to_stage(asset_path=self._usd_path, prim_path=self._prim_path) + usd_path = self._usd_path + if self._encrypted: + # Create a temporary file to store the decrytped asset, load it, and then delete it + encrypted_filename = self._usd_path.replace(".usd", ".encrypted.usd") + decrypted_fd, usd_path = tempfile.mkstemp(os.path.basename(self._usd_path), dir=og.tempdir) + decrypt_file(encrypted_filename, usd_path) + + prim = add_asset_to_stage(asset_path=usd_path, prim_path=self._prim_path) + + if self._encrypted: + os.close(decrypted_fd) + # On Windows, Isaac Sim won't let go of the file until the prim is removed, so we can't delete it. + if os.name == "posix": + os.remove(usd_path) + + return prim def _create_prim_with_same_kwargs(self, prim_path, name, load_config): # Add additional kwargs diff --git a/omnigibson/prims/entity_prim.py b/omnigibson/prims/entity_prim.py index f7b38097d..ea33efd2b 100644 --- a/omnigibson/prims/entity_prim.py +++ b/omnigibson/prims/entity_prim.py @@ -61,7 +61,7 @@ def __init__( self._visual_only = None # This needs to be initialized to be used for _load() of PrimitiveObject - self._prim_type = load_config["prim_type"] if "prim_type" in load_config else PrimType.RIGID + self._prim_type = load_config["prim_type"] if load_config is not None and "prim_type" in load_config else PrimType.RIGID assert self._prim_type in iter(PrimType), f"Unknown prim type {self._prim_type}!" # Run super init @@ -138,6 +138,10 @@ def _post_load(self): if gm.AG_CLOTH: self.create_attachment_point_link() + # Globally disable any requested collision links + for link_name in self.disabled_collision_link_names: + self._links[link_name].disable_collisions() + # Disable any requested collision pairs for a_name, b_name in self.disabled_collision_pairs: link_a, link_b = self._links[a_name], self._links[b_name] @@ -1092,6 +1096,14 @@ def joint_has_limits(self): """ return np.array([j.has_limit for j in self._joints.values()]) + @property + def disabled_collision_link_names(self): + """ + Returns: + list of str: List of link names for this entity whose collisions should be globally disabled + """ + return [] + @property def disabled_collision_pairs(self): """ @@ -1291,6 +1303,8 @@ def keep_still(self): self.set_angular_velocity(velocity=np.zeros(3)) for joint in self._joints.values(): joint.keep_still() + # Make sure object is awake + self.wake() def create_attachment_point_link(self): """ @@ -1361,6 +1375,9 @@ def _load_state(self, state): for joint_name, joint_state in state["joints"].items(): self._joints[joint_name]._load_state(state=joint_state) + # Make sure this object is awake + self.wake() + def _serialize(self, state): # We serialize by first flattening the root link state and then iterating over all joints and # adding them to the a flattened array diff --git a/omnigibson/prims/material_prim.py b/omnigibson/prims/material_prim.py index ccd635029..3345a7369 100644 --- a/omnigibson/prims/material_prim.py +++ b/omnigibson/prims/material_prim.py @@ -153,6 +153,14 @@ def set_input(self, inp, val): f"Got invalid shader input to set! Current inputs are: {self.shader_input_names}. Got: {inp}" self._shader.GetInput(inp).Set(val) + @property + def is_glass(self): + """ + Returns: + bool: Whether this material is a glass material or not + """ + return "glass_color" in self.shader_input_names + @property def shader(self): """ @@ -1044,3 +1052,23 @@ def enable_diffuse_transmission(self, val): val (bool): this material's applied enable_diffuse_transmission """ self.set_input(inp="enable_diffuse_transmission", val=val) + + @property + def glass_color(self): + """ + Returns: + 3-array: this material's applied (R,G,B) glass color (only applicable to OmniGlass materials) + """ + assert self.is_glass, f"Tried to query glass_color shader input, " \ + f"but material at {self.prim_path} is not an OmniGlass material!" + return np.array(self.get_input(inp="glass_color")) + + @glass_color.setter + def glass_color(self, color): + """ + Args: + color (3-array): this material's applied (R,G,B) glass color (only applicable to OmniGlass materials) + """ + assert self.is_glass, f"Tried to set glass_color shader input, " \ + f"but material at {self.prim_path} is not an OmniGlass material!" + self.set_input(inp="glass_color", val=Gf.Vec3f(*np.array(color, dtype=float))) diff --git a/omnigibson/robots/tiago.py b/omnigibson/robots/tiago.py index a9e2e4657..1387778a5 100644 --- a/omnigibson/robots/tiago.py +++ b/omnigibson/robots/tiago.py @@ -509,9 +509,20 @@ def gripper_control_idx(self): def finger_lengths(self): return {arm: 0.12 for arm in self.arm_names} + @property + def disabled_collision_link_names(self): + # These should NEVER have collisions in the first place (i.e.: these are poorly modeled geoms from the source + # asset) -- they are strictly engulfed within ANOTHER collision mesh from a DIFFERENT link + return [name for arm in self.arm_names for name in [f"arm_{arm}_tool_link", f"wrist_{arm}_ft_link", f"wrist_{arm}_ft_tool_link"]] + @property def disabled_collision_pairs(self): - return [] + return [ + ["torso_fixed_column_link", "torso_fixed_link"], + ["torso_fixed_column_link", "torso_lift_link"], + ["arm_left_6_link", "gripper_left_link"], + ["arm_right_6_link", "gripper_right_link"], + ] @property def arm_link_names(self): diff --git a/omnigibson/systems/macro_particle_system.py b/omnigibson/systems/macro_particle_system.py index 71f73fd64..13dc767c6 100644 --- a/omnigibson/systems/macro_particle_system.py +++ b/omnigibson/systems/macro_particle_system.py @@ -1472,58 +1472,3 @@ def _deserialize(cls, state): idx += len_velocities return state_dict, idx - - -MacroVisualParticleSystem.create( - name="dust", - scale_relative_to_parent=False, - create_particle_template=lambda prim_path, name: og.objects.PrimitiveObject( - prim_path=prim_path, - primitive_type="Cube", - name=name, - class_id=SemanticClass.DIRT, - size=0.01, - rgba=[0.2, 0.2, 0.1, 1.0], - visible=False, - fixed_base=False, - visual_only=True, - include_default_states=False, - abilities={}, - ) -) - - -MacroVisualParticleSystem.create( - name="stain", - scale_relative_to_parent=True, - create_particle_template=lambda prim_path, name: og.objects.USDObject( - prim_path=prim_path, - usd_path=os.path.join(gm.ASSET_PATH, "models", "stain", "stain.usd"), - name=name, - class_id=SemanticClass.DIRT, - visible=False, - fixed_base=False, - visual_only=True, - include_default_states=False, - abilities={}, - ), -) - - -MacroPhysicalParticleSystem.create( - name="raspberry", - particle_density=800.0, - create_particle_template=lambda prim_path, name: og.objects.DatasetObject( - prim_path=prim_path, - name=name, - # class_id=SemanticClass.DIRT, - visible=False, - fixed_base=False, - visual_only=True, - include_default_states=False, - category="raspberry", - model="spkers", - abilities={}, - ), - scale=np.ones(3) * 5.0, -) diff --git a/omnigibson/systems/micro_particle_system.py b/omnigibson/systems/micro_particle_system.py index 4380d9195..0c0361f29 100644 --- a/omnigibson/systems/micro_particle_system.py +++ b/omnigibson/systems/micro_particle_system.py @@ -1510,65 +1510,6 @@ def cm_create_particle_template(cls): **kwargs, ) -FluidSystem.create( - name="water", - particle_contact_offset=0.012, - particle_density=500.0, - is_viscous=False, - material_mtl_name="DeepWater", -) - -FluidSystem.create( - name="milk", - particle_contact_offset=0.008, - particle_density=500.0, - is_viscous=False, - material_mtl_name="WholeMilk", -) - -FluidSystem.create( - name="strawberry_smoothie", - particle_contact_offset=0.008, - particle_density=500.0, - is_viscous=True, - material_mtl_name="SkimMilk", - customize_particle_material=customize_particle_material_factory("specular_reflection_color", [1.0, 0.64, 0.64]), -) - -GranularSystem.create( - name="diced_apple", - particle_density=500.0, - create_particle_template=lambda prim_path, name: og.objects.DatasetObject( - prim_path=prim_path, - name=name, - category="apple", - model="agveuv", - visible=False, - fixed_base=False, - visual_only=True, - include_default_states=False, - abilities={}, - ), - scale=np.ones(3) * 0.3, -) - -GranularSystem.create( - name="dango", - particle_density=500.0, - create_particle_template=lambda prim_path, name: og.objects.PrimitiveObject( - prim_path=prim_path, - name=name, - primitive_type="Sphere", - radius=0.015, - rgba=[1.0, 1.0, 1.0, 1.0], - visible=False, - fixed_base=False, - visual_only=True, - include_default_states=False, - abilities={}, - ) -) - class Cloth(MicroParticleSystem): """ diff --git a/omnigibson/systems/system_base.py b/omnigibson/systems/system_base.py index 1be46a052..b0f0dd560 100644 --- a/omnigibson/systems/system_base.py +++ b/omnigibson/systems/system_base.py @@ -31,6 +31,10 @@ _CALLBACKS_ON_SYSTEM_CLEAR = dict() +# Modifiers denoting a semantic difference in the system +SYSTEM_PREFIXES = {"diced", "cooked"} + + class BaseSystem(SerializableNonInstance, UniquelyNamedNonInstance): """ Base class for all systems. These are non-instance objects that should be used globally for a given environment. @@ -52,7 +56,11 @@ class BaseSystem(SerializableNonInstance, UniquelyNamedNonInstance): def __init_subclass__(cls, **kwargs): # While class names are camel case, we convert them to snake case to be consistent with object categories. - cls._snake_case_name = camel_case_to_snake_case(cls.__name__) + name = camel_case_to_snake_case(cls.__name__) + # Make sure prefixes preserve their double underscore + for prefix in SYSTEM_PREFIXES: + name = name.replace(f"{prefix}_", f"{prefix}__") + cls._snake_case_name = name cls.min_scale = np.ones(3) cls.max_scale = np.ones(3) @@ -1120,12 +1128,19 @@ def _customize_particle_material(mat: MaterialPrim): --> None system_type = metadata["type"] system_kwargs = dict(name=system_name) - asset_path = os.path.join(system_dir, f"{system_name}.usd") - has_asset = os.path.exists(asset_path) + particle_assets = set(os.listdir(system_dir)) + particle_assets.remove("metadata.json") + has_asset = len(particle_assets) > 0 + if has_asset: + model = sorted(particle_assets)[0] + asset_path = os.path.join(system_dir, model, "usd", f"{model}.usd") + else: + asset_path = None + if not has_asset: if system_type == "macro_visual_particle": # Fallback to stain asset - asset_path = os.path.join(gm.ASSET_PATH, "models", "stain", "stain.usd") + asset_path = os.path.join(gm.DATASET_PATH, "systems", "stain", "ahkjul", "usd", "stain.usd") has_asset = True if has_asset: def generate_particle_template_fcn(): @@ -1134,6 +1149,7 @@ def generate_particle_template_fcn(): prim_path=prim_path, name=name, usd_path=asset_path, + encrypted=True, category=system_name, visible=False, fixed_base=False, @@ -1190,7 +1206,7 @@ def import_og_systems(): if os.path.exists(system_dir): system_names = os.listdir(system_dir) for system_name in system_names: - if system_name.replace("__", "_") not in REGISTERED_SYSTEMS: + if system_name not in REGISTERED_SYSTEMS: _create_system_from_metadata(system_name=system_name) diff --git a/omnigibson/transition_rules.py b/omnigibson/transition_rules.py index 36b3510ca..4fbd1d9c1 100644 --- a/omnigibson/transition_rules.py +++ b/omnigibson/transition_rules.py @@ -2,8 +2,10 @@ from abc import ABCMeta, abstractmethod from collections import namedtuple, defaultdict import numpy as np +import json from copy import copy import itertools +import os import omnigibson as og from omnigibson.macros import gm, create_module_macros from omnigibson.systems import get_system, is_system_active, PhysicalParticleSystem, VisualParticleSystem, REGISTERED_SYSTEMS @@ -40,9 +42,9 @@ ObjectAttrs = namedtuple( "ObjectAttrs", _attrs_fields, defaults=(None,) * len(_attrs_fields)) -# Tuple of lists of objects to be added or removed returned from transitions. +# Tuple of lists of objects to be added or removed returned from transitions, if not None TransitionResults = namedtuple( - "TransitionResults", ["add", "remove"], defaults=([], [])) + "TransitionResults", ["add", "remove"], defaults=(None, None)) # Global dicts that will contain mappings REGISTERED_RULES = dict() @@ -169,23 +171,33 @@ def step(cls): # og.sim.remove_object(removed_obj) # Then add new objects - for added_obj_attr in added_obj_attrs: - new_obj = added_obj_attr.obj - og.sim.import_object(new_obj) - # By default, added_obj_attr is populated with all Nones -- so these will all be pass-through operations - # unless pos / orn (or, conversely, bb_pos / bb_orn) is specified - if added_obj_attr.pos is not None or added_obj_attr.orn is not None: - new_obj.set_position_orientation(position=added_obj_attr.pos, orientation=added_obj_attr.orn) - elif isinstance(new_obj, DatasetObject) and \ - (added_obj_attr.bb_pos is not None or added_obj_attr.bb_orn is not None): - new_obj.set_bbox_center_position_orientation(position=added_obj_attr.bb_pos, - orientation=added_obj_attr.bb_orn) - else: - raise ValueError("Expected at least one of pos, orn, bb_pos, or bb_orn to be specified in ObjectAttrs!") - # Additionally record any requested states if specified to be updated during the next transition step - if added_obj_attr.states is not None or added_obj_attr.callback is not None: - cls._INIT_INFO[new_obj]["states"] = added_obj_attr.states - cls._INIT_INFO[new_obj]["callback"] = added_obj_attr.callback + if len(added_obj_attrs) > 0: + # TODO: Can we avoid this? Currently Rigid contact checking fails if we import objects dynamically + state = og.sim.dump_state() + for added_obj_attr in added_obj_attrs: + new_obj = added_obj_attr.obj + og.sim.import_object(new_obj) + # By default, added_obj_attr is populated with all Nones -- so these will all be pass-through operations + # unless pos / orn (or, conversely, bb_pos / bb_orn) is specified + if added_obj_attr.pos is not None or added_obj_attr.orn is not None: + new_obj.set_position_orientation(position=added_obj_attr.pos, orientation=added_obj_attr.orn) + elif isinstance(new_obj, DatasetObject) and \ + (added_obj_attr.bb_pos is not None or added_obj_attr.bb_orn is not None): + new_obj.set_bbox_center_position_orientation(position=added_obj_attr.bb_pos, + orientation=added_obj_attr.bb_orn) + else: + raise ValueError("Expected at least one of pos, orn, bb_pos, or bb_orn to be specified in ObjectAttrs!") + # Additionally record any requested states if specified to be updated during the next transition step + if added_obj_attr.states is not None or added_obj_attr.callback is not None: + cls._INIT_INFO[new_obj] = { + "states": added_obj_attr.states, + "callback": added_obj_attr.callback, + } + gm.ENABLE_TRANSITION_RULES = False + og.sim.stop() + og.sim.play() + gm.ENABLE_TRANSITION_RULES = True + og.sim.load_state(state) @classmethod def clear(cls): @@ -459,9 +471,13 @@ def __call__(self, object_candidates): # Iterate over all current candidates -- if there's a mismatch in last valid candidates and current, # then we store it, otherwise, we don't for filter_name in self.modifies_filter_names: - objs = [obj for obj in object_candidates[filter_name] if obj not in self._last_valid_candidates] + # Compute current valid candidates + objs = [obj for obj in object_candidates[filter_name] if obj not in self._last_valid_candidates[filter_name]] + # Store last valid objects -- these are all candidates that were validated by self._condition at the + # current timestep + self._last_valid_candidates[filter_name] = set(object_candidates[filter_name]) + # Update current object candidates with the change-filtered ones object_candidates[filter_name] = objs - self._last_valid_candidates[filter_name] = set(objs) valid = valid and len(objs) > 0 # Valid if any object conditions have changed and we still have valid objects @@ -701,7 +717,7 @@ def _generate_conditions(cls): @classmethod def transition(cls, object_candidates): - t_results = TransitionResults() + objs_to_add, objs_to_remove = [], [] for sliceable_obj in object_candidates["sliceable"]: # Object parts offset annotation are w.r.t the base link of the whole object. pos, orn = sliceable_obj.get_position_orientation() @@ -740,12 +756,12 @@ def transition(cls, object_candidates): bb_pos=part_bb_pos, bb_orn=part_bb_orn, ) - t_results.add.append(new_obj_attrs) + objs_to_add.append(new_obj_attrs) # Delete original object from stage. - t_results.remove.append(sliceable_obj) + objs_to_remove.append(sliceable_obj) - return t_results + return TransitionResults(add=objs_to_add, remove=objs_to_remove) class DicingRule(BaseTransitionRule): @@ -766,16 +782,16 @@ def _generate_conditions(cls): @classmethod def transition(cls, object_candidates): - t_results = TransitionResults() + objs_to_remove = [] for diceable_obj in object_candidates["diceable"]: system = get_system(f"diced_{diceable_obj.category}") system.generate_particles_from_link(diceable_obj, diceable_obj.root_link, check_contact=False, use_visual_meshes=False) # Delete original object from stage. - t_results.remove.append(diceable_obj) + objs_to_remove.append(diceable_obj) - return t_results + return TransitionResults(add=[], remove=objs_to_remove) class CookingPhysicalParticleRule(BaseTransitionRule): @@ -794,7 +810,6 @@ def _generate_conditions(cls): @classmethod def transition(cls, object_candidates): - t_results = TransitionResults() fillable_objs = object_candidates["fillable"] # Iterate over all active physical particle systems, and for any non-cooked particles inside, @@ -818,7 +833,7 @@ def transition(cls, object_candidates): system.remove_particles(idxs=in_volume_idx) cooked_system.generate_particles(positions=particle_positions[in_volume_idx]) - return t_results + return TransitionResults(add=[], remove=[]) class MeltingRule(BaseTransitionRule): @@ -837,7 +852,7 @@ def _generate_conditions(cls): @classmethod def transition(cls, object_candidates): - t_results = TransitionResults() + objs_to_remove = [] # Convert the meltable object into its melted substance for meltable_obj in object_candidates["meltable"]: @@ -845,9 +860,9 @@ def transition(cls, object_candidates): system.generate_particles_from_link(meltable_obj, meltable_obj.root_link, check_contact=False, use_visual_meshes=False) # Delete original object from stage. - t_results.remove.append(meltable_obj) + objs_to_remove.append(meltable_obj) - return t_results + return TransitionResults(add=[], remove=objs_to_remove) class RecipeRule(BaseTransitionRule): @@ -866,6 +881,9 @@ class RecipeRule(BaseTransitionRule): # Flattened array of all simulator objects, sorted by category _OBJECTS = None + # Maps object to idx within the _OBJECTS array + _OBJECTS_TO_IDX = None + def __init_subclass__(cls, **kwargs): # Run super first super().__init_subclass__(**kwargs) @@ -874,7 +892,15 @@ def __init_subclass__(cls, **kwargs): cls._RECIPES = dict() @classmethod - def add_recipe(cls, name, input_objects=None, input_systems=None, output_objects=None, output_systems=None, **kwargs): + def add_recipe( + cls, + name, + input_objects=None, + input_systems=None, + output_objects=None, + output_systems=None, + **kwargs, + ): """ Adds a recipe to this recipe rule to check against. This defines a valid mapping of inputs that will transform into the outputs @@ -920,7 +946,7 @@ def _validate_recipe_systems_are_contained(cls, recipe, container): """ for system_name in recipe["input_systems"]: system = get_system(system_name=system_name) - if container.states[Contains].get_value(system=get_system(system_name=system_name)): + if not container.states[Contains].get_value(system=get_system(system_name=system_name)): return False return True @@ -937,7 +963,13 @@ def _validate_nonrecipe_systems_not_contained(cls, recipe, container): bool: True if none of the non-relevant systems are contained """ relevant_systems = set(recipe["input_systems"]) - return system.name in relevant_systems or not container.states[Contains].get_value(system=system) + for system in og.sim.scene.system_registry.objects: + # Skip cloth system + if system.name == "cloth": + continue + if system.name not in relevant_systems and container.states[Contains].get_value(system=system): + return False + return True @classmethod def _validate_recipe_objects_are_contained(cls, recipe, in_volume): @@ -971,8 +1003,9 @@ def _validate_nonrecipe_objects_not_contained(cls, recipe, in_volume): Returns: bool: True if none of the non-relevant objects are contained """ - idxs = np.concatenate(cls._CATEGORY_IDXS[obj_category] for obj_category in recipe["input_objects"].keys()) - return not np.any(np.delete(in_volume, idxs)) + nonrecipe_objects_in_volume = in_volume if len(recipe["input_objects"]) == 0 else \ + np.delete(in_volume, np.concatenate([cls._CATEGORY_IDXS[obj_category] for obj_category in recipe["input_objects"].keys()])) + return not np.any(nonrecipe_objects_in_volume) @classmethod def _validate_recipe_systems_exist(cls, recipe): @@ -1104,6 +1137,9 @@ def _compute_container_info(cls, object_candidates, container, global_info): in_volume = container.states[ContainedParticles].check_in_volume(obj_positions) | \ np.array([obj.states[OnTop].get_value(container) for obj in cls._OBJECTS]) + # Container itself is never within its own volume + in_volume[cls._OBJECTS_TO_IDX[container]] = False + return dict(in_volume=in_volume) @classmethod @@ -1115,6 +1151,7 @@ def refresh(cls, object_candidates): cls._ACTIVE_RECIPES = dict() cls._CATEGORY_IDXS = dict() cls._OBJECTS = [] + cls._OBJECTS_TO_IDX = dict() # Prune any recipes whose objects / system requirements are not met by the current set of objects / systems objects_by_category = og.sim.scene.object_registry.get_dict("category") @@ -1129,7 +1166,9 @@ def refresh(cls, object_candidates): for category, objects in objects_by_category.items(): cls._CATEGORY_IDXS[category] = i + np.arange(len(objects)) cls._OBJECTS += list(objects) - i += len(objects) + for obj in objects: + cls._OBJECTS_TO_IDX[obj] = i + i += 1 # Wrap relevant objects as numpy array so we can index into it efficiently cls._OBJECTS = np.array(cls._OBJECTS) @@ -1141,7 +1180,7 @@ def candidate_filters(cls): @classmethod def transition(cls, object_candidates): - t_results = TransitionResults() + objs_to_add, objs_to_remove = [], [] # Compute global info global_info = cls._compute_global_rule_info(object_candidates=object_candidates) @@ -1166,10 +1205,10 @@ def transition(cls, object_candidates): recipe_results = cls._execute_recipe( container=container, recipe=recipe, - in_volume=in_volume, + in_volume=container_info["in_volume"], ) - t_results.add += recipe_results.add - t_results.remove += recipe_results.remove + objs_to_add += recipe_results.add + objs_to_remove += recipe_results.remove break # Otherwise, if we didn't find a valid recipe, we execute a garbage transition instead if requested @@ -1186,12 +1225,12 @@ def transition(cls, object_candidates): output_objects=dict(), output_systems=[m.DEFAULT_GARBAGE_SYSTEM], ), - in_volume=in_volume, + in_volume=container_info["in_volume"], ) - t_results.add += garbage_results.add - t_results.remove += garbage_results.remove + objs_to_add += garbage_results.add + objs_to_remove += garbage_results.remove - return t_results + return TransitionResults(add=objs_to_add, remove=objs_to_remove) @classmethod def _execute_recipe(cls, container, recipe, in_volume): @@ -1210,35 +1249,34 @@ def _execute_recipe(cls, container, recipe, in_volume): Returns: TransitionResults: Results of the executed recipe transition """ - t_results = TransitionResults() + objs_to_add, objs_to_remove = [], [] # Compute total volume of all contained items volume = 0 # Remove all recipe system particles contained in the container - for system in PhysicalParticleSystem.get_active_systems(): + contained_particles_state = container.states[ContainedParticles] + for system in PhysicalParticleSystem.get_active_systems().values(): if container.states[Contains].get_value(system): - volume += contained_particles_state.get_value()[0] * np.pi * (system.particle_radius ** 3) * 4 / 3 - container.states[Filled].set_value(system, False) - for system in VisualParticleSystem.get_active_systems(): + volume += contained_particles_state.get_value(system).n_in_volume * np.pi * (system.particle_radius ** 3) * 4 / 3 + container.states[Contains].set_value(system, False) + for system in VisualParticleSystem.get_active_systems().values(): group_name = system.get_group_name(container) if group_name in system.groups and system.num_group_particles(group_name) > 0: system.remove_all_group_particles(group=group_name) # Remove either all objects or only the recipe-relevant objects inside the container - t_results.remove = np.concatenate([ - cls._OBJECTS[np.where(in_volume[cls._CATEGORY_IDXS[obj_category]])[0]] - for obj_category in recipe["input_objects"].keys() - ]).tolist() if cls.ignore_nonrecipe_objects else cls._OBJECTS[np.where(in_volume)[0]].tolist() + objs_to_remove.extend(np.concatenate([ + cls._OBJECTS[np.where(in_volume[cls._CATEGORY_IDXS[obj_category]])[0]] + for obj_category in recipe["input_objects"].keys() + ]) if cls.ignore_nonrecipe_objects else cls._OBJECTS[np.where(in_volume)[0]]) volume += sum(obj.volume for obj in objs_to_remove) # Define callback for spawning new objects inside container def _spawn_object_in_container(obj): - # We will either sample Inside or OnTop, based on the relative AABB extents of the container link - # vs. the object link - container_aabb = container.states[ContainedParticles].link.aabb_extent - obj_aabb = obj.aabb_extent - state = Inside if np.all(container_aabb > obj_aabb) else OnTop + # For simplicity sake, sample only OnTop + # TODO: Can we sample inside intelligently? + state = OnTop # TODO: What to do if setter fails? assert obj.states[state].set_value(container, True) @@ -1250,25 +1288,29 @@ def _spawn_object_in_container(obj): obj = DatasetObject( name=f"{category}_{n_category_objs + i}", category=category, - model=np.random.sample(models), + model=np.random.choice(models), ) new_obj_attrs = ObjectAttrs( obj=obj, callback=_spawn_object_in_container, + pos=np.ones(3) * (100.0 + i), ) - t_results.add.append(new_obj_attrs) + objs_to_add.append(new_obj_attrs) # Spawn in new fluid - out_system = get_system(output_system) - out_system.generate_particles_from_link( - obj=container, - link=contained_particles_state.link, - check_contact=cls.ignore_nonrecipe_objects, - max_samples=volume // (np.pi * (out_system.particle_radius ** 3) * 4 / 3), - ) + if len(recipe["output_systems"]) > 0: + # Only one system is allowed to be spawned + assert len(recipe["output_systems"]) == 1, "Only a single output system can be spawned for a given recipe!" + out_system = get_system(recipe["output_systems"][0]) + out_system.generate_particles_from_link( + obj=container, + link=contained_particles_state.link, + check_contact=cls.ignore_nonrecipe_objects, + max_samples=int(volume / (np.pi * (out_system.particle_radius ** 3) * 4 / 3)), + ) # Return transition results - return t_results + return TransitionResults(add=objs_to_add, remove=objs_to_remove) @classproperty def ignore_nonrecipe_objects(cls): @@ -1297,6 +1339,7 @@ def _do_not_register_classes(cls): return classes +# TODO: Make category-specific, e.g.: blender, coffee_maker, etc. class BlenderRule(RecipeRule): """ Transition mixing rule that leverages "blender" ability objects, which require toggledOn in order to trigger @@ -1387,7 +1430,7 @@ def refresh(cls, object_candidates): cls._HEAT_STEPS = dict() if cls._HEAT_STEPS is None else cls._HEAT_STEPS cls._LAST_HEAT_TIMESTEP = dict() if cls._LAST_HEAT_TIMESTEP is None else cls._LAST_HEAT_TIMESTEP - for name in cls._RECIPES.keys(): + for name in cls._ACTIVE_RECIPES.keys(): if name not in cls._HEAT_STEPS: cls._HEAT_STEPS[name] = 0 cls._LAST_HEAT_TIMESTEP[name] = -1 @@ -1603,10 +1646,23 @@ def modifies_filter_names(self): ] -# Create strawberry smoothie blender rule -BlenderRule.add_recipe( - name="strawberry_smoothie_recipe", - input_objects={"strawberry": 5, "ice_cube": 5}, - input_systems=["milk"], - output_system="strawberry_smoothie" -) +def import_recipes(): + # Wrap bddl here so it's only imported if necessary + import bddl + recipe_fpath = f"{os.path.dirname(bddl.__file__)}/generated_data/transition_rule_recipes.json" + if not os.path.exists(recipe_fpath): + log.warning(f"Cannot find recipe file at {recipe_fpath}. Skipping importing recipes.") + return + with open(recipe_fpath, "r") as f: + rule_recipes = json.load(f) + for rule_name, recipes in rule_recipes.items(): + rule = REGISTERED_RULES[rule_name] + for recipe in recipes: + rule.add_recipe(**recipe) + +# Optionally import bddl for rule recipes +try: + import_recipes() + +except ImportError: + log.warning("BDDL could not be imported - rule recipes will be unavailable.") diff --git a/omnigibson/utils/bddl_utils.py b/omnigibson/utils/bddl_utils.py index 6d2595d0c..c80f22b25 100644 --- a/omnigibson/utils/bddl_utils.py +++ b/omnigibson/utils/bddl_utils.py @@ -244,9 +244,9 @@ def get_state(self, state, *args, **kwargs): **kwargs (dict): Any keyword arguments to pass to getter, in order Returns: - None or any: Returned value(s) from @state if self.wrapped_obj exists (i.e.: not None), else None + any: Returned value(s) from @state if self.wrapped_obj exists (i.e.: not None), else False """ - return self.wrapped_obj.states[state].get_value(*args, **kwargs) if self.exists else None + return self.wrapped_obj.states[state].get_value(*args, **kwargs) if self.exists else False def set_state(self, state, *args, **kwargs): """ diff --git a/omnigibson/utils/geometry_utils.py b/omnigibson/utils/geometry_utils.py index d716262df..161dbea38 100644 --- a/omnigibson/utils/geometry_utils.py +++ b/omnigibson/utils/geometry_utils.py @@ -296,30 +296,19 @@ def generate_points_in_volume_checker_function(obj, volume_link, use_visual_mesh where @vol is the total volume being checked (expressed in global scale) aggregated across all container sub-volumes """ - # If the object doesn't uniform scale, we make sure the volume link has no relative orientation w.r.t to - # the object (root link) frame - # TODO: Can we remove this restriction in the future? The current paradigm of how scale operates makes this difficult - if (obj.scale.max() - obj.scale.min()) > 1e-3: - volume_link_quat = volume_link.get_orientation() - object_quat = obj.get_orientation() - quat_distance = T.quat_distance(volume_link_quat, object_quat) - assert np.isclose(quat_distance[3], 1, atol=1e-3), \ - f"Volume link must have no relative orientation w.r.t the root link! (i.e.: quat distance [0, 0, 0, 1])! " \ - f"Got quat distance: {quat_distance}" # Iterate through all visual meshes and keep track of any that are prefixed with container container_meshes = [] meshes = volume_link.visual_meshes if use_visual_meshes else volume_link.collision_meshes for container_mesh_name, container_mesh in meshes.items(): if mesh_name_prefixes is None or mesh_name_prefixes in container_mesh_name: - container_meshes.append(container_mesh.prim) + container_meshes.append(container_mesh) # Programmatically define the volume checker functions based on each container found volume_checker_fcns = [] - volume_calc_fcns = [] for sub_container_mesh in container_meshes: - mesh_type = sub_container_mesh.GetTypeName() + mesh_type = sub_container_mesh.prim.GetTypeName() if mesh_type == "Mesh": - fcn, vol_fcn = _generate_convex_hull_volume_checker_functions(convex_hull_mesh=sub_container_mesh) + fcn, vol_fcn = _generate_convex_hull_volume_checker_functions(convex_hull_mesh=sub_container_mesh.prim) elif mesh_type == "Sphere": fcn = lambda mesh, particle_positions: check_points_in_sphere( size=mesh.GetAttribute("radius").Get(), @@ -328,7 +317,6 @@ def generate_points_in_volume_checker_function(obj, volume_link, use_visual_mesh scale=np.array(mesh.GetAttribute("xformOp:scale").Get()), particle_positions=particle_positions, ) - vol_fcn = lambda mesh: 4 / 3 * np.pi * (mesh.GetAttribute("radius").Get() ** 3) elif mesh_type == "Cylinder": fcn = lambda mesh, particle_positions: check_points_in_cylinder( size=[mesh.GetAttribute("radius").Get(), mesh.GetAttribute("height").Get()], @@ -337,7 +325,6 @@ def generate_points_in_volume_checker_function(obj, volume_link, use_visual_mesh scale=np.array(mesh.GetAttribute("xformOp:scale").Get()), particle_positions=particle_positions, ) - vol_fcn = lambda mesh: np.pi * (mesh.GetAttribute("radius").Get() ** 2) * mesh.GetAttribute("height").Get() elif mesh_type == "Cone": fcn = lambda mesh, particle_positions: check_points_in_cone( size=[mesh.GetAttribute("radius").Get(), mesh.GetAttribute("height").Get()], @@ -346,7 +333,6 @@ def generate_points_in_volume_checker_function(obj, volume_link, use_visual_mesh scale=np.array(mesh.GetAttribute("xformOp:scale").Get()), particle_positions=particle_positions, ) - vol_fcn = lambda mesh: np.pi * (mesh.GetAttribute("radius").Get() ** 2) * mesh.GetAttribute("height").Get() / 3.0 elif mesh_type == "Cube": fcn = lambda mesh, particle_positions: check_points_in_cube( size=mesh.GetAttribute("size").Get(), @@ -355,12 +341,10 @@ def generate_points_in_volume_checker_function(obj, volume_link, use_visual_mesh scale=np.array(mesh.GetAttribute("xformOp:scale").Get()), particle_positions=particle_positions, ) - vol_fcn = lambda mesh: mesh.GetAttribute("size").Get() ** 3 else: raise ValueError(f"Cannot create volume checker function for mesh of type: {mesh_type}") volume_checker_fcns.append(fcn) - volume_calc_fcns.append(vol_fcn) # Define the actual volume checker function def check_points_in_volumes(particle_positions): @@ -383,19 +367,40 @@ def check_points_in_volumes(particle_positions): in_volumes = np.zeros(n_particles).astype(bool) for checker_fcn, mesh in zip(volume_checker_fcns, container_meshes): - in_volumes |= checker_fcn(mesh, particle_positions) + in_volumes |= checker_fcn(mesh.prim, particle_positions) return in_volumes # Define the actual volume calculator function - def calculate_volume(): - # Aggregate values across all subvolumes - # NOTE: Assumes all volumes are strictly disjointed (becuase we sum over all subvolumes to calculate - # total raw volume) - # TODO: Is there a way we can explicitly check if disjointed? - vols = [calc_fcn(mesh) * np.product(mesh.GetAttribute("xformOp:scale").Get()) - for calc_fcn, mesh in zip(volume_calc_fcns, container_meshes)] - # Aggregate over all volumes and scale by the link's global scale - return np.sum(vols) * np.product(volume_link.get_world_scale()) + def calculate_volume(precision=1e-5): + # We use monte-carlo sampling to approximate the voluem up to @precision + # NOTE: precision defines the RELATIVE precision of the volume computation -- i.e.: the relative error with + # respect to the volume link's global AABB + + # Convert precision to minimum number of particles to sample + min_n_particles = int(np.ceil(1. / precision)) + + # Make sure container meshes are visible so AABB computation is correct + for mesh in container_meshes: + mesh.visible = True + + # Determine equally-spaced sampling distance to achieve this minimum particle count + aabb_volume = np.product(volume_link.aabb_extent) + sampling_distance = np.cbrt(aabb_volume / min_n_particles) + low, high = volume_link.aabb + n_particles_per_axis = ((high - low) / sampling_distance).astype(int) + 1 + assert np.all(n_particles_per_axis), "Must increase precision for calculate_volume -- too coarse for sampling!" + # 1e-10 is added because the extent might be an exact multiple of particle radius + arrs = [np.arange(lo, hi, sampling_distance) + for lo, hi, n in zip(low, high, n_particles_per_axis)] + # Generate 3D-rectangular grid of points, and only keep the ones inside the mesh + points = np.stack([arr.flatten() for arr in np.meshgrid(*arrs)]).T + + # Re-hide container meshes + for mesh in container_meshes: + mesh.visible = False + + # Return the fraction of the link AABB's volume based on fraction of points enclosed within it + return aabb_volume * np.mean(check_points_in_volumes(points)) return check_points_in_volumes, calculate_volume diff --git a/omnigibson/utils/object_state_utils.py b/omnigibson/utils/object_state_utils.py index 18e1812de..0a19f5243 100644 --- a/omnigibson/utils/object_state_utils.py +++ b/omnigibson/utils/object_state_utils.py @@ -18,7 +18,8 @@ # Create settings for this module m = create_module_macros(module_path=__file__) -m.DEFAULT_SAMPLING_ATTEMPTS = 100 +m.DEFAULT_HIGH_LEVEL_SAMPLING_ATTEMPTS = 10 +m.DEFAULT_LOW_LEVEL_SAMPLING_ATTEMPTS = 10 m.ON_TOP_RAY_CASTING_SAMPLING_PARAMS = Dict({ "bimodal_stdev_fraction": 1e-6, "bimodal_mean_fraction": 1.0, @@ -45,7 +46,7 @@ def sample_kinematics( predicate, objA, objB, - max_trials=m.DEFAULT_SAMPLING_ATTEMPTS, + max_trials=m.DEFAULT_LOW_LEVEL_SAMPLING_ATTEMPTS, z_offset=0.05, skip_falling=False, ): diff --git a/omnigibson/utils/registry_utils.py b/omnigibson/utils/registry_utils.py index ce8370fa3..2a9dc154b 100644 --- a/omnigibson/utils/registry_utils.py +++ b/omnigibson/utils/registry_utils.py @@ -139,9 +139,7 @@ def _add(self, obj, keys=None): if attr in mapping: log.warning(f"Instance identifier '{k}' should be unique for adding to this registry mapping! Existing {k}: {attr}") # Special case for "name" attribute, which should ALWAYS be unique - if k == "name": - log.error(f"For name attribute, objects MUST be unique. Exiting.") - exit(-1) + assert k != "name", "For name attribute, objects MUST be unique." mapping[attr] = obj else: # Not unique case diff --git a/omnigibson/utils/usd_utils.py b/omnigibson/utils/usd_utils.py index 7e51d9e81..d20512c14 100644 --- a/omnigibson/utils/usd_utils.py +++ b/omnigibson/utils/usd_utils.py @@ -505,7 +505,7 @@ def _compute_non_flatcache_aabb(cls, prim_path): # Sanity check values if np.any(aabb[3:] < aabb[:3]): - raise ValueError(f"Got invalid aabb values: low={aabb[:3]}, high={aabb[3:]}") + raise ValueError(f"Got invalid aabb values for prim: {prim_path}: low={aabb[:3]}, high={aabb[3:]}") return aabb[:3], aabb[3:] diff --git a/setup.py b/setup.py index d9d943b2e..123ce37e2 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name="omnigibson", - version="0.2.0", + version="0.2.1", author="Stanford University", long_description_content_type="text/markdown", long_description=long_description, diff --git a/tests/test_object_states.py b/tests/test_object_states.py index 54c808305..0772b9e61 100644 --- a/tests/test_object_states.py +++ b/tests/test_object_states.py @@ -758,7 +758,7 @@ def test_particle_source(): # Cannot set this state with pytest.raises(NotImplementedError): - sink.states[ParticleSource].set_value(water_system, True) + sink.states[ParticleSource].set_value(True) @og_test @@ -785,7 +785,7 @@ def test_particle_sink(): # Cannot set this state with pytest.raises(NotImplementedError): - sink.states[ParticleSink].set_value(water_system, True) + sink.states[ParticleSink].set_value(True) @og_test @@ -844,7 +844,7 @@ def test_particle_applier(): # Cannot set this state with pytest.raises(NotImplementedError): - spray_bottle.states[ParticleApplier].set_value(water_system, True) + spray_bottle.states[ParticleApplier].set_value(True) @og_test @@ -903,7 +903,7 @@ def test_particle_remover(): # Cannot set this state with pytest.raises(NotImplementedError): - vacuum.states[ParticleRemover].set_value(water_system, True) + vacuum.states[ParticleRemover].set_value(True) @og_test @@ -1071,7 +1071,11 @@ def test_filled(): og.sim.step() assert stockpot.states[Filled].get_value(system) - stockpot.states[Filled].set_value(system, False) + + # Cannot set Filled state False + with pytest.raises(NotImplementedError): + stockpot.states[Filled].set_value(system, False) + system.remove_all_particles() for _ in range(5): og.sim.step() @@ -1088,7 +1092,7 @@ def test_contains(): get_system("water"), get_system("stain"), get_system("raspberry"), - get_system("diced_apple"), + get_system("diced__apple"), ) for system in systems: stockpot.set_position_orientation(position=np.ones(3) * 50.0, orientation=[0, 0, 0, 1.0]) @@ -1115,7 +1119,7 @@ def test_contains(): assert stockpot.states[Contains].get_value(system) # Remove all particles and make sure contains returns False - system.remove_all_particles() + stockpot.states[Contains].set_value(system, False) og.sim.step() assert not stockpot.states[Contains].get_value(system) @@ -1134,7 +1138,7 @@ def test_covered(): get_system("water"), get_system("stain"), get_system("raspberry"), - get_system("diced_apple"), + get_system("diced__apple"), ) for obj in (bracelet, oyster, breakfast_table): for system in systems: diff --git a/tests/test_transition_rules.py b/tests/test_transition_rules.py new file mode 100644 index 000000000..a6269a3ef --- /dev/null +++ b/tests/test_transition_rules.py @@ -0,0 +1,101 @@ +from omnigibson.macros import macros as m +from omnigibson.object_states import * +from omnigibson.systems import get_system, is_physical_particle_system, is_visual_particle_system +from omnigibson.utils.constants import PrimType +from omnigibson.utils.physx_utils import apply_force_at_pos, apply_torque +import omnigibson.utils.transform_utils as T +from omnigibson.utils.usd_utils import BoundingBoxAPI +from omnigibson.objects import DatasetObject +import omnigibson as og + +from utils import og_test, get_random_pose, place_objA_on_objB_bbox, place_obj_on_floor_plane + +import pytest +import numpy as np + + +@og_test +def test_blender_rule(): + blender = og.sim.scene.object_registry("name", "blender") + + blender.set_orientation([0, 0, 0, 1]) + place_obj_on_floor_plane(blender) + og.sim.step() + + milk = get_system("whole_milk") + chocolate_sauce = get_system("chocolate_sauce") + milkshake = get_system("milkshake") + milk.generate_particles(positions=np.array([[0, 0.02, 0.47]])) + chocolate_sauce.generate_particles(positions=np.array([[0, -0.02, 0.47]])) + + ice_cream = DatasetObject( + name="ice_cream", + category="scoop_of_ice_cream", + model="dodndj", + bounding_box=[0.076, 0.077, 0.065], + ) + og.sim.import_object(ice_cream) + ice_cream.set_position([0, 0, 0.52]) + + for i in range(5): + og.sim.step() + + assert milkshake.n_particles == 0 + + blender.states[ToggledOn].set_value(True) + og.sim.step() + + assert milk.n_particles == 0 + assert chocolate_sauce.n_particles == 0 + assert milkshake.n_particles > 0 + + # Remove objects and systems from recipe output + milkshake.remove_all_particles() + if og.sim.scene.object_registry("name", "ice_cream") is not None: + og.sim.remove_object(obj=ice_cream) + + +@og_test +def test_cooking_rule(): + oven = og.sim.scene.object_registry("name", "oven") + oven.keep_still() + oven.set_orientation([0, 0, -0.707, 0.707]) + place_obj_on_floor_plane(oven) + og.sim.step() + + sheet = DatasetObject( + name="sheet", + category="baking_sheet", + model="yhurut", + bounding_box=[0.520, 0.312, 0.0395], + ) + + og.sim.import_object(sheet) + sheet.set_position_orientation([0.072, 0.004, 0.455], [0, 0, 0, 1]) + + dough = DatasetObject( + name="dough", + category="sugar_cookie_dough", + model="qewbbb", + bounding_box=[0.200, 0.192, 0.0957], + ) + og.sim.import_object(dough) + dough.set_position_orientation([0.072, 0.004, 0.555], [0, 0, 0, 1]) + + for i in range(10): + og.sim.step() + + assert len(og.sim.scene.object_registry("category", "sugar_cookie", default_val=[])) == 0 + + oven.states[ToggledOn].set_value(True) + og.sim.step() + og.sim.step() + + dough_exists = og.sim.scene.object_registry("name", "dough") is not None + assert not dough_exists or not dough.states[OnTop].get_value(sheet) + assert len(og.sim.scene.object_registry("category", "sugar_cookie")) > 0 + + # Remove objects + if dough_exists: + og.sim.remove_object(dough) + og.sim.remove_object(sheet) diff --git a/tests/utils.py b/tests/utils.py index 82584dd9e..eecc55611 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -20,18 +20,19 @@ def wrapper(): num_objs = 0 -def get_obj_cfg(name, category, model, prim_type=PrimType.RIGID, scale=None, abilities=None, visual_only=False): +def get_obj_cfg(name, category, model, prim_type=PrimType.RIGID, scale=None, bounding_box=None, abilities=None, visual_only=False): global num_objs num_objs += 1 return { "type": "DatasetObject", - "fit_avg_dim_volume": scale is None, + "fit_avg_dim_volume": scale is None and bounding_box is None, "name": name, "category": category, "model": model, "prim_type": prim_type, - "position": [150, 150, num_objs * 5], + "position": [150, 150, 150 + num_objs * 5], "scale": scale, + "bounding_box": bounding_box, "abilities": abilities, "visual_only": visual_only, } @@ -65,11 +66,15 @@ def assert_test_scene(): get_obj_cfg("remover_dishtowel", "dishtowel", "dtfspn", abilities={"particleRemover": {"method": ParticleModifyMethod.ADJACENCY, "conditions": {"water": []}}}), get_obj_cfg("spray_bottle", "spray_bottle", "asztxi", visual_only=True, abilities={"toggleable": {}, "particleApplier": {"method": ParticleModifyMethod.PROJECTION, "conditions": {"water": [(ParticleModifyCondition.TOGGLEDON, True)]}}}), get_obj_cfg("vacuum", "vacuum", "bdmsbr", visual_only=True, abilities={"toggleable": {}, "particleRemover": {"method": ParticleModifyMethod.PROJECTION, "conditions": {"water": [(ParticleModifyCondition.TOGGLEDON, True)]}}}), + get_obj_cfg("blender", "blender", "cwkvib", bounding_box=[0.316, 0.318, 0.649], abilities={"fillable": {}, "blender": {}, "toggleable": {}}), + get_obj_cfg("oven", "oven", "cgtaer", bounding_box=[0.943, 0.837, 1.297]), ], "robots": [ { "type": "Fetch", "obs_modalities": [], + "position": [150, 150, 100], + "orientation": [0, 0, 0, 1], } ] } @@ -85,10 +90,6 @@ def assert_test_scene(): # Create the environment env = og.Environment(configs=cfg, action_timestep=1 / 60., physics_timestep=1 / 60.) - env.robots[0].set_position_orientation([150, 150, 0], [0, 0, 0, 1]) - og.sim.step() - og.sim.scene.update_initial_state() - def get_random_pose(pos_low=10.0, pos_hi=20.0): pos = np.random.uniform(pos_low, pos_hi, 3)