robolib.systems.physics_system

physics_system.py - Generic physics in robosim. Should only use entities and components and low level utilities.

 1"""physics_system.py - Generic physics in robosim. Should only use entities and components and low level utilities."""
 2import numpy as np
 3from microecs import World
 4
 5from robolib.utils import Point3D, logger
 6from robolib.physics import motion_level1, motion_level2, make_grid_3d, check_collision, integrate_velocity_into_pose
 7from robolib.components import HasMotion, HasPose, HasCollision, HasModel, PhysicsLevel
 8
 9class PhysicsSystem:
10    """
11    A orchestration physics system that runs each subtick. It does 4 steps atomically:
12    - candidate motion: move robots based on their physics 'level' (+other forces)
13    - detect collisions and other similar physical events on the candidate new pose
14    - update the candidate position based on the collisions
15    - commit the new physics state
16    """
17
18    def _motion_from_inputs(self, world: World, dt: float, physics_levels: list[PhysicsLevel] | None = None):
19        qr = world.query(HasMotion)
20        physics_levels = physics_levels or [PhysicsLevel.LEVEL_1, PhysicsLevel.LEVEL_2]
21
22        for level in physics_levels:
23            # TODO: we definitely need a syntax for this pattern (equivalent of: SELECT x where y > 5)
24            mask = (qr.physics_level[:, 0] == level) # (N, ) mask for all the entities (equivalent of where level=level)
25            motion_input = qr.motion_input[mask]
26            if len(motion_input) == 0:
27                continue
28
29            if level == PhysicsLevel.LEVEL_1:
30                vel = motion_level1(motion_input=motion_input, max_velocities=qr.max_velocities[mask])
31                qr.candidate_velocity[mask] = vel
32            elif level == PhysicsLevel.LEVEL_2:
33                vel, acc = motion_level2(motion_input=motion_input, dt=dt, current_velocity=qr.velocity[mask],
34                                         max_accelerations=qr.max_accelerations[mask],
35                                         drag_coefficient=qr.drag_coefficient[mask])
36                qr.candidate_velocity[mask] = vel
37                qr.candidate_acceleration[mask] = acc
38            else:
39                logger.log_every_s(f"Unknown physics level: {level}", "ERROR", freq_s=2)
40
41    def _integrate_velocity_into_pose(self, world: World, dt: float):
42        qr = world.query(HasMotion, HasPose)
43        qr.candidate_pose = integrate_velocity_into_pose(qr.pose, qr.candidate_velocity, dt)
44
45    def _detect_collisions(self, world: World, cell_size: Point3D):
46        qr = world.query(HasModel, HasCollision, HasPose)
47
48        # Step 1) Update the static collider model using the future pose
49        qr.collider_radii = qr.model_radius * qr.scale
50        qr.collider_bbox = qr.candidate_pose[:, 0:3, 3][:, None] + qr.model_bbox * qr.scale[..., None]
51
52        qr.is_colliding = False # note: this broadcasts over all entities in the query result!
53        entities = [world.get_entity(eid) for eid in qr.entity_ids]
54        grid = make_grid_3d(entities, cell_size)
55
56        for cell_entities in grid.values():
57            for i, e1 in enumerate(cell_entities):
58                for e2 in cell_entities[i+1:]:
59                    if check_collision(e1, e2):
60                        e1.is_colliding = e2.is_colliding = True
61
62    def _update_motion_from_collisions(self, world: World):
63        qr = world.query(HasPose, HasCollision, HasMotion)
64        # NOTE: bounce back by negating velocity. We should use some sort of impulse addition + normals
65        # here to figure out the direction of the velocity.
66        qr.candidate_pose = np.where(qr.is_colliding[..., None], qr.pose, qr.candidate_pose)
67        qr.candidate_velocity = np.where(qr.is_colliding, -qr.candidate_velocity, qr.candidate_velocity)
68
69    def _commit(self, world: World):
70        qr = world.query(HasPose, HasMotion)
71        qr.pose = qr.candidate_pose
72        qr.velocity = qr.candidate_velocity
73        qr.acceleration = qr.candidate_acceleration
74
75    def __call__(self, world: World, dt: float, cell_size: Point3D, physics_levels: list[PhysicsLevel] | None = None):
76        self._motion_from_inputs(world, dt, physics_levels)
77        self._integrate_velocity_into_pose(world, dt)
78        self._detect_collisions(world, cell_size)
79        self._update_motion_from_collisions(world)
80        self._commit(world)
class PhysicsSystem:
10class PhysicsSystem:
11    """
12    A orchestration physics system that runs each subtick. It does 4 steps atomically:
13    - candidate motion: move robots based on their physics 'level' (+other forces)
14    - detect collisions and other similar physical events on the candidate new pose
15    - update the candidate position based on the collisions
16    - commit the new physics state
17    """
18
19    def _motion_from_inputs(self, world: World, dt: float, physics_levels: list[PhysicsLevel] | None = None):
20        qr = world.query(HasMotion)
21        physics_levels = physics_levels or [PhysicsLevel.LEVEL_1, PhysicsLevel.LEVEL_2]
22
23        for level in physics_levels:
24            # TODO: we definitely need a syntax for this pattern (equivalent of: SELECT x where y > 5)
25            mask = (qr.physics_level[:, 0] == level) # (N, ) mask for all the entities (equivalent of where level=level)
26            motion_input = qr.motion_input[mask]
27            if len(motion_input) == 0:
28                continue
29
30            if level == PhysicsLevel.LEVEL_1:
31                vel = motion_level1(motion_input=motion_input, max_velocities=qr.max_velocities[mask])
32                qr.candidate_velocity[mask] = vel
33            elif level == PhysicsLevel.LEVEL_2:
34                vel, acc = motion_level2(motion_input=motion_input, dt=dt, current_velocity=qr.velocity[mask],
35                                         max_accelerations=qr.max_accelerations[mask],
36                                         drag_coefficient=qr.drag_coefficient[mask])
37                qr.candidate_velocity[mask] = vel
38                qr.candidate_acceleration[mask] = acc
39            else:
40                logger.log_every_s(f"Unknown physics level: {level}", "ERROR", freq_s=2)
41
42    def _integrate_velocity_into_pose(self, world: World, dt: float):
43        qr = world.query(HasMotion, HasPose)
44        qr.candidate_pose = integrate_velocity_into_pose(qr.pose, qr.candidate_velocity, dt)
45
46    def _detect_collisions(self, world: World, cell_size: Point3D):
47        qr = world.query(HasModel, HasCollision, HasPose)
48
49        # Step 1) Update the static collider model using the future pose
50        qr.collider_radii = qr.model_radius * qr.scale
51        qr.collider_bbox = qr.candidate_pose[:, 0:3, 3][:, None] + qr.model_bbox * qr.scale[..., None]
52
53        qr.is_colliding = False # note: this broadcasts over all entities in the query result!
54        entities = [world.get_entity(eid) for eid in qr.entity_ids]
55        grid = make_grid_3d(entities, cell_size)
56
57        for cell_entities in grid.values():
58            for i, e1 in enumerate(cell_entities):
59                for e2 in cell_entities[i+1:]:
60                    if check_collision(e1, e2):
61                        e1.is_colliding = e2.is_colliding = True
62
63    def _update_motion_from_collisions(self, world: World):
64        qr = world.query(HasPose, HasCollision, HasMotion)
65        # NOTE: bounce back by negating velocity. We should use some sort of impulse addition + normals
66        # here to figure out the direction of the velocity.
67        qr.candidate_pose = np.where(qr.is_colliding[..., None], qr.pose, qr.candidate_pose)
68        qr.candidate_velocity = np.where(qr.is_colliding, -qr.candidate_velocity, qr.candidate_velocity)
69
70    def _commit(self, world: World):
71        qr = world.query(HasPose, HasMotion)
72        qr.pose = qr.candidate_pose
73        qr.velocity = qr.candidate_velocity
74        qr.acceleration = qr.candidate_acceleration
75
76    def __call__(self, world: World, dt: float, cell_size: Point3D, physics_levels: list[PhysicsLevel] | None = None):
77        self._motion_from_inputs(world, dt, physics_levels)
78        self._integrate_velocity_into_pose(world, dt)
79        self._detect_collisions(world, cell_size)
80        self._update_motion_from_collisions(world)
81        self._commit(world)

A orchestration physics system that runs each subtick. It does 4 steps atomically:

  • candidate motion: move robots based on their physics 'level' (+other forces)
  • detect collisions and other similar physical events on the candidate new pose
  • update the candidate position based on the collisions
  • commit the new physics state