Skip to content

system_base

BaseSystem

Bases: SerializableNonInstance, UniquelyNamedNonInstance

Base class for all systems. These are non-instance objects that should be used globally for a given environment. This is useful for items in a scene that are non-discrete / cannot be distinguished into individual instances, e.g.: water, particles, etc. While we keep the python convention of the system class name being camel case (e.g. StrawberrySmoothie), we adopt the snake case for the system registry to unify with the category of BaseObject. For example, get_system("strawberry_smoothie") will return the StrawberrySmoothie class.

Source code in omnigibson/systems/system_base.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
class BaseSystem(SerializableNonInstance, UniquelyNamedNonInstance):
    """
    Base class for all systems. These are non-instance objects that should be used globally for a given environment.
    This is useful for items in a scene that are non-discrete / cannot be distinguished into individual instances,
    e.g.: water, particles, etc. While we keep the python convention of the system class name being camel case
    (e.g. StrawberrySmoothie), we adopt the snake case for the system registry to unify with the category of BaseObject.
    For example, get_system("strawberry_smoothie") will return the StrawberrySmoothie class.
    """
    # Scaling factor to sample from when generating a new particle
    min_scale = None              # (x,y,z) scaling
    max_scale = None              # (x,y,z) scaling

    # Whether this system has been initialized or not
    initialized = False

    # Internal variables used for bookkeeping
    _uuid = None
    _snake_case_name = None

    def __init_subclass__(cls, **kwargs):
        # While class names are camel case, we convert them to snake case to be consistent with object categories.
        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)

        # Run super init
        super().__init_subclass__(**kwargs)

        # Register this system if requested
        if cls._register_system:
            global REGISTERED_SYSTEMS, UUID_TO_SYSTEMS
            REGISTERED_SYSTEMS[cls._snake_case_name] = cls
            cls._uuid = get_uuid(cls._snake_case_name)
            UUID_TO_SYSTEMS[cls._uuid] = cls

    @classproperty
    def name(cls):
        # Class name is the unique name assigned
        return cls._snake_case_name

    @classproperty
    def uuid(cls):
        return cls._uuid

    @classproperty
    def prim_path(cls):
        """
        Returns:
            str: Path to this system's prim in the scene stage
        """
        return f"/World/{cls.name}"

    @classproperty
    def n_particles(cls):
        """
        Returns:
            int: Number of particles belonging to this system
        """
        raise NotImplementedError()

    @classproperty
    def material(cls):
        """
        Returns:
            None or MaterialPrim: Material belonging to this system, if there is any
        """
        return None

    @classproperty
    def _register_system(cls):
        """
        Returns:
            bool: True if this system should be registered (i.e.: it is not an intermediate class but a "final" subclass
                representing a system we'd actually like to use, e.g.: water, dust, etc. Should be set by the subclass
        """
        # We assume we aren't registering by default
        return False

    @classproperty
    def _store_local_poses(cls):
        """
        Returns:
            bool: Whether to store local particle poses or not when state is saved. Default is False
        """
        return False

    @classmethod
    def initialize(cls):
        """
        Initializes this system
        """
        global _CALLBACKS_ON_SYSTEM_INIT

        assert not cls.initialized, f"Already initialized system {cls.name}!"
        og.sim.stage.DefinePrim(cls.prim_path, "Scope")

        cls.initialized = True

        # Add to registry
        SYSTEM_REGISTRY.add(obj=cls)

        # Avoid circular import
        if og.sim.is_playing():
            from omnigibson.transition_rules import TransitionRuleAPI
            TransitionRuleAPI.refresh_all_rules()

        # Run any callbacks
        for callback in _CALLBACKS_ON_SYSTEM_INIT.values():
            callback(cls)

    @classmethod
    def update(cls):
        """
        Executes any necessary system updates, once per og.sim._non_physics_step
        """
        # Default is no-op
        pass

    @classmethod
    def remove_all_particles(cls):
        """
        Removes all particles and deletes them from the simulator
        """
        raise NotImplementedError()

    @classmethod
    def remove_particles(
            cls,
            idxs,
            **kwargs,
    ):
        """
        Removes pre-existing particles

        Args:
            idxs (np.array): (n_particles,) shaped array specifying IDs of particles to delete
            **kwargs (dict): Any additional keyword-specific arguments required by subclass implementation
        """
        raise NotImplementedError()

    @classmethod
    def generate_particles(
            cls,
            positions,
            orientations=None,
            scales=None,
            **kwargs,
    ):
        """
        Generates new particles

        Args:
            positions (np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) positions
            orientations (None or np.array): (n_particles, 4) shaped array specifying per-particle (x,y,z,w) quaternion
                orientations. If not specified, all will be set to canonical orientation (0, 0, 0, 1)
            scales (None or np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) scales.
                If not specified, will be uniformly randomly sampled from (cls.min_scale, cls.max_scale)
            **kwargs (dict): Any additional keyword-specific arguments required by subclass implementation
        """
        raise NotImplementedError()

    @classmethod
    def clear(cls):
        """
        Clears this system, so that it may possibly be re-initialized. Useful for, e.g., when loading from a new
        scene during the same sim instance
        """
        if cls.initialized:
            cls._clear()

    @classmethod
    def _clear(cls):
        global SYSTEM_REGISTRY, _CALLBACKS_ON_SYSTEM_CLEAR
        # Run any callbacks
        for callback in _CALLBACKS_ON_SYSTEM_CLEAR.values():
            callback(cls)

        cls.reset()
        lazy.omni.isaac.core.utils.prims.delete_prim(cls.prim_path)
        cls.initialized = False

        # Remove from active registry
        SYSTEM_REGISTRY.remove(obj=cls)

        # Avoid circular import
        if og.sim.is_playing():
            from omnigibson.transition_rules import TransitionRuleAPI
            TransitionRuleAPI.refresh_all_rules()

    @classmethod
    def reset(cls):
        """
        Reset this system
        """
        cls.remove_all_particles()

    @classmethod
    def create(cls, name, min_scale=None, max_scale=None, **kwargs):
        """
        Helper function to programmatically generate systems

        Args:
            name (str): Name of the visual particles, in snake case.
            min_scale (None or 3-array): If specified, sets the minumum bound for particles' relative scale.
                Else, defaults to 1
            max_scale (None or 3-array): If specified, sets the maximum bound for particles' relative scale.
                Else, defaults to 1
            **kwargs (any): keyword-mapped parameters to override / set in the child class, where the keys represent
                the class attribute to modify and the values represent the functions / value to set
                (Note: These values should have either @classproperty or @classmethod decorators!)

        Returns:
            BaseSystem: Generated system class given input arguments
        """
        @classmethod
        def cm_initialize(cls):
            # Potentially override the min / max scales
            if min_scale is not None:
                cls.min_scale = np.array(min_scale)
            if max_scale is not None:
                cls.max_scale = np.array(max_scale)

            # Run super (we have to use a bit esoteric syntax in order to accommodate this procedural method for
            # using super calls -- cf. https://stackoverflow.com/questions/22403897/what-does-it-mean-by-the-super-object-returned-is-unbound-in-python
            super(cls).__get__(cls).initialize()

        kwargs["initialize"] = cm_initialize

        # Create and return the class
        return subclass_factory(name=snake_case_to_camel_case(name), base_classes=cls, **kwargs)

    @classmethod
    def get_active_systems(cls):
        """
        Returns:
            dict: Mapping from system name to system for all systems that are subclasses of this system AND active (initialized)
        """
        return {system.name: system for system in SYSTEM_REGISTRY.objects if issubclass(system, cls)}

    @classmethod
    def sample_scales(cls, n):
        """
        Samples scales uniformly based on @cls.min_scale and @cls.max_scale

        Args:
            n (int): Number of scales to sample

        Returns:
            (n, 3) array: Array of sampled scales
        """
        return np.random.uniform(cls.min_scale, cls.max_scale, (n, 3))

    @classmethod
    def get_particles_position_orientation(cls):
        """
        Computes all particles' positions and orientations that belong to this system in the world frame

        Note: This is more optimized than doing a for loop with self.get_particle_position_orientation()

        Returns:
            2-tuple:
                - (n, 3)-array: per-particle (x,y,z) position
                - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError()

    @classmethod
    def get_particle_position_orientation(cls, idx):
        """
        Compute particle's position and orientation. This automatically takes into account the relative
        pose w.r.t. its parent link and the global pose of that parent link.

        Args:
            idx (int): Index of the particle to compute position and orientation for. Note: this is
                equivalent to grabbing the corresponding idx'th entry from @get_particles_position_orientation()

        Returns:
            2-tuple:
                - 3-array: (x,y,z) position
                - 4-array: (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError()

    @classmethod
    def set_particles_position_orientation(cls, positions=None, orientations=None):
        """
        Sets all particles' positions and orientations that belong to this system in the world frame

        Note: This is more optimized than doing a for loop with self.set_particle_position_orientation()

        Args:
            positions (n-array): (n, 3) per-particle (x,y,z) position
            orientations (n-array): (n, 4) per-particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError()

    @classmethod
    def set_particle_position_orientation(cls, idx, position=None, orientation=None):
        """
        Sets particle's position and orientation. This automatically takes into account the relative
        pose w.r.t. its parent link and the global pose of that parent link.

        Args:
            idx (int): Index of the particle to set position and orientation for. Note: this is
                equivalent to setting the corresponding idx'th entry from @set_particles_position_orientation()
            position (3-array): particle (x,y,z) position
            orientation (4-array): particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError()

    @classmethod
    def get_particles_local_pose(cls):
        """
        Computes all particles' positions and orientations that belong to this system in the particles' parent frames

        Returns:
            2-tuple:
                - (n, 3)-array: per-particle (x,y,z) position
                - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError()

    @classmethod
    def get_particle_local_pose(cls, idx):
        """
        Compute particle's position and orientation in the particle's parent frame

        Args:
            idx (int): Index of the particle to compute position and orientation for. Note: this is
                equivalent to grabbing the corresponding idx'th entry from @get_particles_local_pose()

        Returns:
            2-tuple:
                - 3-array: (x,y,z) position
                - 4-array: (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError()

    @classmethod
    def set_particles_local_pose(cls, positions=None, orientations=None):
        """
        Sets all particles' positions and orientations that belong to this system in the particles' parent frames

        Args:
            positions (n-array): (n, 3) per-particle (x,y,z) position
            orientations (n-array): (n, 4) per-particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError()

    @classmethod
    def set_particle_local_pose(cls, idx, position=None, orientation=None):
        """
        Sets particle's position and orientation in the particle's parent frame

        Args:
            idx (int): Index of the particle to set position and orientation for. Note: this is
                equivalent to setting the corresponding idx'th entry from @set_particles_local_pose()
            position (3-array): particle (x,y,z) position
            orientation (4-array): particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError()

    def __init__(self):
        raise ValueError("System classes should not be created!")

    @classproperty
    def state_size(cls):
        # We have n_particles (1), min / max scale (3*2), each particle pose (7*n)
        return 7 + 7 * cls.n_particles

    @classmethod
    def _dump_state(cls):
        positions, orientations = cls.get_particles_local_pose() if \
            cls._store_local_poses else cls.get_particles_position_orientation()
        return dict(
            n_particles=cls.n_particles,
            min_scale=cls.min_scale,
            max_scale=cls.max_scale,
            positions=positions,
            orientations=orientations,
        )

    @classmethod
    def _load_state(cls, state):
        # Sanity check loading particles
        assert cls.n_particles == state["n_particles"], f"Inconsistent number of particles found when loading " \
                                                        f"particles state! Current number: {cls.n_particles}, " \
                                                        f"loaded number: {state['n_particles']}"
        # Load scale
        cls.min_scale = state["min_scale"]
        cls.max_scale = state["max_scale"]

        # Load the poses
        setter = cls.set_particles_local_pose if cls._store_local_poses else cls.set_particles_position_orientation
        setter(positions=state["positions"], orientations=state["orientations"])

    @classmethod
    def _serialize(cls, state):
        # Array is n_particles, then min_scale and max_scale, then poses for all particles
        return np.concatenate([
            [state["n_particles"]],
            state["min_scale"],
            state["max_scale"],
            state["positions"].flatten(),
            state["orientations"].flatten(),
        ], dtype=float)

    @classmethod
    def _deserialize(cls, state):
        # First index is number of particles, then min_scale and max_scale, then the individual particle poses
        state_dict = dict()
        n_particles = int(state[0])
        len_positions = n_particles * 3
        len_orientations = n_particles * 4
        state_dict["n_particles"] = n_particles
        state_dict["min_scale"] = state[1:4]
        state_dict["max_scale"] = state[4:7]
        state_dict["positions"] = state[7:7+len_positions].reshape(-1, 3)
        state_dict["orientations"] = state[7+len_positions:7+len_positions+len_orientations].reshape(-1, 4)

        return state_dict, 7 + len_positions + len_orientations

clear() classmethod

Clears this system, so that it may possibly be re-initialized. Useful for, e.g., when loading from a new scene during the same sim instance

Source code in omnigibson/systems/system_base.py
@classmethod
def clear(cls):
    """
    Clears this system, so that it may possibly be re-initialized. Useful for, e.g., when loading from a new
    scene during the same sim instance
    """
    if cls.initialized:
        cls._clear()

create(name, min_scale=None, max_scale=None, **kwargs) classmethod

Helper function to programmatically generate systems

Parameters:

Name Type Description Default
name str

Name of the visual particles, in snake case.

required
min_scale None or 3 - array

If specified, sets the minumum bound for particles' relative scale. Else, defaults to 1

None
max_scale None or 3 - array

If specified, sets the maximum bound for particles' relative scale. Else, defaults to 1

None
**kwargs any

keyword-mapped parameters to override / set in the child class, where the keys represent the class attribute to modify and the values represent the functions / value to set (Note: These values should have either @classproperty or @classmethod decorators!)

{}

Returns:

Name Type Description
BaseSystem

Generated system class given input arguments

Source code in omnigibson/systems/system_base.py
@classmethod
def create(cls, name, min_scale=None, max_scale=None, **kwargs):
    """
    Helper function to programmatically generate systems

    Args:
        name (str): Name of the visual particles, in snake case.
        min_scale (None or 3-array): If specified, sets the minumum bound for particles' relative scale.
            Else, defaults to 1
        max_scale (None or 3-array): If specified, sets the maximum bound for particles' relative scale.
            Else, defaults to 1
        **kwargs (any): keyword-mapped parameters to override / set in the child class, where the keys represent
            the class attribute to modify and the values represent the functions / value to set
            (Note: These values should have either @classproperty or @classmethod decorators!)

    Returns:
        BaseSystem: Generated system class given input arguments
    """
    @classmethod
    def cm_initialize(cls):
        # Potentially override the min / max scales
        if min_scale is not None:
            cls.min_scale = np.array(min_scale)
        if max_scale is not None:
            cls.max_scale = np.array(max_scale)

        # Run super (we have to use a bit esoteric syntax in order to accommodate this procedural method for
        # using super calls -- cf. https://stackoverflow.com/questions/22403897/what-does-it-mean-by-the-super-object-returned-is-unbound-in-python
        super(cls).__get__(cls).initialize()

    kwargs["initialize"] = cm_initialize

    # Create and return the class
    return subclass_factory(name=snake_case_to_camel_case(name), base_classes=cls, **kwargs)

generate_particles(positions, orientations=None, scales=None, **kwargs) classmethod

Generates new particles

Parameters:

Name Type Description Default
positions array

(n_particles, 3) shaped array specifying per-particle (x,y,z) positions

required
orientations None or array

(n_particles, 4) shaped array specifying per-particle (x,y,z,w) quaternion orientations. If not specified, all will be set to canonical orientation (0, 0, 0, 1)

None
scales None or array

(n_particles, 3) shaped array specifying per-particle (x,y,z) scales. If not specified, will be uniformly randomly sampled from (cls.min_scale, cls.max_scale)

None
**kwargs dict

Any additional keyword-specific arguments required by subclass implementation

{}
Source code in omnigibson/systems/system_base.py
@classmethod
def generate_particles(
        cls,
        positions,
        orientations=None,
        scales=None,
        **kwargs,
):
    """
    Generates new particles

    Args:
        positions (np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) positions
        orientations (None or np.array): (n_particles, 4) shaped array specifying per-particle (x,y,z,w) quaternion
            orientations. If not specified, all will be set to canonical orientation (0, 0, 0, 1)
        scales (None or np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) scales.
            If not specified, will be uniformly randomly sampled from (cls.min_scale, cls.max_scale)
        **kwargs (dict): Any additional keyword-specific arguments required by subclass implementation
    """
    raise NotImplementedError()

get_active_systems() classmethod

Returns:

Name Type Description
dict

Mapping from system name to system for all systems that are subclasses of this system AND active (initialized)

Source code in omnigibson/systems/system_base.py
@classmethod
def get_active_systems(cls):
    """
    Returns:
        dict: Mapping from system name to system for all systems that are subclasses of this system AND active (initialized)
    """
    return {system.name: system for system in SYSTEM_REGISTRY.objects if issubclass(system, cls)}

get_particle_local_pose(idx) classmethod

Compute particle's position and orientation in the particle's parent frame

Parameters:

Name Type Description Default
idx int

Index of the particle to compute position and orientation for. Note: this is equivalent to grabbing the corresponding idx'th entry from @get_particles_local_pose()

required

Returns:

Type Description

2-tuple: - 3-array: (x,y,z) position - 4-array: (x,y,z,w) quaternion orientation

Source code in omnigibson/systems/system_base.py
@classmethod
def get_particle_local_pose(cls, idx):
    """
    Compute particle's position and orientation in the particle's parent frame

    Args:
        idx (int): Index of the particle to compute position and orientation for. Note: this is
            equivalent to grabbing the corresponding idx'th entry from @get_particles_local_pose()

    Returns:
        2-tuple:
            - 3-array: (x,y,z) position
            - 4-array: (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError()

get_particle_position_orientation(idx) classmethod

Compute particle's position and orientation. This automatically takes into account the relative pose w.r.t. its parent link and the global pose of that parent link.

Parameters:

Name Type Description Default
idx int

Index of the particle to compute position and orientation for. Note: this is equivalent to grabbing the corresponding idx'th entry from @get_particles_position_orientation()

required

Returns:

Type Description

2-tuple: - 3-array: (x,y,z) position - 4-array: (x,y,z,w) quaternion orientation

Source code in omnigibson/systems/system_base.py
@classmethod
def get_particle_position_orientation(cls, idx):
    """
    Compute particle's position and orientation. This automatically takes into account the relative
    pose w.r.t. its parent link and the global pose of that parent link.

    Args:
        idx (int): Index of the particle to compute position and orientation for. Note: this is
            equivalent to grabbing the corresponding idx'th entry from @get_particles_position_orientation()

    Returns:
        2-tuple:
            - 3-array: (x,y,z) position
            - 4-array: (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError()

get_particles_local_pose() classmethod

Computes all particles' positions and orientations that belong to this system in the particles' parent frames

Returns:

Type Description

2-tuple: - (n, 3)-array: per-particle (x,y,z) position - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation

Source code in omnigibson/systems/system_base.py
@classmethod
def get_particles_local_pose(cls):
    """
    Computes all particles' positions and orientations that belong to this system in the particles' parent frames

    Returns:
        2-tuple:
            - (n, 3)-array: per-particle (x,y,z) position
            - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError()

get_particles_position_orientation() classmethod

Computes all particles' positions and orientations that belong to this system in the world frame

Note: This is more optimized than doing a for loop with self.get_particle_position_orientation()

Returns:

Type Description

2-tuple: - (n, 3)-array: per-particle (x,y,z) position - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation

Source code in omnigibson/systems/system_base.py
@classmethod
def get_particles_position_orientation(cls):
    """
    Computes all particles' positions and orientations that belong to this system in the world frame

    Note: This is more optimized than doing a for loop with self.get_particle_position_orientation()

    Returns:
        2-tuple:
            - (n, 3)-array: per-particle (x,y,z) position
            - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError()

initialize() classmethod

Initializes this system

Source code in omnigibson/systems/system_base.py
@classmethod
def initialize(cls):
    """
    Initializes this system
    """
    global _CALLBACKS_ON_SYSTEM_INIT

    assert not cls.initialized, f"Already initialized system {cls.name}!"
    og.sim.stage.DefinePrim(cls.prim_path, "Scope")

    cls.initialized = True

    # Add to registry
    SYSTEM_REGISTRY.add(obj=cls)

    # Avoid circular import
    if og.sim.is_playing():
        from omnigibson.transition_rules import TransitionRuleAPI
        TransitionRuleAPI.refresh_all_rules()

    # Run any callbacks
    for callback in _CALLBACKS_ON_SYSTEM_INIT.values():
        callback(cls)

material()

Returns:

Type Description

None or MaterialPrim: Material belonging to this system, if there is any

Source code in omnigibson/systems/system_base.py
@classproperty
def material(cls):
    """
    Returns:
        None or MaterialPrim: Material belonging to this system, if there is any
    """
    return None

n_particles()

Returns:

Name Type Description
int

Number of particles belonging to this system

Source code in omnigibson/systems/system_base.py
@classproperty
def n_particles(cls):
    """
    Returns:
        int: Number of particles belonging to this system
    """
    raise NotImplementedError()

prim_path()

Returns:

Name Type Description
str

Path to this system's prim in the scene stage

Source code in omnigibson/systems/system_base.py
@classproperty
def prim_path(cls):
    """
    Returns:
        str: Path to this system's prim in the scene stage
    """
    return f"/World/{cls.name}"

remove_all_particles() classmethod

Removes all particles and deletes them from the simulator

Source code in omnigibson/systems/system_base.py
@classmethod
def remove_all_particles(cls):
    """
    Removes all particles and deletes them from the simulator
    """
    raise NotImplementedError()

remove_particles(idxs, **kwargs) classmethod

Removes pre-existing particles

Parameters:

Name Type Description Default
idxs array

(n_particles,) shaped array specifying IDs of particles to delete

required
**kwargs dict

Any additional keyword-specific arguments required by subclass implementation

{}
Source code in omnigibson/systems/system_base.py
@classmethod
def remove_particles(
        cls,
        idxs,
        **kwargs,
):
    """
    Removes pre-existing particles

    Args:
        idxs (np.array): (n_particles,) shaped array specifying IDs of particles to delete
        **kwargs (dict): Any additional keyword-specific arguments required by subclass implementation
    """
    raise NotImplementedError()

reset() classmethod

Reset this system

Source code in omnigibson/systems/system_base.py
@classmethod
def reset(cls):
    """
    Reset this system
    """
    cls.remove_all_particles()

sample_scales(n) classmethod

Samples scales uniformly based on @cls.min_scale and @cls.max_scale

Parameters:

Name Type Description Default
n int

Number of scales to sample

required

Returns:

Type Description

(n, 3) array: Array of sampled scales

Source code in omnigibson/systems/system_base.py
@classmethod
def sample_scales(cls, n):
    """
    Samples scales uniformly based on @cls.min_scale and @cls.max_scale

    Args:
        n (int): Number of scales to sample

    Returns:
        (n, 3) array: Array of sampled scales
    """
    return np.random.uniform(cls.min_scale, cls.max_scale, (n, 3))

set_particle_local_pose(idx, position=None, orientation=None) classmethod

Sets particle's position and orientation in the particle's parent frame

Parameters:

Name Type Description Default
idx int

Index of the particle to set position and orientation for. Note: this is equivalent to setting the corresponding idx'th entry from @set_particles_local_pose()

required
position 3 - array

particle (x,y,z) position

None
orientation 4 - array

particle (x,y,z,w) quaternion orientation

None
Source code in omnigibson/systems/system_base.py
@classmethod
def set_particle_local_pose(cls, idx, position=None, orientation=None):
    """
    Sets particle's position and orientation in the particle's parent frame

    Args:
        idx (int): Index of the particle to set position and orientation for. Note: this is
            equivalent to setting the corresponding idx'th entry from @set_particles_local_pose()
        position (3-array): particle (x,y,z) position
        orientation (4-array): particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError()

set_particle_position_orientation(idx, position=None, orientation=None) classmethod

Sets particle's position and orientation. This automatically takes into account the relative pose w.r.t. its parent link and the global pose of that parent link.

Parameters:

Name Type Description Default
idx int

Index of the particle to set position and orientation for. Note: this is equivalent to setting the corresponding idx'th entry from @set_particles_position_orientation()

required
position 3 - array

particle (x,y,z) position

None
orientation 4 - array

particle (x,y,z,w) quaternion orientation

None
Source code in omnigibson/systems/system_base.py
@classmethod
def set_particle_position_orientation(cls, idx, position=None, orientation=None):
    """
    Sets particle's position and orientation. This automatically takes into account the relative
    pose w.r.t. its parent link and the global pose of that parent link.

    Args:
        idx (int): Index of the particle to set position and orientation for. Note: this is
            equivalent to setting the corresponding idx'th entry from @set_particles_position_orientation()
        position (3-array): particle (x,y,z) position
        orientation (4-array): particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError()

set_particles_local_pose(positions=None, orientations=None) classmethod

Sets all particles' positions and orientations that belong to this system in the particles' parent frames

Parameters:

Name Type Description Default
positions n - array

(n, 3) per-particle (x,y,z) position

None
orientations n - array

(n, 4) per-particle (x,y,z,w) quaternion orientation

None
Source code in omnigibson/systems/system_base.py
@classmethod
def set_particles_local_pose(cls, positions=None, orientations=None):
    """
    Sets all particles' positions and orientations that belong to this system in the particles' parent frames

    Args:
        positions (n-array): (n, 3) per-particle (x,y,z) position
        orientations (n-array): (n, 4) per-particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError()

set_particles_position_orientation(positions=None, orientations=None) classmethod

Sets all particles' positions and orientations that belong to this system in the world frame

Note: This is more optimized than doing a for loop with self.set_particle_position_orientation()

Parameters:

Name Type Description Default
positions n - array

(n, 3) per-particle (x,y,z) position

None
orientations n - array

(n, 4) per-particle (x,y,z,w) quaternion orientation

None
Source code in omnigibson/systems/system_base.py
@classmethod
def set_particles_position_orientation(cls, positions=None, orientations=None):
    """
    Sets all particles' positions and orientations that belong to this system in the world frame

    Note: This is more optimized than doing a for loop with self.set_particle_position_orientation()

    Args:
        positions (n-array): (n, 3) per-particle (x,y,z) position
        orientations (n-array): (n, 4) per-particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError()

update() classmethod

Executes any necessary system updates, once per og.sim._non_physics_step

Source code in omnigibson/systems/system_base.py
@classmethod
def update(cls):
    """
    Executes any necessary system updates, once per og.sim._non_physics_step
    """
    # Default is no-op
    pass

PhysicalParticleSystem

Bases: BaseSystem

System whose generated particles are subject to physics

Source code in omnigibson/systems/system_base.py
class PhysicalParticleSystem(BaseSystem):
    """
    System whose generated particles are subject to physics
    """
    @classmethod
    def initialize(cls):
        # Run super first
        super().initialize()

        # Make sure min and max scale are identical
        assert np.all(cls.min_scale == cls.max_scale), \
            "Min and max scale should be identical for PhysicalParticleSystem!"

    @classproperty
    def particle_density(cls):
        """
        Returns:
            float: The per-particle density, in kg / m^3
        """
        raise NotImplementedError()

    @classproperty
    def particle_radius(cls):
        """
        Returns:
            float: Radius for the particles to be generated, for the purpose of sampling
        """
        raise NotImplementedError()

    @classproperty
    def particle_contact_radius(cls):
        """
        Returns:
            float: Contact radius for the particles to be generated, for the purpose of estimating contacts
        """
        raise NotImplementedError()

    @classproperty
    def particle_particle_rest_distance(cls):
        """
        Returns:
            The minimum distance between individual particles at rest
        """
        return cls.particle_radius * 2.0

    @classmethod
    def check_in_contact(cls, positions):
        """
        Checks whether each particle specified by @particle_positions are in contact with any rigid body.

        NOTE: This is a rough proxy for contact, given @positions. Should not be taken as ground truth.
        This is because for efficiency and underlying physics reasons, it's easier to treat particles as spheres
        for fast checking. For particles directly spawned from Omniverse's underlying ParticleSystem API, it is a
        rough proxy semantically, though it is accurate in sim-physics since all spawned particles interact as spheres.
        For particles spawned manually as rigid bodies, it is a rough proxy both semantically and physically, as the
        object physically interacts with its non-uniform geometry.

        Args:
            positions (np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) positions

        Returns:
            n-array: (n_particles,) boolean array, True if in contact, otherwise False
        """
        in_contact = np.zeros(len(positions), dtype=bool)
        for idx, pos in enumerate(positions):
            # TODO: Maybe multiply particle contact radius * 2?
            in_contact[idx] = og.sim.psqi.overlap_sphere_any(cls.particle_contact_radius, pos)
        return in_contact

    @classmethod
    def generate_particles_from_link(
            cls,
            obj,
            link,
            use_visual_meshes=True,
            mesh_name_prefixes=None,
            check_contact=True,
            sampling_distance=None,
            max_samples=None,
            **kwargs,
    ):
        """
        Generates a new particle instancer with unique identification number @idn, with particles sampled from the mesh
        located at @mesh_prim_path, and registers it internally. This will also check for collision with other rigid
        objects before spawning in individual particles

        Args:
            obj (EntityPrim): Object whose @link's visual meshes will be converted into sampled particles
            link (RigidPrim): @obj's link whose visual meshes will be converted into sampled particles
            use_visual_meshes (bool): Whether to use visual meshes of the link to generate particles
            mesh_name_prefixes (None or str): If specified, specifies the substring that must exist in @link's
                mesh names in order for that mesh to be included in the particle generator function.
                If None, no filtering will be used.
            check_contact (bool): If True, will only spawn in particles that do not collide with other rigid bodies
            sampling_distance (None or float): If specified, sets the distance between sampled particles. If None,
                a simulator autocomputed value will be used
            max_samples (None or int): If specified, maximum number of particles to sample
            **kwargs (dict): Any additional keyword-mapped arguments required by subclass implementation
        """
        # Run sanity checks
        assert cls.initialized, "Must initialize system before generating particle instancers!"

        # Generate a checker function to see if particles are within the link's volumes
        check_in_volume, _ = generate_points_in_volume_checker_function(
            obj=obj,
            volume_link=link,
            use_visual_meshes=use_visual_meshes,
            mesh_name_prefixes=mesh_name_prefixes,
        )

        # Grab the link's AABB (or fallback to obj AABB if link does not have a valid AABB),
        # and generate a grid of points based on the sampling distance
        try:
            low, high = link.visual_aabb
            extent = link.visual_aabb_extent
        except ValueError:
            low, high = obj.aabb
            extent = obj.aabb_extent
        # We sample the range of each extent minus
        sampling_distance = 2 * cls.particle_radius if sampling_distance is None else sampling_distance
        n_particles_per_axis = (extent / sampling_distance).astype(int)
        assert np.all(n_particles_per_axis), f"link {link.name} is too small to sample any particle of radius {cls.particle_radius}."

        # 1e-10 is added because the extent might be an exact multiple of particle radius
        arrs = [np.arange(l + cls.particle_radius, h - cls.particle_radius + 1e-10, cls.particle_particle_rest_distance)
                for l, h, n in zip(low, high, n_particles_per_axis)]
        # Generate 3D-rectangular grid of points
        particle_positions = np.stack([arr.flatten() for arr in np.meshgrid(*arrs)]).T
        # Check which points are inside the volume and only keep those
        particle_positions = particle_positions[np.where(check_in_volume(particle_positions))[0]]

        # Also prune any that in contact with anything if requested
        if check_contact:
            particle_positions = particle_positions[np.where(cls.check_in_contact(particle_positions) == 0)[0]]

        # Also potentially sub-sample if we're past our limit
        if max_samples is not None and len(particle_positions) > max_samples:
            particle_positions = particle_positions[
                np.random.choice(len(particle_positions), size=(int(max_samples),), replace=False)]

        return cls.generate_particles(
            positions=particle_positions,
            **kwargs,
        )

    @classmethod
    def generate_particles_on_object(
            cls,
            obj,
            sampling_distance=None,
            max_samples=None,
            min_samples_for_success=1,
            **kwargs,
    ):
        """
        Generates @n_particles new particle objects and samples their locations on the top surface of object @obj

        Args:
            obj (BaseObject): Object on which to generate a particle instancer with sampled particles on the object's
                top surface
            sampling_distance (None or float): If specified, sets the distance between sampled particles. If None,
                a simulator autocomputed value will be used
            max_samples (None or int): If specified, maximum number of particles to sample
            min_samples_for_success (int): Minimum number of particles required to be sampled successfully in order
                for this generation process to be considered successful
            **kwargs (dict): Any additional keyword-mapped arguments required by subclass implementation

        Returns:
            bool: True if enough particles were generated successfully (number of successfully sampled points >=
                min_samples_for_success), otherwise False
        """
        assert max_samples >= min_samples_for_success, "number of particles to sample should exceed the min for success"

        # We densely sample a grid of points by ray-casting from top to bottom to find the valid positions
        radius = cls.particle_radius
        results = sample_cuboid_on_object_full_grid_topdown(
            obj,
            # the grid is fully dense - particles are sitting next to each other
            ray_spacing=radius * 2 if sampling_distance is None else sampling_distance,
            # assume the particles are extremely small - sample cuboids of size 0 for better performance
            cuboid_dimensions=np.zeros(3),
            # raycast start inside the aabb in x-y plane and outside the aabb in the z-axis
            aabb_offset=np.array([-radius, -radius, radius]),
            # bottom padding should be the same as the particle radius
            cuboid_bottom_padding=radius,
            # undo_cuboid_bottom_padding should be False - the sampled positions are above the surface by its radius
            undo_cuboid_bottom_padding=False,
        )
        particle_positions = np.array([result[0] for result in results if result[0] is not None])
        # Also potentially sub-sample if we're past our limit
        if max_samples is not None and len(particle_positions) > max_samples:
            particle_positions = particle_positions[
                np.random.choice(len(particle_positions), size=(max_samples,), replace=False)]

        n_particles = len(particle_positions)
        success = n_particles >= min_samples_for_success
        # If we generated a sufficient number of points, generate them in the simulator
        if success:
            cls.generate_particles(
                positions=particle_positions,
                **kwargs,
            )

        return success

check_in_contact(positions) classmethod

Checks whether each particle specified by @particle_positions are in contact with any rigid body.

NOTE: This is a rough proxy for contact, given @positions. Should not be taken as ground truth. This is because for efficiency and underlying physics reasons, it's easier to treat particles as spheres for fast checking. For particles directly spawned from Omniverse's underlying ParticleSystem API, it is a rough proxy semantically, though it is accurate in sim-physics since all spawned particles interact as spheres. For particles spawned manually as rigid bodies, it is a rough proxy both semantically and physically, as the object physically interacts with its non-uniform geometry.

Parameters:

Name Type Description Default
positions array

(n_particles, 3) shaped array specifying per-particle (x,y,z) positions

required

Returns:

Type Description

n-array: (n_particles,) boolean array, True if in contact, otherwise False

Source code in omnigibson/systems/system_base.py
@classmethod
def check_in_contact(cls, positions):
    """
    Checks whether each particle specified by @particle_positions are in contact with any rigid body.

    NOTE: This is a rough proxy for contact, given @positions. Should not be taken as ground truth.
    This is because for efficiency and underlying physics reasons, it's easier to treat particles as spheres
    for fast checking. For particles directly spawned from Omniverse's underlying ParticleSystem API, it is a
    rough proxy semantically, though it is accurate in sim-physics since all spawned particles interact as spheres.
    For particles spawned manually as rigid bodies, it is a rough proxy both semantically and physically, as the
    object physically interacts with its non-uniform geometry.

    Args:
        positions (np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) positions

    Returns:
        n-array: (n_particles,) boolean array, True if in contact, otherwise False
    """
    in_contact = np.zeros(len(positions), dtype=bool)
    for idx, pos in enumerate(positions):
        # TODO: Maybe multiply particle contact radius * 2?
        in_contact[idx] = og.sim.psqi.overlap_sphere_any(cls.particle_contact_radius, pos)
    return in_contact

Generates a new particle instancer with unique identification number @idn, with particles sampled from the mesh located at @mesh_prim_path, and registers it internally. This will also check for collision with other rigid objects before spawning in individual particles

Parameters:

Name Type Description Default
obj EntityPrim

Object whose @link's visual meshes will be converted into sampled particles

required
link RigidPrim

@obj's link whose visual meshes will be converted into sampled particles

required
use_visual_meshes bool

Whether to use visual meshes of the link to generate particles

True
mesh_name_prefixes None or str

If specified, specifies the substring that must exist in @link's mesh names in order for that mesh to be included in the particle generator function. If None, no filtering will be used.

None
check_contact bool

If True, will only spawn in particles that do not collide with other rigid bodies

True
sampling_distance None or float

If specified, sets the distance between sampled particles. If None, a simulator autocomputed value will be used

None
max_samples None or int

If specified, maximum number of particles to sample

None
**kwargs dict

Any additional keyword-mapped arguments required by subclass implementation

{}
Source code in omnigibson/systems/system_base.py
@classmethod
def generate_particles_from_link(
        cls,
        obj,
        link,
        use_visual_meshes=True,
        mesh_name_prefixes=None,
        check_contact=True,
        sampling_distance=None,
        max_samples=None,
        **kwargs,
):
    """
    Generates a new particle instancer with unique identification number @idn, with particles sampled from the mesh
    located at @mesh_prim_path, and registers it internally. This will also check for collision with other rigid
    objects before spawning in individual particles

    Args:
        obj (EntityPrim): Object whose @link's visual meshes will be converted into sampled particles
        link (RigidPrim): @obj's link whose visual meshes will be converted into sampled particles
        use_visual_meshes (bool): Whether to use visual meshes of the link to generate particles
        mesh_name_prefixes (None or str): If specified, specifies the substring that must exist in @link's
            mesh names in order for that mesh to be included in the particle generator function.
            If None, no filtering will be used.
        check_contact (bool): If True, will only spawn in particles that do not collide with other rigid bodies
        sampling_distance (None or float): If specified, sets the distance between sampled particles. If None,
            a simulator autocomputed value will be used
        max_samples (None or int): If specified, maximum number of particles to sample
        **kwargs (dict): Any additional keyword-mapped arguments required by subclass implementation
    """
    # Run sanity checks
    assert cls.initialized, "Must initialize system before generating particle instancers!"

    # Generate a checker function to see if particles are within the link's volumes
    check_in_volume, _ = generate_points_in_volume_checker_function(
        obj=obj,
        volume_link=link,
        use_visual_meshes=use_visual_meshes,
        mesh_name_prefixes=mesh_name_prefixes,
    )

    # Grab the link's AABB (or fallback to obj AABB if link does not have a valid AABB),
    # and generate a grid of points based on the sampling distance
    try:
        low, high = link.visual_aabb
        extent = link.visual_aabb_extent
    except ValueError:
        low, high = obj.aabb
        extent = obj.aabb_extent
    # We sample the range of each extent minus
    sampling_distance = 2 * cls.particle_radius if sampling_distance is None else sampling_distance
    n_particles_per_axis = (extent / sampling_distance).astype(int)
    assert np.all(n_particles_per_axis), f"link {link.name} is too small to sample any particle of radius {cls.particle_radius}."

    # 1e-10 is added because the extent might be an exact multiple of particle radius
    arrs = [np.arange(l + cls.particle_radius, h - cls.particle_radius + 1e-10, cls.particle_particle_rest_distance)
            for l, h, n in zip(low, high, n_particles_per_axis)]
    # Generate 3D-rectangular grid of points
    particle_positions = np.stack([arr.flatten() for arr in np.meshgrid(*arrs)]).T
    # Check which points are inside the volume and only keep those
    particle_positions = particle_positions[np.where(check_in_volume(particle_positions))[0]]

    # Also prune any that in contact with anything if requested
    if check_contact:
        particle_positions = particle_positions[np.where(cls.check_in_contact(particle_positions) == 0)[0]]

    # Also potentially sub-sample if we're past our limit
    if max_samples is not None and len(particle_positions) > max_samples:
        particle_positions = particle_positions[
            np.random.choice(len(particle_positions), size=(int(max_samples),), replace=False)]

    return cls.generate_particles(
        positions=particle_positions,
        **kwargs,
    )

generate_particles_on_object(obj, sampling_distance=None, max_samples=None, min_samples_for_success=1, **kwargs) classmethod

Generates @n_particles new particle objects and samples their locations on the top surface of object @obj

Parameters:

Name Type Description Default
obj BaseObject

Object on which to generate a particle instancer with sampled particles on the object's top surface

required
sampling_distance None or float

If specified, sets the distance between sampled particles. If None, a simulator autocomputed value will be used

None
max_samples None or int

If specified, maximum number of particles to sample

None
min_samples_for_success int

Minimum number of particles required to be sampled successfully in order for this generation process to be considered successful

1
**kwargs dict

Any additional keyword-mapped arguments required by subclass implementation

{}

Returns:

Name Type Description
bool

True if enough particles were generated successfully (number of successfully sampled points >= min_samples_for_success), otherwise False

Source code in omnigibson/systems/system_base.py
@classmethod
def generate_particles_on_object(
        cls,
        obj,
        sampling_distance=None,
        max_samples=None,
        min_samples_for_success=1,
        **kwargs,
):
    """
    Generates @n_particles new particle objects and samples their locations on the top surface of object @obj

    Args:
        obj (BaseObject): Object on which to generate a particle instancer with sampled particles on the object's
            top surface
        sampling_distance (None or float): If specified, sets the distance between sampled particles. If None,
            a simulator autocomputed value will be used
        max_samples (None or int): If specified, maximum number of particles to sample
        min_samples_for_success (int): Minimum number of particles required to be sampled successfully in order
            for this generation process to be considered successful
        **kwargs (dict): Any additional keyword-mapped arguments required by subclass implementation

    Returns:
        bool: True if enough particles were generated successfully (number of successfully sampled points >=
            min_samples_for_success), otherwise False
    """
    assert max_samples >= min_samples_for_success, "number of particles to sample should exceed the min for success"

    # We densely sample a grid of points by ray-casting from top to bottom to find the valid positions
    radius = cls.particle_radius
    results = sample_cuboid_on_object_full_grid_topdown(
        obj,
        # the grid is fully dense - particles are sitting next to each other
        ray_spacing=radius * 2 if sampling_distance is None else sampling_distance,
        # assume the particles are extremely small - sample cuboids of size 0 for better performance
        cuboid_dimensions=np.zeros(3),
        # raycast start inside the aabb in x-y plane and outside the aabb in the z-axis
        aabb_offset=np.array([-radius, -radius, radius]),
        # bottom padding should be the same as the particle radius
        cuboid_bottom_padding=radius,
        # undo_cuboid_bottom_padding should be False - the sampled positions are above the surface by its radius
        undo_cuboid_bottom_padding=False,
    )
    particle_positions = np.array([result[0] for result in results if result[0] is not None])
    # Also potentially sub-sample if we're past our limit
    if max_samples is not None and len(particle_positions) > max_samples:
        particle_positions = particle_positions[
            np.random.choice(len(particle_positions), size=(max_samples,), replace=False)]

    n_particles = len(particle_positions)
    success = n_particles >= min_samples_for_success
    # If we generated a sufficient number of points, generate them in the simulator
    if success:
        cls.generate_particles(
            positions=particle_positions,
            **kwargs,
        )

    return success

particle_contact_radius()

Returns:

Name Type Description
float

Contact radius for the particles to be generated, for the purpose of estimating contacts

Source code in omnigibson/systems/system_base.py
@classproperty
def particle_contact_radius(cls):
    """
    Returns:
        float: Contact radius for the particles to be generated, for the purpose of estimating contacts
    """
    raise NotImplementedError()

particle_density()

Returns:

Name Type Description
float

The per-particle density, in kg / m^3

Source code in omnigibson/systems/system_base.py
@classproperty
def particle_density(cls):
    """
    Returns:
        float: The per-particle density, in kg / m^3
    """
    raise NotImplementedError()

particle_particle_rest_distance()

Returns:

Type Description

The minimum distance between individual particles at rest

Source code in omnigibson/systems/system_base.py
@classproperty
def particle_particle_rest_distance(cls):
    """
    Returns:
        The minimum distance between individual particles at rest
    """
    return cls.particle_radius * 2.0

particle_radius()

Returns:

Name Type Description
float

Radius for the particles to be generated, for the purpose of sampling

Source code in omnigibson/systems/system_base.py
@classproperty
def particle_radius(cls):
    """
    Returns:
        float: Radius for the particles to be generated, for the purpose of sampling
    """
    raise NotImplementedError()

VisualParticleSystem

Bases: BaseSystem

Particle system class for generating particles not subject to physics, and are attached to individual objects

Source code in omnigibson/systems/system_base.py
class VisualParticleSystem(BaseSystem):
    """
    Particle system class for generating particles not subject to physics, and are attached to individual objects
    """
    # Maps group name to the particles associated with it
    # This is an ordered dict of ordered dict (nested ordered dict maps particle names to particle instance)
    _group_particles = None

    # Maps group name to the parent object (the object with particles attached to it) of the group
    _group_objects = None

    # Maps group name to tuple (min_scale, max_scale) to apply to sampled particles for that group
    _group_scales = None

    @classmethod
    def initialize(cls):
        # Run super method first
        super().initialize()

        # Initialize mutable class variables so they don't automatically get overridden by children classes
        cls._group_particles = dict()
        cls._group_objects = dict()
        cls._group_scales = dict()

    @classproperty
    def particle_object(cls):
        """
        Returns:
            XFormPrim: Particle object to be used as a template for duplication
        """
        raise NotImplementedError()

    @classproperty
    def groups(cls):
        """
        Returns:
            set of str: Current attachment particle group names
        """
        return set(cls._group_particles.keys())

    @classproperty
    def _store_local_poses(cls):
        # Store local poses since particles are attached to moving bodies
        return True

    @classproperty
    def scale_relative_to_parent(cls):
        """
        Returns:
            bool: Whether or not particles should be scaled relative to the group's parent object. NOTE: If True,
                this will OVERRIDE cls.min_scale and cls.max_scale when sampling particles!
        """
        return False

    @classproperty
    def state_size(cls):
        # Get super size first
        state_size = super().state_size

        # Additionally, we have n_groups (1), with m_particles for each group (n), attached_obj_uuids (n), and
        # particle ids, particle indices, and corresponding link info for each particle (m * 3)
        return state_size + 1 + 2 * len(cls._group_particles) + \
               sum(3 * cls.num_group_particles(group) for group in cls.groups)

    @classmethod
    def _clear(cls):
        super()._clear()

        # Clear all groups as well
        cls._group_particles = dict()
        cls._group_objects = dict()
        cls._group_scales = dict()

    @classmethod
    def remove_all_group_particles(cls, group):
        """
        Remove particle with name @name from both the simulator as well as internally

        Args:
            group (str): Name of the attachment group to remove all particles from
        """
        # Make sure the group exists
        cls._validate_group(group=group)
        # Remove all particles from the group
        for particle_name in tuple(cls._group_particles[group].keys()):
            cls.remove_particle_by_name(name=particle_name)

    @classmethod
    def num_group_particles(cls, group):
        """
        Gets the number of particles for the given group in the simulator

        Args:
            group (str): Name of the attachment group to remove all particles from.

        Returns:
            int: Number of particles allocated to this group in the scene. Note that if @group does not
                exist, this will return 0
        """
        # Make sure the group exists
        cls._validate_group(group=group)
        return len(cls._group_particles[group])

    @classmethod
    def get_group_name(cls, obj):
        """
        Grabs the corresponding group name for object @obj

        Args:
            obj (BaseObject): Object for which its procedurally generated particle attachment name should be grabbed

        Returns:
            str: Name of the attachment group to use when executing commands from this class on
                that specific attachment group
        """
        return obj.name

    @classmethod
    def create_attachment_group(cls, obj):
        """
        Creates an attachment group internally for object @obj. Note that this does NOT automatically generate particles
        for this object (should call generate_group_particles(...) ).

        Args:
            obj (BaseObject): Object for which a new particle attachment group will be created for

        Returns:
            str: Name of the attachment group to use when executing commands from this class on
                that specific attachment group
        """
        group = cls.get_group_name(obj=obj)
        # This should only happen once for a single attachment group, so we explicitly check to make sure the object
        # doesn't already exist
        assert group not in cls.groups, \
            f"Cannot create new attachment group because group with name {group} already exists!"

        # Create the group
        cls._group_particles[group] = dict()
        cls._group_objects[group] = obj

        # Compute the group scale if we're scaling relative to parent
        if cls.scale_relative_to_parent:
            cls._group_scales[group] = cls._compute_relative_group_scales(group=group)

        return group

    @classmethod
    def remove_attachment_group(cls, group):
        """
        Removes an attachment group internally for object @obj. Note that this will automatically remove any particles
        currently assigned to that group

        Args:
            group (str): Name of the attachment group to remove

        Returns:
            str: Name of the attachment group to use when executing commands from this class on
                that specific attachment group
        """
        # Make sure the group exists
        cls._validate_group(group=group)

        # Remove all particles from the group
        cls.remove_all_group_particles(group=group)

        # Remove the actual groups
        cls._group_particles.pop(group)
        cls._group_objects.pop(group)
        if cls.scale_relative_to_parent:
            cls._group_scales.pop(group)

        return group

    @classmethod
    def _compute_relative_group_scales(cls, group):
        """
        Computes relative particle scaling for group @group required when @cls.scale_relative_to_parent is True

        Args:
            group (str): Specific group for which to compute the relative particle scaling

        Returns:
            2-tuple:
                - 3-array: min scaling factor
                - 3-array: max scaling factor
        """
        # First set the bbox ranges -- depends on the object's bounding box
        obj = cls._group_objects[group]
        median_aabb_dim = np.median(obj.aabb_extent)

        # Compute lower and upper limits to bbox
        bbox_lower_limit_from_aabb = m.BBOX_LOWER_LIMIT_FRACTION_OF_AABB * median_aabb_dim
        bbox_lower_limit = np.clip(
            bbox_lower_limit_from_aabb,
            m.BBOX_LOWER_LIMIT_MIN,
            m.BBOX_LOWER_LIMIT_MAX,
        )

        bbox_upper_limit_from_aabb = m.BBOX_UPPER_LIMIT_FRACTION_OF_AABB * median_aabb_dim
        bbox_upper_limit = np.clip(
            bbox_upper_limit_from_aabb,
            m.BBOX_UPPER_LIMIT_MIN,
            m.BBOX_UPPER_LIMIT_MAX,
        )

        # Convert these into scaling factors for the x and y axes for our particle object
        particle_bbox = cls.particle_object.aabb_extent
        minimum = np.array([bbox_lower_limit / particle_bbox[0], bbox_lower_limit / particle_bbox[1], 1.0])
        maximum = np.array([bbox_upper_limit / particle_bbox[0], bbox_upper_limit / particle_bbox[1], 1.0])

        return minimum, maximum

    @classmethod
    def sample_scales_by_group(cls, group, n):
        """
        Samples @n particle scales for group @group.

        Args:
            group (str): Specific group for which to sample scales
            n (int): Number of scales to sample

        Returns:
            (n, 3) array: Array of sampled scales
        """
        # Make sure the group exists
        cls._validate_group(group=group)

        # Sample based on whether we're scaling relative to parent or not
        scales = np.random.uniform(*cls._group_scales[group], (n, 3)) if cls.scale_relative_to_parent else cls.sample_scales(n=n)

        # Since the particles will be placed under the object, it will be affected/stretched by obj.scale. In order to
        # preserve the absolute size of the particles, we need to scale the particle by obj.scale in some way. However,
        # since the particles have a relative rotation w.r.t the object, the scale between the two don't align. As a
        # heuristics, we divide it by the avg_scale, which is the cubic root of the product of the scales along 3 axes.
        obj = cls._group_objects[group]
        avg_scale = np.cbrt(np.product(obj.scale))
        return scales / avg_scale

    @classmethod
    def generate_particles(
            cls,
            positions,
            orientations=None,
            scales=None,
            **kwargs,
    ):
        # Should not be called, since particles must be tied to a group!
        raise ValueError("Cannot call generate_particles for a VisualParticleSystem! "
                         "Call generate_group_particles() instead.")

    @classmethod
    def generate_group_particles(
            cls,
            group,
            positions,
            orientations=None,
            scales=None,
            link_prim_paths=None,
    ):
        """
        Generates new particle objects within group @group at the specified pose (@positions, @orientations) with
        corresponding scales @scales.

        NOTE: Assumes positions are the exact contact point on @group object's surface. If cls._CLIP_INTO_OBJECTS
            is not True, then the positions will be offset away from the object by half of its bbox

        Args:
            group (str): Object on which to sample particle locations
            positions (np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) positions
            orientations (None or np.array): (n_particles, 4) shaped array specifying per-particle (x,y,z,w) quaternion
                orientations. If not specified, all will be set to canonical orientation (0, 0, 0, 1)
            scales (None or np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) scaling in its
                local frame. If not specified, all we randomly sampled based on @cls.min_scale and @cls.max_scale
            link_prim_paths (None or list of str): Determines which link each generated particle will
                be attached to. If not specified, all will be attached to the group object's prim, NOT a link
        """
        raise NotImplementedError

    @classmethod
    def generate_group_particles_on_object(cls, group, max_samples=None, min_samples_for_success=1):
        """
        Generates @max_samples new particle objects and samples their locations on the surface of object @obj. Note
        that if any particles are in the group already, they will be removed

        Args:
            group (str): Object on which to sample particle locations
            max_samples (None or int): If specified, maximum number of particles to sample
            min_samples_for_success (int): Minimum number of particles required to be sampled successfully in order
                for this generation process to be considered successful

        Returns:
            bool: True if enough particles were generated successfully (number of successfully sampled points >=
                min_samples_for_success), otherwise False
        """
        raise NotImplementedError

    @classmethod
    def get_group_particles_position_orientation(cls, group):
        """
        Computes all particles' positions and orientations that belong to @group

        Note: This is more optimized than doing a for loop with self.get_particle_position_orientation()

        Args:
            group (str): Group name whose particle positions and orientations should be computed

        Returns:
            2-tuple:
                - (n, 3)-array: per-particle (x,y,z) position
                - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError

    @classmethod
    def set_group_particles_position_orientation(cls, group, positions=None, orientations=None):
        """
        Sets all particles' positions and orientations that belong to @group

        Note: This is more optimized than doing a for loop with self.set_particle_position_orientation()

        Args:
            group (str): Group name whose particle positions and orientations should be computed
            positions (n-array): (n, 3) per-particle (x,y,z) position
            orientations (n-array): (n, 4) per-particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError

    @classmethod
    def get_group_particles_local_pose(cls, group):
        """
        Computes all particles' positions and orientations that belong to @group in the particles' parent frame

        Args:
            group (str): Group name whose particle positions and orientations should be computed

        Returns:
            2-tuple:
                - (n, 3)-array: per-particle (x,y,z) position
                - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError

    @classmethod
    def set_group_particles_local_pose(cls, group, positions=None, orientations=None):
        """
        Sets all particles' positions and orientations that belong to @group in the particles' parent frame

        Args:
            group (str): Group name whose particle positions and orientations should be computed
            positions (n-array): (n, 3) per-particle (x,y,z) position
            orientations (n-array): (n, 4) per-particle (x,y,z,w) quaternion orientation
        """
        raise NotImplementedError

    @classmethod
    def _validate_group(cls, group):
        """
        Checks if particle attachment group @group exists. (If not, can create the group via create_attachment_group).
        This will raise a ValueError if it doesn't exist.

        Args:
            group (str): Name of the group to check for
        """
        if group not in cls.groups:
            raise ValueError(f"Particle attachment group {group} does not exist!")

create_attachment_group(obj) classmethod

Creates an attachment group internally for object @obj. Note that this does NOT automatically generate particles for this object (should call generate_group_particles(...) ).

Parameters:

Name Type Description Default
obj BaseObject

Object for which a new particle attachment group will be created for

required

Returns:

Name Type Description
str

Name of the attachment group to use when executing commands from this class on that specific attachment group

Source code in omnigibson/systems/system_base.py
@classmethod
def create_attachment_group(cls, obj):
    """
    Creates an attachment group internally for object @obj. Note that this does NOT automatically generate particles
    for this object (should call generate_group_particles(...) ).

    Args:
        obj (BaseObject): Object for which a new particle attachment group will be created for

    Returns:
        str: Name of the attachment group to use when executing commands from this class on
            that specific attachment group
    """
    group = cls.get_group_name(obj=obj)
    # This should only happen once for a single attachment group, so we explicitly check to make sure the object
    # doesn't already exist
    assert group not in cls.groups, \
        f"Cannot create new attachment group because group with name {group} already exists!"

    # Create the group
    cls._group_particles[group] = dict()
    cls._group_objects[group] = obj

    # Compute the group scale if we're scaling relative to parent
    if cls.scale_relative_to_parent:
        cls._group_scales[group] = cls._compute_relative_group_scales(group=group)

    return group

generate_group_particles(group, positions, orientations=None, scales=None, link_prim_paths=None) classmethod

Generates new particle objects within group @group at the specified pose (@positions, @orientations) with corresponding scales @scales.

Assumes positions are the exact contact point on @group object's surface. If cls._CLIP_INTO_OBJECTS

is not True, then the positions will be offset away from the object by half of its bbox

Parameters:

Name Type Description Default
group str

Object on which to sample particle locations

required
positions array

(n_particles, 3) shaped array specifying per-particle (x,y,z) positions

required
orientations None or array

(n_particles, 4) shaped array specifying per-particle (x,y,z,w) quaternion orientations. If not specified, all will be set to canonical orientation (0, 0, 0, 1)

None
scales None or array

(n_particles, 3) shaped array specifying per-particle (x,y,z) scaling in its local frame. If not specified, all we randomly sampled based on @cls.min_scale and @cls.max_scale

None
link_prim_paths None or list of str

Determines which link each generated particle will be attached to. If not specified, all will be attached to the group object's prim, NOT a link

None
Source code in omnigibson/systems/system_base.py
@classmethod
def generate_group_particles(
        cls,
        group,
        positions,
        orientations=None,
        scales=None,
        link_prim_paths=None,
):
    """
    Generates new particle objects within group @group at the specified pose (@positions, @orientations) with
    corresponding scales @scales.

    NOTE: Assumes positions are the exact contact point on @group object's surface. If cls._CLIP_INTO_OBJECTS
        is not True, then the positions will be offset away from the object by half of its bbox

    Args:
        group (str): Object on which to sample particle locations
        positions (np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) positions
        orientations (None or np.array): (n_particles, 4) shaped array specifying per-particle (x,y,z,w) quaternion
            orientations. If not specified, all will be set to canonical orientation (0, 0, 0, 1)
        scales (None or np.array): (n_particles, 3) shaped array specifying per-particle (x,y,z) scaling in its
            local frame. If not specified, all we randomly sampled based on @cls.min_scale and @cls.max_scale
        link_prim_paths (None or list of str): Determines which link each generated particle will
            be attached to. If not specified, all will be attached to the group object's prim, NOT a link
    """
    raise NotImplementedError

generate_group_particles_on_object(group, max_samples=None, min_samples_for_success=1) classmethod

Generates @max_samples new particle objects and samples their locations on the surface of object @obj. Note that if any particles are in the group already, they will be removed

Parameters:

Name Type Description Default
group str

Object on which to sample particle locations

required
max_samples None or int

If specified, maximum number of particles to sample

None
min_samples_for_success int

Minimum number of particles required to be sampled successfully in order for this generation process to be considered successful

1

Returns:

Name Type Description
bool

True if enough particles were generated successfully (number of successfully sampled points >= min_samples_for_success), otherwise False

Source code in omnigibson/systems/system_base.py
@classmethod
def generate_group_particles_on_object(cls, group, max_samples=None, min_samples_for_success=1):
    """
    Generates @max_samples new particle objects and samples their locations on the surface of object @obj. Note
    that if any particles are in the group already, they will be removed

    Args:
        group (str): Object on which to sample particle locations
        max_samples (None or int): If specified, maximum number of particles to sample
        min_samples_for_success (int): Minimum number of particles required to be sampled successfully in order
            for this generation process to be considered successful

    Returns:
        bool: True if enough particles were generated successfully (number of successfully sampled points >=
            min_samples_for_success), otherwise False
    """
    raise NotImplementedError

get_group_name(obj) classmethod

Grabs the corresponding group name for object @obj

Parameters:

Name Type Description Default
obj BaseObject

Object for which its procedurally generated particle attachment name should be grabbed

required

Returns:

Name Type Description
str

Name of the attachment group to use when executing commands from this class on that specific attachment group

Source code in omnigibson/systems/system_base.py
@classmethod
def get_group_name(cls, obj):
    """
    Grabs the corresponding group name for object @obj

    Args:
        obj (BaseObject): Object for which its procedurally generated particle attachment name should be grabbed

    Returns:
        str: Name of the attachment group to use when executing commands from this class on
            that specific attachment group
    """
    return obj.name

get_group_particles_local_pose(group) classmethod

Computes all particles' positions and orientations that belong to @group in the particles' parent frame

Parameters:

Name Type Description Default
group str

Group name whose particle positions and orientations should be computed

required

Returns:

Type Description

2-tuple: - (n, 3)-array: per-particle (x,y,z) position - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation

Source code in omnigibson/systems/system_base.py
@classmethod
def get_group_particles_local_pose(cls, group):
    """
    Computes all particles' positions and orientations that belong to @group in the particles' parent frame

    Args:
        group (str): Group name whose particle positions and orientations should be computed

    Returns:
        2-tuple:
            - (n, 3)-array: per-particle (x,y,z) position
            - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError

get_group_particles_position_orientation(group) classmethod

Computes all particles' positions and orientations that belong to @group

Note: This is more optimized than doing a for loop with self.get_particle_position_orientation()

Parameters:

Name Type Description Default
group str

Group name whose particle positions and orientations should be computed

required

Returns:

Type Description

2-tuple: - (n, 3)-array: per-particle (x,y,z) position - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation

Source code in omnigibson/systems/system_base.py
@classmethod
def get_group_particles_position_orientation(cls, group):
    """
    Computes all particles' positions and orientations that belong to @group

    Note: This is more optimized than doing a for loop with self.get_particle_position_orientation()

    Args:
        group (str): Group name whose particle positions and orientations should be computed

    Returns:
        2-tuple:
            - (n, 3)-array: per-particle (x,y,z) position
            - (n, 4)-array: per-particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError

groups()

Returns:

Type Description

set of str: Current attachment particle group names

Source code in omnigibson/systems/system_base.py
@classproperty
def groups(cls):
    """
    Returns:
        set of str: Current attachment particle group names
    """
    return set(cls._group_particles.keys())

num_group_particles(group) classmethod

Gets the number of particles for the given group in the simulator

Parameters:

Name Type Description Default
group str

Name of the attachment group to remove all particles from.

required

Returns:

Name Type Description
int

Number of particles allocated to this group in the scene. Note that if @group does not exist, this will return 0

Source code in omnigibson/systems/system_base.py
@classmethod
def num_group_particles(cls, group):
    """
    Gets the number of particles for the given group in the simulator

    Args:
        group (str): Name of the attachment group to remove all particles from.

    Returns:
        int: Number of particles allocated to this group in the scene. Note that if @group does not
            exist, this will return 0
    """
    # Make sure the group exists
    cls._validate_group(group=group)
    return len(cls._group_particles[group])

particle_object()

Returns:

Name Type Description
XFormPrim

Particle object to be used as a template for duplication

Source code in omnigibson/systems/system_base.py
@classproperty
def particle_object(cls):
    """
    Returns:
        XFormPrim: Particle object to be used as a template for duplication
    """
    raise NotImplementedError()

remove_all_group_particles(group) classmethod

Remove particle with name @name from both the simulator as well as internally

Parameters:

Name Type Description Default
group str

Name of the attachment group to remove all particles from

required
Source code in omnigibson/systems/system_base.py
@classmethod
def remove_all_group_particles(cls, group):
    """
    Remove particle with name @name from both the simulator as well as internally

    Args:
        group (str): Name of the attachment group to remove all particles from
    """
    # Make sure the group exists
    cls._validate_group(group=group)
    # Remove all particles from the group
    for particle_name in tuple(cls._group_particles[group].keys()):
        cls.remove_particle_by_name(name=particle_name)

remove_attachment_group(group) classmethod

Removes an attachment group internally for object @obj. Note that this will automatically remove any particles currently assigned to that group

Parameters:

Name Type Description Default
group str

Name of the attachment group to remove

required

Returns:

Name Type Description
str

Name of the attachment group to use when executing commands from this class on that specific attachment group

Source code in omnigibson/systems/system_base.py
@classmethod
def remove_attachment_group(cls, group):
    """
    Removes an attachment group internally for object @obj. Note that this will automatically remove any particles
    currently assigned to that group

    Args:
        group (str): Name of the attachment group to remove

    Returns:
        str: Name of the attachment group to use when executing commands from this class on
            that specific attachment group
    """
    # Make sure the group exists
    cls._validate_group(group=group)

    # Remove all particles from the group
    cls.remove_all_group_particles(group=group)

    # Remove the actual groups
    cls._group_particles.pop(group)
    cls._group_objects.pop(group)
    if cls.scale_relative_to_parent:
        cls._group_scales.pop(group)

    return group

sample_scales_by_group(group, n) classmethod

Samples @n particle scales for group @group.

Parameters:

Name Type Description Default
group str

Specific group for which to sample scales

required
n int

Number of scales to sample

required

Returns:

Type Description

(n, 3) array: Array of sampled scales

Source code in omnigibson/systems/system_base.py
@classmethod
def sample_scales_by_group(cls, group, n):
    """
    Samples @n particle scales for group @group.

    Args:
        group (str): Specific group for which to sample scales
        n (int): Number of scales to sample

    Returns:
        (n, 3) array: Array of sampled scales
    """
    # Make sure the group exists
    cls._validate_group(group=group)

    # Sample based on whether we're scaling relative to parent or not
    scales = np.random.uniform(*cls._group_scales[group], (n, 3)) if cls.scale_relative_to_parent else cls.sample_scales(n=n)

    # Since the particles will be placed under the object, it will be affected/stretched by obj.scale. In order to
    # preserve the absolute size of the particles, we need to scale the particle by obj.scale in some way. However,
    # since the particles have a relative rotation w.r.t the object, the scale between the two don't align. As a
    # heuristics, we divide it by the avg_scale, which is the cubic root of the product of the scales along 3 axes.
    obj = cls._group_objects[group]
    avg_scale = np.cbrt(np.product(obj.scale))
    return scales / avg_scale

scale_relative_to_parent()

Returns:

Name Type Description
bool

Whether or not particles should be scaled relative to the group's parent object. NOTE: If True, this will OVERRIDE cls.min_scale and cls.max_scale when sampling particles!

Source code in omnigibson/systems/system_base.py
@classproperty
def scale_relative_to_parent(cls):
    """
    Returns:
        bool: Whether or not particles should be scaled relative to the group's parent object. NOTE: If True,
            this will OVERRIDE cls.min_scale and cls.max_scale when sampling particles!
    """
    return False

set_group_particles_local_pose(group, positions=None, orientations=None) classmethod

Sets all particles' positions and orientations that belong to @group in the particles' parent frame

Parameters:

Name Type Description Default
group str

Group name whose particle positions and orientations should be computed

required
positions n - array

(n, 3) per-particle (x,y,z) position

None
orientations n - array

(n, 4) per-particle (x,y,z,w) quaternion orientation

None
Source code in omnigibson/systems/system_base.py
@classmethod
def set_group_particles_local_pose(cls, group, positions=None, orientations=None):
    """
    Sets all particles' positions and orientations that belong to @group in the particles' parent frame

    Args:
        group (str): Group name whose particle positions and orientations should be computed
        positions (n-array): (n, 3) per-particle (x,y,z) position
        orientations (n-array): (n, 4) per-particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError

set_group_particles_position_orientation(group, positions=None, orientations=None) classmethod

Sets all particles' positions and orientations that belong to @group

Note: This is more optimized than doing a for loop with self.set_particle_position_orientation()

Parameters:

Name Type Description Default
group str

Group name whose particle positions and orientations should be computed

required
positions n - array

(n, 3) per-particle (x,y,z) position

None
orientations n - array

(n, 4) per-particle (x,y,z,w) quaternion orientation

None
Source code in omnigibson/systems/system_base.py
@classmethod
def set_group_particles_position_orientation(cls, group, positions=None, orientations=None):
    """
    Sets all particles' positions and orientations that belong to @group

    Note: This is more optimized than doing a for loop with self.set_particle_position_orientation()

    Args:
        group (str): Group name whose particle positions and orientations should be computed
        positions (n-array): (n, 3) per-particle (x,y,z) position
        orientations (n-array): (n, 4) per-particle (x,y,z,w) quaternion orientation
    """
    raise NotImplementedError