Repository navigation
Add hull collision type #139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,17 @@ | ||
| from enum import StrEnum | ||
|
|
||
| from crazyflow.dynamics import Dynamics | ||
| from crazyflow.sim.sim import Sim | ||
|
|
||
| __all__ = ["Sim", "Dynamics"] | ||
|
|
||
| class CollisionType(StrEnum): | ||
| """Collision geometry of the drones, selected with ``select_collision_type``.""" | ||
|
|
||
| sphere = "sphere" | ||
| box = "box" | ||
| hull = "hull" | ||
| default = sphere | ||
|
|
||
|
|
||
| from crazyflow.sim.sim import Sim # noqa: E402 (sim.py imports CollisionType from this package) | ||
|
|
||
| __all__ = ["Sim", "Dynamics", "CollisionType"] | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||
| from __future__ import annotations | ||||||||||||
|
|
||||||||||||
| import copy | ||||||||||||
| import warnings | ||||||||||||
| import xml.etree.ElementTree as ET | ||||||||||||
| from collections import OrderedDict | ||||||||||||
| from functools import partial, wraps | ||||||||||||
|
|
@@ -30,6 +31,7 @@ | |||||||||||
| from crazyflow.dynamics import Dynamics | ||||||||||||
| from crazyflow.dynamics import load_params as load_dynamics_params | ||||||||||||
| from crazyflow.exception import ConfigError, NotInitializedError | ||||||||||||
| from crazyflow.sim import CollisionType | ||||||||||||
| from crazyflow.sim.data import SimControls, SimCore, SimData, SimParams, SimState, SimStateDeriv | ||||||||||||
| from crazyflow.sim.dynamics import ( | ||||||||||||
| first_principles_dynamics, | ||||||||||||
|
|
@@ -345,6 +347,8 @@ def _replicate_drone( | |||||||||||
| asset.append(clone) | ||||||||||||
| new_spec = mujoco.MjSpec.from_string(ET.tostring(root, encoding="unicode")) | ||||||||||||
| new_spec.copy_during_attach = True # write-only, re-apply after from_string() drops it | ||||||||||||
| for mesh in spec.meshes: # to_xml() drops maxhullvert before MuJoCo 3.16, re-apply it | ||||||||||||
| new_spec.mesh(mesh.name).maxhullvert = mesh.maxhullvert | ||||||||||||
| return new_spec | ||||||||||||
|
|
||||||||||||
| def build_mjx_model(self, spec: mujoco.MjSpec) -> tuple[Any, Any, Model, Data]: | ||||||||||||
|
|
@@ -509,6 +513,8 @@ def build_mjx(self): | |||||||||||
| self.viewer = None | ||||||||||||
| self.spec.numeric("max_geom_pairs").data = [self.max_geom_pairs] | ||||||||||||
| self.mj_model, self.mj_data, self.mjx_model, self.mjx_data = self.build_mjx_model(self.spec) | ||||||||||||
| core = self.data.core.replace(mjx_synced=jnp.zeros_like(self.data.core.mjx_synced)) | ||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This should work |
||||||||||||
| self.data = self.data.replace(core=core) | ||||||||||||
|
|
||||||||||||
| def init_data( | ||||||||||||
| self, | ||||||||||||
|
|
@@ -764,26 +770,30 @@ def seed_sim(data: SimData, seed: int, device: Device) -> SimData: | |||||||||||
| return data.replace(core=data.core.replace(rng_key=rng_key)) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def use_box_collision(sim: Sim, enable: bool = True): | ||||||||||||
| """Changes the collision geometry to use boxes or spheres (default). | ||||||||||||
| def select_collision_type(sim: Sim, collision_type: CollisionType = CollisionType.default): | ||||||||||||
| """Select the collision geometry of all drones and rebuild the MJX model. | ||||||||||||
|
|
||||||||||||
| Args: | ||||||||||||
| sim: The simulation instance. | ||||||||||||
| enable: If True, use box collision geometry. If False, use sphere collision geometry. | ||||||||||||
| collision_type: ``sphere`` (default) is a bounding sphere, ``box`` an oriented bounding box | ||||||||||||
| and ``hull`` the convex hull of the drone mesh, limited to 32 vertices. The selected | ||||||||||||
| geometry is in geom group 3, which the viewer hides by default (toggle with key 3). | ||||||||||||
|
Comment on lines
+778
to
+780
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
|
||||||||||||
| Warning: | ||||||||||||
| Using box collision geometry is more computationally expensive than sphere collision | ||||||||||||
| geometry, especially for larger swarms. It is recommended to only enable box collision | ||||||||||||
| geometry for small swarms or when high accuracy is required. | ||||||||||||
| Box and hull collisions are more expensive than spheres. MJX tests all edge pairs of two | ||||||||||||
| hulls, which is cheap on GPU but slows down ``Sim.contacts()`` significantly on CPU for | ||||||||||||
| swarms with many drone pairs. | ||||||||||||
| """ | ||||||||||||
| if collision_type == CollisionType.hull and sim.device.platform == "cpu" and sim.n_drones > 16: | ||||||||||||
| warnings.warn( | ||||||||||||
| f"Hull collision with {sim.n_drones} drones slows down `Sim.contacts()` significantly " | ||||||||||||
| "on CPU. Use a GPU or the sphere/box collision type for large swarms." | ||||||||||||
| ) | ||||||||||||
| for geom in sim.spec.geoms: | ||||||||||||
| if geom.name.startswith("col_sphere"): | ||||||||||||
| geom.contype = 1 * (not enable) | ||||||||||||
| geom.conaffinity = 1 * (not enable) | ||||||||||||
| geom.rgba[3] = 1 * (not enable) | ||||||||||||
| if geom.name.startswith("col_box"): | ||||||||||||
| geom.contype = 1 * enable | ||||||||||||
| geom.conaffinity = 1 * enable | ||||||||||||
| geom.rgba[3] = 1 * enable | ||||||||||||
|
|
||||||||||||
| if not geom.name.startswith(("col_sphere", "col_box", "col_hull")): | ||||||||||||
| continue | ||||||||||||
| enable = geom.name.startswith(f"col_{collision_type}") | ||||||||||||
| geom.contype = 1 * enable | ||||||||||||
| geom.conaffinity = 1 * enable | ||||||||||||
| geom.rgba[3] = 1 * enable | ||||||||||||
| sim.build_mjx() | ||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,24 +1,82 @@ | ||||||||||||||||||||||||||||
| import mujoco | ||||||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from crazyflow.sim import Dynamics, Sim | ||||||||||||||||||||||||||||
| from crazyflow.sim.sim import use_box_collision | ||||||||||||||||||||||||||||
| from crazyflow.control import Control | ||||||||||||||||||||||||||||
| from crazyflow.sim import CollisionType, Dynamics, Sim | ||||||||||||||||||||||||||||
| from crazyflow.sim.sim import select_collision_type | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| RED, GREEN = (1.0, 0.0, 0.0), (0.0, 1.0, 0.0) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def add_contact_indicators(sim: Sim) -> list[int]: | ||||||||||||||||||||||||||||
| """Add a disc above each drone that turns red on contact and return the geom ids.""" | ||||||||||||||||||||||||||||
| for i in range(sim.n_drones): | ||||||||||||||||||||||||||||
| geom = sim.spec.body(f"drone:{i}").add_geom() | ||||||||||||||||||||||||||||
| geom.name = f"indicator:{i}" | ||||||||||||||||||||||||||||
| geom.type = mujoco.mjtGeom.mjGEOM_CYLINDER | ||||||||||||||||||||||||||||
| geom.size[:] = [0.05, 0.003, 0.0] | ||||||||||||||||||||||||||||
| geom.pos[:] = [0.0, 0.0, 0.1] | ||||||||||||||||||||||||||||
| geom.contype = 0 | ||||||||||||||||||||||||||||
| geom.conaffinity = 0 | ||||||||||||||||||||||||||||
| geom.rgba[:] = [*GREEN, 1.0] | ||||||||||||||||||||||||||||
| sim.build_mjx() | ||||||||||||||||||||||||||||
| return [sim.mj_model.geom(f"indicator:{i}").id for i in range(sim.n_drones)] | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def crossing_trajectory(t: float, amplitude: float, period: float) -> tuple[np.ndarray, np.ndarray]: | ||||||||||||||||||||||||||||
| """Drones 0 and 2 oscillate along x in opposite directions through the hovering drone 1. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||
| Position and velocity setpoints at time ``t`` with shape (3, 3) each. | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
| direction = np.array([1.0, 0.0, -1.0]) | ||||||||||||||||||||||||||||
| phase = 2 * np.pi * t / period | ||||||||||||||||||||||||||||
| pos, vel = np.zeros((3, 3)), np.zeros((3, 3)) | ||||||||||||||||||||||||||||
| pos[:, 0] = amplitude * np.sin(phase) * direction | ||||||||||||||||||||||||||||
| pos[:, 2] = 1.0 | ||||||||||||||||||||||||||||
| vel[:, 0] = amplitude * 2 * np.pi / period * np.cos(phase) * direction | ||||||||||||||||||||||||||||
| return pos, vel | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def main(): | ||||||||||||||||||||||||||||
| """Spawn multiple drones in multiple worlds and check for contacts.""" | ||||||||||||||||||||||||||||
| """Fly the outer drones back and forth through a hovering drone and check for contacts. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| The crossing is repeated with every collision type. Each type reports contacts for a different | ||||||||||||||||||||||||||||
| fraction of the crossing, from the loose sphere to the tight convex hull. A disc above each | ||||||||||||||||||||||||||||
| drone turns red while the drone is in contact. Press 3 in the viewer to show the collision | ||||||||||||||||||||||||||||
| geometry. | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+48
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||
| n_worlds, n_drones = 2, 3 | ||||||||||||||||||||||||||||
| sim = Sim(n_worlds=n_worlds, n_drones=n_drones, dynamics=Dynamics.so_rpy, device="cpu") | ||||||||||||||||||||||||||||
| use_box_collision(sim, enable=True) # Enable box collision for all drones | ||||||||||||||||||||||||||||
| sim = Sim( | ||||||||||||||||||||||||||||
| n_worlds=n_worlds, | ||||||||||||||||||||||||||||
| n_drones=n_drones, | ||||||||||||||||||||||||||||
| control=Control.state, | ||||||||||||||||||||||||||||
| dynamics=Dynamics.so_rpy, | ||||||||||||||||||||||||||||
| device="cpu", | ||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+55
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We initially added different options per example to show how these things can be changed on the side. Now that we have a dedicated example for almost all of them, we should remove them to focus on the stuff the example is actually about. We can probably clean these up as we edit examples (like this one here) |
||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| indicators = add_contact_indicators(sim) | ||||||||||||||||||||||||||||
| fps = 60 | ||||||||||||||||||||||||||||
| amplitude, period = 0.3, 2.0 | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| cmd = np.zeros((sim.n_worlds, sim.n_drones, 4)) | ||||||||||||||||||||||||||||
| cmd[..., 3] = sim.data.params.mass[0] * 9.81 * 1.04 | ||||||||||||||||||||||||||||
| for i in range(int(2 * sim.control_freq)): | ||||||||||||||||||||||||||||
| sim.attitude_control(cmd) | ||||||||||||||||||||||||||||
| sim.step(sim.freq // sim.control_freq) | ||||||||||||||||||||||||||||
| if ((i * fps) % sim.control_freq) < fps: | ||||||||||||||||||||||||||||
| sim.render() | ||||||||||||||||||||||||||||
| print(f"Contacts: {sim.contacts().any()}") | ||||||||||||||||||||||||||||
| pos, _ = crossing_trajectory(0.0, amplitude, period) # Start on the trajectory | ||||||||||||||||||||||||||||
| states = sim.data.states | ||||||||||||||||||||||||||||
| sim.data = sim.data.replace(states=states.replace(pos=states.pos.at[:].set(pos))) | ||||||||||||||||||||||||||||
| cmd = np.zeros((sim.n_worlds, sim.n_drones, 16)) | ||||||||||||||||||||||||||||
| cmd[..., 12] = 1.0 # Identity quaternion | ||||||||||||||||||||||||||||
| for collision_type in CollisionType: | ||||||||||||||||||||||||||||
| print(f"Collision type: {collision_type}") | ||||||||||||||||||||||||||||
| select_collision_type(sim, collision_type) # Switches mid-flight, the sim keeps its state | ||||||||||||||||||||||||||||
| for i in range(int(2 * period * sim.control_freq)): | ||||||||||||||||||||||||||||
| t = i / sim.control_freq | ||||||||||||||||||||||||||||
| cmd[..., :3], cmd[..., 3:6] = crossing_trajectory(t, amplitude, period) | ||||||||||||||||||||||||||||
| sim.state_control(cmd) | ||||||||||||||||||||||||||||
| sim.step(sim.freq // sim.control_freq) | ||||||||||||||||||||||||||||
| if ((i * fps) % sim.control_freq) < fps: | ||||||||||||||||||||||||||||
| for drone, gid in enumerate(indicators): | ||||||||||||||||||||||||||||
| hit = sim.contacts(f"drone:{drone}")[0].any() | ||||||||||||||||||||||||||||
| sim.mj_model.geom_rgba[gid, :3] = RED if hit else GREEN | ||||||||||||||||||||||||||||
| sim.render() | ||||||||||||||||||||||||||||
| print(f"Contacts: {sim.contacts().any()}") | ||||||||||||||||||||||||||||
| sim.close() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this rather be in
crazyflow/sim/collision.pythen?