Home

Awesome

tdw_physics

These classes create a generic structure for generating physics datasets using TDW. They aim to:

  1. Simplify the process of writing many similar controllers.
  2. Ensure uniform output data organization across similar controllers.

Requirements

Installation

  1. cd path/to/tdw_physics
  2. pip3 install -e . (This installs the tdw_physics module).

Controllers

See the controllers/ directory for controllers that use tdw_physics as well as documentation. See below for the output .hdf5 file structure.

Changelog

See changelog.

Usage

tdw_physics provides abstract Controller classes. To write your own physics dataset controller, you should create a superclass of the appropriate controller.

Abstract ControllerOutput Data
RigidbodiesDatasetTranforms, Images, CameraMatrices, Rigidbodies, Collision, EnvironmentCollision
TransformsDatasetTransforms, Images, CameraMatrices
FlexDatasetTransforms, Images, CameraMatrics, FlexParticles

Every tdw_physics controller will do the following:

  1. When a dataset controller is initially launched, it will always sent these commands.
  2. Initialize a scene (these commands get sent only once in the entire dataset).
  3. Run a series of trials:
    1. Initialize the trial.
    2. Create a new .hdf5 output file.
    3. Write static data to the .hdf5 file. This data won't change between frames.
    4. Step through frames. Write per-frame data to the .hdf5 file. Check if the trial is done.
    5. When the trial is done, destroy all objects and close the .hdf5 file.

Each trial outputs a separate .hdf5 file in the root output directory. The files are named sequentially, e.g.:

root/
....0000.hdf5
....0001.hdf1

How to Create a Dataset Controller

Regardless of which abstract controller you use, you must override the following functions:

FunctionTypeReturn
get_scene_initialization_commands()List[dict]A list of commands to initialize the dataset's scene. These commands are sent only once in the entire dataset run (e.g. post-processing commands).
get_trial_initialization_commands()List[dict]A list of commands to initialize a single trial. This should include all object setup, avatar position and camera rotation, etc. You do not need to include any cleanup commands such as destroy_object; that is handled automatically elsewhere. NOTE: You must use alternate functions to add objects; see below.
get_per_frame_commands(resp: List[bytes], frame: int):List[dict]Commands to send per-frame, based on the response from the build.
get_field_of_view()floatThe avatar's field of view value.

RigidbodiesDataset

from typing import List
from tdw_physics.rigidbodies_dataset import RigidbodiesDataset

class MyDataset(RigidbodiesDataset):
    def get_scene_initialization_commands(self) -> List[dict]:
        # Your code here.

    def get_trial_initialization_commands(self) -> List[dict]:
        # Your code here.

    def get_per_frame_commands(self, resp: List[bytes], frame: int) -> List[dict]:
        # Your code here.

    def get_field_of_view(self) -> float:
        # Your code here.

A dataset creator that receives and writes per frame: Tranforms, Images, CameraMatrices, Rigidbodies, Collision, and EnvironmentCollision.

Ending a trial

A RigidbodiesDataset trial ends when all objects are "sleeping" i.e. non-moving, or after 1000 frames. Objects that have fallen below the scene's floor (y < -1) are ignored.

You can override this by adding the function def is_done():

    def is_done(self, resp: List[bytes], frame: int) -> bool:
        return frame > 1000 # End after 1000 frames even if objects are still moving.

Adding objects

Objects should only be added in get_trial_initialization_commands() or (more rarely) get_per_frame_commands().

def get_add_physics_object()

Get commands to add an object and assign physics properties. Write the object's static info to the .hdf5 file.

Return: A list of commands to add an object and set its physics values.

from typing import List
from tdw_physics.rigidbodies_dataset import RigidbodiesDataset

class MyDataset(RigidbodiesDataset):
    def get_trial_initialization_commands(self) -> List[dict]:
        commands = []
        # Your code here.
        object_id = self.get_unique_id()
        commands.extend(self.get_add_physics_object(model_name="iron_box",
                                                    library="models_core.json",
                                                    object_id=object_id,
                                                    position={"x": 0, "y": 0, "z": 0},
                                                    rotation={"x": 0, "y": 0, "z": 0},
                                                    default_physics_values=False,
                                                    mass=1.5,
                                                    dynamic_friction=0.1,
                                                    static_friction=0.2,
                                                    bounciness=0.5))
        return commands
ParameterTypeDefaultDescription
model_namestrThe name of the model.
positionDict[str, float]NoneThe position of the model. If None, defaults to {"x": 0, "y": 0, "z": 0}.
rotationDict[str, float]NoneThe starting rotation of the model, in Euler angles. If None, defaults to {"x": 0, "y": 0, "z": 0}.
librarystr""The path to the records file. If left empty, the default library will be selected. See ModelLibrarian.get_library_filenames() and ModelLibrarian.get_default_library().
object_idintThe ID of the new object.
scale_factorDict[str, float]NoneThe scale factor.
kinematicboolFalseIf True, the object will be kinematic.
gravityboolTrueIf True, the object won't respond to gravity.
default_physics_valuesboolTrueIf True, use default physics values. Not all objects have default physics values. To determine if object does: has_default_physics_values = model_name in DEFAULT_OBJECT_AUDIO_STATIC_DATA.
massfloat1The mass of the object. Ignored if default_physics_values == True.
dynamic_frictionfloat0.3The dynamic friction of the object. Ignored if default_physics_values == True.
static_frictionfloat0.3The static friction of the object. Ignored if default_physics_values == True.
bouncinessfloat0.7The bounciness of the object. Ignored if default_physics_values == True.

def get_objects_by_mass()

Return: IDs of objects with mass <= the mass threshold.

ParameterTypeDefaultDescription
massfloatThe mass threshold.

def get_falling_commands()

Return: A list of lists; per-frame commands to make small objects fly up.

ParameterTypeDefaultDescription
massfloat3Objects with <= this mass might receive a force.

PHYSICS_INFO

RigidbodiesDataset caches default physics info per object (see above) in a dictionary where the key is the model name and the values is a PhysicsInfo object:

from tdw_physics.physics_info import PHYSICS_INFO

info = PHYSICS_INFO["chair_billiani_doll"]

print(info.model_name) # chair_billiani_doll
print(info.library)
print(info.mass)
print(info.dynamic_friction)
print(info.static_friction)
print(info.bounciness)

.hdf5 file structure

static/    # Data that doesn't change per frame.
....object_ids
....mass
....static_friction
....dynamic_friction
....bounciness
frames/    # Per-frame data.
....0000/    # The frame number.
........images/    # Each image pass.
............_img
............_id
............_depth
............_normals
............_flow
........objects/    # Per-object data.
............positions
............forwards
............rotations
............velocities
............angular_velocities
........collisions/    # Collisions between two objects.
............object_ids
............relative_velocities
............contacts
........env_collisions/    # Collisions between one object and the environment.
............object_ids
............contacts
........camera_matrices/
............projection_matrix
............camera_matrix
....0001/
........ (etc.)

TransformsDataset

from typing import List
from tdw_physics.transforms_dataset import TransformsDataset

class MyDataset(TransformsDataset):
    def get_scene_initialization_commands(self) -> List[dict]:
        # Your code here.

    def get_trial_initialization_commands(self) -> List[dict]:
        # Your code here.

    def get_per_frame_commands(self, resp: List[bytes], frame: int) -> List[dict]:
        # Your code here.

    def get_field_of_view(self) -> float:
        # Your code here.

A dataset creator that receives and writes per frame: Transforms, Images, CameraMatrices.

Ending a trial

A TransformsDataset trial has no "end" condition based on trial output data; you will need to define this yourself by adding the function def is_done():

    def is_done(self, resp: List[bytes], frame: int) -> bool:
        return frame > 1000 # End after 1000 frames.

Adding objects

def get_add_object()

Return: An add_object command.

from typing import List
from tdw_physics.transforms_dataset import TransformsDataset

class MyDataset(TransformsDataset):
    def get_trial_initialization_commands(self) -> List[dict]:
        commands = []
        # Your code here.
        commands.append(self.get_add_object(model_name="iron_box",
                                            library="models_core.json",
                                            object_id=self.get_unique_id(),
                                            position={"x": 0, "y": 0, "z": 0},
                                            rotation={"x": 0, "y": 0, "z": 0}))
        return commands
ParameterTypeDefaultDescription
model_namestrThe name of the model.
positionDict[str, float]NoneThe position of the model. If None, defaults to {"x": 0, "y": 0, "z": 0}.
rotationDict[str, float]NoneThe starting rotation of the model, in Euler angles. If None, defaults to {"x": 0, "y": 0, "z": 0}.
librarystr""The path to the records file. If left empty, the default library will be selected. See ModelLibrarian.get_library_filenames() and ModelLibrarian.get_default_library().
object_idintThe ID of the new object.

.hdf5 file structure

static/    # Data that doesn't change per frame.
....object_ids
frames/    # Per-frame data.
....0000/    # The frame number.
........images/    # Each image pass.
............_img
............_id
............_depth
............_normals
............_flow
........objects/    # Per-object data.
............positions
............forwards
............rotations
........camera_matrices/
............projection_matrix
............camera_matrix
....0001/
........ (etc.)

FlexDataset

from typing import List
from tdw_physics.flex_dataset import FlexDataset

class MyDataset(FlexDataset):
    def get_scene_initialization_commands(self) -> List[dict]:
        # Your code here.

    def get_trial_initialization_commands(self) -> List[dict]:
        # Your code here.

    def get_per_frame_commands(self, resp: List[bytes], frame: int) -> List[dict]:
        # Your code here.

    def get_field_of_view(self) -> float:
        # Your code here.

A dataset creator that receives and writes per frame: Transforms, Images, CameraMatrices, and FlexParticles.

Adding objects

Controller.add_object() and Conroller.get_add_object() will throw an exception. You must instead use wrapper functions to add Flex objects. They will automatically cache the object ID, allowing the object to be destroyed at the end of the trial.

def add_solid_object()

Return: A list of commands to add a solid-body object.

from typing import List
from tdw_physics.flex_dataset import FlexDataset

class MyDataset(FlexDataset):
    def get_trial_initialization_commands(self) -> List[dict]:
        commands = []
        # Your code here.
        commands.extend(self.add_solid_object(model_name="microwave",
                                              library="models_full.json", 
                                              object_id=self.get_unique_id(),                                      
                                              position={"x": 0, "y": 0, "z": 0},
                                              rotation={"x": 0, "y": 0, "z": 0},
                                              scale_factor={"x": 1, "y": 1, "z": 1},
                                              mesh_expansion=0,
                                              particle_spacing=0.125,
                                              mass_scale=1))
        return commands
ParameterTypeDefaultDescription
model_namestrThe model name.
object_idintThe object ID.
librarystrThe model librarian.
positionDict[str, float]The initial position of the object.
rotationDict[str, float]The initial rotation of the object, in Euler angles.
scale_factorDict[str, float]NoneThe object scale factor. If None, the scale is (1, 1, 1).
mesh_expansionfloat0
particle_spacingfloat0.125
mass_scalefloat1

def add_soft_object()

Return: A list of commands to add a soft-body object.

from typing import List
from tdw_physics.flex_dataset import FlexDataset

class MyDataset(FlexDataset):
    def get_trial_initialization_commands(self) -> List[dict]:
        commands = []
        # Your code here.
        commands.extend(self.add_soft_object(model_name="microwave",
                                             library="models_full.json",
                                             object_id=self.get_unique_id(),
                                             position={"x": 0, "y": 0, "z": 0},
                                             rotation={"x": 0, "y": 0, "z": 0},
                                             scale_factor={"x": 1, "y": 1, "z": 1},
                                             volume_sampling=2,
                                             surface_sampling=0,
                                             mass_scale=1,
                                             cluster_spacing=0.2,
                                             cluster_radius=0.2,
                                             cluster_stiffness=0.2,
                                             link_radius=0.1,
                                             link_stiffness=0.5,
                                             particle_spacing=0.02))
        return commands
ParameterTypeDefaultDescription
model_namestrThe model name.
object_idintThe object ID.
librarystrThe model librarian.
positionDict[str, float]The initial position of the object.
rotationDict[str, float]The initial rotation of the object, in Euler angles.
scale_factorDict[str, float]NoneThe object scale factor. If None, the scale is (1, 1, 1).
volume_samplingfloat2
surface_samplingfloat0
mass_scalefloat1
cluster_spacingfloat0.2
cluster_radiusfloat0.2
cluster_stiffnessfloat0.2
link_radiusfloat0.1
link_stiffnessfloat0.5
particle_spacingfloat0.02

def add_cloth_object()

Return: A list of commands to add a cloth object.

from typing import List
from tdw_physics.flex_dataset import FlexDataset

class MyDataset(FlexDataset):
    def get_trial_initialization_commands(self) -> List[dict]:
        commands = []
        # Your code here.
        commands.extend(self.add_cloth_object(model_name="cloth_square",
                                              library="models_special.json",
                                              object_id=self.get_unique_id(),
                                              position={"x": 0, "y": 0, "z": 0},
                                              rotation={"x": 0, "y": 0, "z": 0},
                                              scale_factor={"x": 1, "y": 1, "z": 1},
                                              stretch_stiffness=0.1,
                                              bend_stiffness=0.1,
                                              tether_stiffness=0.1,
                                              tether_give=0,
                                              pressure=0,
                                              mass_scale=1))
        return commands
ParameterTypeDefaultDescription
model_namestrThe model name.
object_idintThe object ID.
librarystrThe model librarian.
positionDict[str, float]The initial position of the object.
rotationDict[str, float]The initial rotation of the object, in Euler angles.
scale_factorDict[str, float]NoneThe object scale factor. If None, the scale is (1, 1, 1).
mesh_tesselationint1
stretch_stiffnessint0.1
bend_stiffnessint0.1
tether_stiffnessfloat0
tether_givefloat0
pressurefloat0
mass_scalefloat1

def add_fluid_object()

Return: A list of commands to add a fluid object.

from typing import List
from tdw_physics.flex_dataset import FlexDataset


class MyDataset(FlexDataset):
    def get_trial_initialization_commands(self) -> List[dict]:
        commands = []

        # Your code here.

        # Cache the pool ID to destroy it correctly.
        pool_id = Controller.get_unique_id()
        self.non_flex_objects.append(pool_id)
        # Add the pool.
        commands.append(self.get_add_object(model_name="fluid_receptacle1x1",
                                            library="models_special.json",
                                            object_id=pool_id,
                                            position={"x": 0, "y": 0, "z": 0},
                                            rotation={"x": 0, "y": 0, "z": 0}))
        # Add a container here.

        # Add the fluid.
        commands.extend(self.add_fluid_object(position={"x": 0, "y": 1.0, "z": 0},
                                              rotation={"x": 0, "y": 0, "z": 0},
                                              object_id=Controller.get_unique_id(),
                                              fluid_type="water"))
        return commands
ParameterTypeDefaultDescription
object_idintThe object ID.
positionDict[str, float]The initial position of the object.
rotationDict[str, float]The initial rotation of the object, in Euler angles.
scaleDict[str, float]NoneThe object scale factor. If None, the scale is (1, 1, 1).
particle_spacingfloat0.05
mass_scalefloat1
fluid_typestrThe name of the fluid type.

.hdf5 file structure

static/    # Data that doesn't change per frame.
....object_ids
....container/ # Flex container parameters.
........radius
........solid_rest
........fluid_rest
........planes
........(etc.)
....solid_actors/ # Flex solid object parameters.
........object_id
........mass_scale
........(etc.)
....soft_actors/ # Flex soft object parameters.
........object_id
........mass_scale
........(etc.)
....cloth_actors/ # Flex cloth object parameters.
........object_id
........mass_scale
........(etc.)
....fluid_actors/ # Flex fluid object parameters.
........object_id
........mass_scale
........(etc.)
frames/    # Per-frame data.
....0000/    # The frame number.
........images/    # Each image pass.
............_img
............_id
............_depth
............_normals
............_flow
........objects/    # Per-object data.
............positions
............forwards
............rotations
........camera_matrices/
............projection_matrix
............camera_matrix
........particles/    # Per-object particles.
........velocities/    # Per-object velocities.
....0001/
........ (etc.)

utils.py

Some helpful utility functions and variables.

def get_move_along_direction()

Return: A position from pos by distance d along a directional vector defined by pos, target.

from tdw_physics.util import get_move_along_direction

p_0 = {"x": 1, "y": 0, "z": -2}
p_1 = {"x": 5, "y": 0, "z": 3.4}
p_0 = get_move_along_direction(pos=p_0, target=p_1, d=0.7, noise=0.01)
ParameterTypeDefaultDescription
posDict[str, float]The object's position.
targetDict[str, float]The target position.
dfloatThe distance to teleport.
noisefloat0Add a little noise to the teleport.

def get_object_look_at()

Return: A list of commands to rotate an object to look at the target position.

from tdw_physics.util import get_object_look_at

o_id = 0 # Assume that the object has been already added to the scene.
p_1 = {"x": 5, "y": 0, "z": 3.4}
p_0 = get_object_look_at(o_id=o_id, pos=p_1, noise=5)
ParameterTypeDefaultDescription
o_idintThe object's ID.
posDict[str, float]The position to look at.
noisefloat0Rotate the object randomly by this much after applying the look_at command.

def get_args()

Return: Command line arguments common to all controllers.

from tdw_physics.util import get_args
from tdw_physics.rigidbodies_dataset import RigidbodiesDataset

class MyDataset(RigidbodiesDataset):
    # Your code here.
    
if __name__ == "__main__":
    args = get_args("my_dataset")
    MyDataset().run(num=args.num, output_dir=args.dir, temp_path=args.temp, width=args.width, height=args.height)
ParameterTypeDefaultDescription
dataset_dirstrIf you don't provide a --dir argument, the default output director is: "D:/" + dataset_dir

extract_images.py

python3 extract_images.py [ARGUMENTS]

Extract _img images from an .hdf5 file and save them to a destination directory.

ArgumentTypeDefaultDescription
--deststrRoot directory for the images.
--srcstrRoot source directory of the .hdf5 files.