Skip to content

Feature/downwash - #128

Open
rducrist wants to merge 5 commits into
learnsyslab:mainfrom
rducrist:feature/downwash
Open

rducrist wants to merge 5 commits into
learnsyslab:mainfrom
rducrist:feature/downwash

Conversation

@rducrist

Copy link
Copy Markdown
Contributor

This implements downwash as an external wrench. It uses the far-field velocity model from https://arxiv.org/pdf/2403.13321 and the thrust loss computation from https://arxiv.org/pdf/2207.09645.
It works like this:

For each source-target rotor pair, the target rotor’s world position is

$$ \mathbf p_j^W = \mathbf p_{\mathrm{drone}}^W + R_{W\leftarrow B}\mathbf r_j^B. $$

The relative displacement from source to target rotor gives axial separation $$s$$ and lateral distance $$r$$. The far-field velocity model is

$$ \bar{s} = \frac{s}{d}, \qquad h = S(\bar{s}-S_0), \qquad U_c = U_H\frac{B_D}{\bar{s}-S_0}, $$

$$ \xi = \frac{r/d}{h}, \qquad U_D = \frac{U_c} {\left(1+(\sqrt{2}-1)\xi^2\right)^2}. $$

All downwash source velocities are summed to obtain $U_{D,j}$ at each target rotor.

Using the thrust-decay model,

$$ \Delta t_j = -b_v U_{D,j},t_j, \qquad t_j = k_0 + k_1\omega_j + k_2\omega_j^2. $$

Note that $-b_v$ is the thrust loss coefficient. It has to be fitted normally. However for proof of concept I went with a value suggested by AI.

The four individual thrust losses are mapped into a body-frame wrench:

$$ \Delta T = \sum_j \Delta t_j, \qquad \Delta\boldsymbol{\tau}^B = \begin{bmatrix} \Delta\tau_x\\ \Delta\tau_y\\ \Delta\tau_z \end{bmatrix}. $$

Roll/pitch torque comes from the motor arms and mixing matrix; yaw torque comes from the corresponding change in propeller reaction torque. Finally, both are rotated into the world frame and written to states.force and states.torque.

The experiment consists of a drone hovering and the other one flying forth and back underneath the downwash cone at different heights.
image

The resulting downwash cone of the hovering drone looks like this
image

@rducrist

Copy link
Copy Markdown
Contributor Author
Screencast.from.24.09.2026.11.17.34.webm

@ratheron ratheron left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank for for working on this!

In general, the approach is sound. I've added some minor comments. Would be nice to have the specific equations such that it's easier to understand whats going on.

Comment thread examples/plugins/downwash.py Outdated
Comment thread examples/plugins/downwash.py Outdated
Comment thread examples/plugins/downwash.py Outdated
Comment thread examples/plugins/downwash.py Outdated
Comment thread examples/plugins/downwash.py Outdated
Comment thread examples/plugins/downwash.py Outdated
Comment thread examples/plugins/downwash.py
Comment thread examples/plugins/downwash.py Outdated
Comment thread examples/plugins/downwash.py Outdated
Comment on lines +47 to 50
offsets = data.params.L * jnp.stack(
[-mixing_matrix[1], mixing_matrix[0], jnp.zeros_like(mixing_matrix[0])],
axis=0
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The offsets are set freely by you, right? So we could just use the mixing matrix without sign changes and order flips, no?


# Broadcast each source rotation across all target drones and rotors.
source_to_target_body = (
R_body_to_world.as_matrix().mT[:, :, None, None, :, :] @ source_to_target[..., None]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we use rotation.apply() here?

offsets = data.params.L * jnp.stack(
[-mixing_matrix[1], mixing_matrix[0], jnp.zeros_like(mixing_matrix[0])], axis=0
)
rotor_offsets_world = (R_body_to_world.as_matrix() @ offsets).mT

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rotor_offsets_world = (R_body_to_world.as_matrix() @ offsets).mT
rotor_offsets_world = R_body_to_world.apply(offsets)).as_matrix()

If we need the inverse, use .inverse()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use apply here, it's just not as simple as written here. Since offsets is shape (3, 4) and R_body_to_world (1,2,4), we'll encounter some broadcasting issues.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants