Skip to content

Commit baf2035

Browse files
authored
Merge pull request #55 from utiasDSL/feat.rotor_vel_ctrl
Add rotor vel control option
2 parents 75bf7a3 + 4d93467 commit baf2035

7 files changed

Lines changed: 84 additions & 18 deletions

File tree

crazyflow/control/control.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ class Control(str, Enum):
3636
Recommended frequency is >=100 Hz.
3737
"""
3838
force_torque = "force_torque"
39-
"""Force and torque control takes [fx, fy, fz, tx, ty, tz].
39+
"""Force and torque control takes [fc, tx, ty, tz].
40+
41+
Note:
42+
Recommended frequency is >=500 Hz.
43+
"""
44+
rotor_vel = "rotor_vel"
45+
"""Rotor velocity control takes [w1, w2, w3, w4] in RPMs.
4046
4147
Note:
4248
Recommended frequency is >=500 Hz.

crazyflow/sim/data.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ def create(
169169
force_torque=force_torque,
170170
rotor_vel=rotor_vel,
171171
)
172+
case Control.rotor_vel:
173+
return SimControls(
174+
mode=control, state=None, attitude=None, force_torque=None, rotor_vel=rotor_vel
175+
)
172176
case _:
173177
raise ValueError(f"Control mode {control} not implemented")
174178

crazyflow/sim/functional.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from typing import TYPE_CHECKING
44

5+
import jax.numpy as jnp
6+
57
from crazyflow.control import Control
68
from crazyflow.control.control import controllable as _controllable
79
from crazyflow.utils import to_device
@@ -56,6 +58,17 @@ def force_torque_control(data: SimData, controls: Array) -> SimData:
5658
return data
5759

5860

61+
def rotor_vel_control(data: SimData, controls: Array) -> SimData:
62+
"""Rotor velocity control.
63+
64+
Directly set the desired rotor velocities of the drone.
65+
"""
66+
assert data.controls.mode == Control.rotor_vel, f"control type {data.controls.mode} not enabled"
67+
assert controls.shape == (data.core.n_worlds, data.core.n_drones, 4), "controls shape mismatch"
68+
controls = to_device(controls, data.core.steps.device)
69+
return data.replace(controls=data.controls.replace(rotor_vel=controls))
70+
71+
5972
def controllable(data: SimData) -> Array:
6073
"""Check which worlds can currently update their controllers."""
6174
controls = data.controls
@@ -67,6 +80,8 @@ def controllable(data: SimData) -> Array:
6780
case Control.force_torque:
6881
control_steps = controls.force_torque.steps
6982
control_freq = controls.force_torque.freq
83+
case Control.rotor_vel:
84+
return jnp.ones((data.core.n_worlds, 1), dtype=bool, device=data.core.steps.device)
7085
case _:
7186
raise NotImplementedError(f"Control mode {data.controls.mode} not implemented")
7287
return _controllable(data.core.steps, data.core.freq, control_steps, control_freq)

crazyflow/sim/sim.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ def __init__(
7777
):
7878
assert Physics(physics) in Physics, f"Physics mode {physics} not implemented"
7979
assert Control(control) in Control, f"Control mode {control} not implemented"
80-
if physics != Physics.first_principles and control == Control.force_torque:
81-
raise ConfigError("Force-torque control requires first principles physics")
80+
if physics != Physics.first_principles:
81+
if control in (Control.force_torque, Control.rotor_vel):
82+
raise ConfigError(f"Control mode {control} requires first principles physics")
8283
if freq > 10_000 and not jax.config.jax_enable_x64:
8384
raise ConfigError("High frequency simulations require double precision mode")
8485
self.physics = physics
@@ -145,6 +146,10 @@ def force_torque_control(self, controls: Array):
145146
"""Set the desired force and torque for all drones in all worlds."""
146147
self.data = F.force_torque_control(self.data, controls)
147148

149+
def rotor_vel_control(self, controls: Array):
150+
"""Set the desired rotor velocities for all drones in all worlds."""
151+
self.data = F.rotor_vel_control(self.data, controls)
152+
148153
@requires_mujoco_sync
149154
def render(
150155
self,
@@ -303,11 +308,9 @@ def build_data(self) -> SimData:
303308
The simulation data as a single PyTree that can be passed to the pure simulation
304309
functions for stepping and resetting.
305310
"""
306-
state_freq = self.data.controls.state.freq if self.data.controls.state is not None else 0
307-
attitude_freq = (
308-
self.data.controls.attitude.freq if self.data.controls.attitude is not None else 0
309-
)
310-
force_torque_freq = self.data.controls.force_torque.freq
311+
state_freq = 0 if (s := self.data.controls.state) is None else s.freq
312+
attitude_freq = 0 if (a := self.data.controls.attitude) is None else a.freq
313+
force_torque_freq = 0 if (ft := self.data.controls.force_torque) is None else ft.freq
311314
self.data = self.init_data(
312315
state_freq, attitude_freq, force_torque_freq, self.data.core.rng_key
313316
)
@@ -436,6 +439,8 @@ def build_control_fns(
436439
raise NotImplementedError(f"Control mode {control} not implemented for {physics}")
437440
case Control.force_torque:
438441
control_pipeline = (step_force_torque_controller,)
442+
case Control.rotor_vel:
443+
control_pipeline = ()
439444
case _:
440445
raise NotImplementedError(f"Control mode {control} not implemented")
441446

tests/integration/test_interfaces.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import pytest
44
from drone_controllers import parametrize
55
from drone_controllers.mellinger import state2attitude
6+
from drone_models.core import load_params
7+
from drone_models.transform import motor_force2rotor_vel
68
from scipy.spatial.transform import Rotation as R
79

810
from crazyflow.control.control import Control
@@ -52,6 +54,24 @@ def test_attitude_interface(physics: Physics):
5254
assert distance < 0.05, f"Failed to maintain hover with {physics} ({dpos})"
5355

5456

57+
@pytest.mark.integration
58+
def test_rotor_vel_interface():
59+
sim = Sim(physics=Physics.first_principles, control=Control.rotor_vel)
60+
params = load_params("first_principles", "cf2x_L250")
61+
max_rpm = motor_force2rotor_vel(np.array([params["thrust_max"]]), params["rpm2thrust"])[0]
62+
63+
sim.data = sim.data.replace(
64+
states=sim.data.states.replace(pos=sim.data.states.pos.at[..., 2].set(0.5))
65+
)
66+
cmd = np.full((1, 1, 4), max_rpm, dtype=np.float32) # More RPMs than required for hover
67+
sim.rotor_vel_control(cmd)
68+
sim.step(sim.freq * 2) # Run simulation for 2 seconds
69+
70+
# Check if drone is not tilted
71+
assert R.from_quat(sim.data.states.quat[0, 0]).magnitude() < 0.1, "Drone is tilted"
72+
assert sim.data.states.pos[0, 0, 2] > 0.5, "Failed to accelerate with rotor velocity control"
73+
74+
5575
@pytest.mark.integration
5676
@pytest.mark.parametrize("physics", Physics)
5777
def test_swarm_control(physics: Physics):

tests/unit/test_functional.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,16 @@ def test_functional_state_control_device(device: str):
145145
assert isinstance(controls.cmd, jnp.ndarray), "Buffers must remain JAX arrays"
146146
assert isinstance(controls.staged_cmd, jnp.ndarray), "Buffers must remain JAX arrays"
147147
assert jnp.all(controls.staged_cmd == cmd), "Buffers must match command"
148+
149+
150+
@pytest.mark.unit
151+
@pytest.mark.parametrize("control", Control)
152+
def test_functional_controllable(control: Control):
153+
"""Test that functional controllable function works correctly."""
154+
sim = Sim(n_worlds=2, n_drones=3, control=control)
155+
data = sim.build_data()
156+
controllable = F.controllable(data)
157+
assert isinstance(controllable, jnp.ndarray), "Controllable must be a JAX array"
158+
shape = controllable.shape
159+
des_shape = (sim.n_worlds, 1)
160+
assert shape == des_shape, f"Controllable shape must be {des_shape}, got {shape}"

tests/unit/test_sim.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@ def array_compare_assert(x: Array, y: Array, value: bool = True, name: str | Non
5656
def test_sim_init(physics: Physics, device: str, control: Control, n_worlds: int):
5757
n_drones = 1
5858

59-
if physics != Physics.first_principles and control == Control.force_torque:
60-
with pytest.raises(ConfigError):
61-
Sim(n_worlds=n_worlds, physics=physics, device=device, control=control)
62-
return
59+
if physics != Physics.first_principles:
60+
if control in (Control.force_torque, Control.rotor_vel):
61+
with pytest.raises(ConfigError):
62+
Sim(n_worlds=n_worlds, physics=physics, device=device, control=control)
63+
return
6364

6465
sim = Sim(n_worlds=n_worlds, physics=physics, device=device, control=control)
6566
assert sim.n_worlds == n_worlds
@@ -89,10 +90,11 @@ def test_sim_init(physics: Physics, device: str, control: Control, n_worlds: int
8990
assert sim.data.controls.attitude is None
9091

9192
# Test force torque buffer shapes
92-
ft_ctrl = sim.data.controls.force_torque
93-
assert isinstance(ft_ctrl, ControlData)
94-
array_meta_assert(ft_ctrl.cmd, (n_worlds, n_drones, 4), device)
95-
array_meta_assert(ft_ctrl.staged_cmd, (n_worlds, n_drones, 4), device)
93+
if control in (Control.state, Control.attitude, Control.force_torque):
94+
ft_ctrl = sim.data.controls.force_torque
95+
assert isinstance(ft_ctrl, ControlData)
96+
array_meta_assert(ft_ctrl.cmd, (n_worlds, n_drones, 4), device)
97+
array_meta_assert(ft_ctrl.staged_cmd, (n_worlds, n_drones, 4), device)
9698

9799

98100
@pytest.mark.unit
@@ -183,8 +185,9 @@ def test_reset_masked(device: str, physics: Physics):
183185
@pytest.mark.parametrize("physics", Physics)
184186
@pytest.mark.parametrize("control", Control)
185187
def test_sim_step(n_worlds: int, n_drones: int, physics: Physics, control: Control, device: str):
186-
if physics != Physics.first_principles and control == Control.force_torque:
187-
pytest.skip("Force-torque control is not supported with non-first-principles physics")
188+
if physics != Physics.first_principles:
189+
if control in (Control.force_torque, Control.rotor_vel):
190+
pytest.skip(f"{control} is not supported with non-first-principles physics")
188191

189192
sim = Sim(n_worlds=n_worlds, n_drones=n_drones, physics=physics, device=device, control=control)
190193
sim.step(2)

0 commit comments

Comments
 (0)