Skip to content

Commit

Permalink
Ellipsoid fluid force model
Browse files Browse the repository at this point in the history
Python rewrite of MuJoCo's ellipsoid fluid model C code. It allows accessing all individual fluid force components per body and per geom.
  • Loading branch information
vaxenburg authored Jan 31, 2025
2 parents 0cf9ffe + ab29669 commit 479a2e6
Showing 1 changed file with 310 additions and 0 deletions.
310 changes: 310 additions & 0 deletions flybody/ellipsoid_fluid_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
"""MuJoCo's ellipsoid fluid force model, rewritten in python.
The original C code for passive forces, including fluid forces, is here:
https://github.com/google-deepmind/mujoco/blob/main/src/engine/engine_passive.c
"""

import numpy as np

from dm_control import mujoco
from dm_control import mjcf

mjNFLUID = 12
mjMINVAL = 1e-15


def ellipsoid_fluid_forces(
physics: mjcf.Physics,
) -> tuple[dict[str, dict[int, dict[str, np.ndarray]]], np.ndarray]:
"""For current `physics` state, calculate individual force and torque
components of the ellipsoid fluid model. The inertia-based fluid model is ignored.
See the MuJoCo docs for more detail:
https://mujoco.readthedocs.io/en/stable/computation/fluid.html#ellipsoid-model
And the original C code:
https://github.com/google-deepmind/mujoco/blob/main/src/engine/engine_passive.c
For reference, forces and torques and fluidcoefs controlling them (if any):
Forces:
fA: added mass, no fluidcoef.
fD: viscous drag, fluidcoef[0], fluidcoef[1]
fM: Magnus force, fluidcoef[4].
fK: Kutta lift, fluidcoef[3].
fV: viscous resistance, no fluidcoef.
Torques:
gD: viscous drag, fluidcoef[1], fluidcoef[2].
gV: viscous resistance, no fluidcoef.
Args:
physics: A Physics instance. No in-place changes will be done to
this physics instance.
Returns:
fluid_forces: Ellipsoid fluid force components for all bodies for which
the ellipsoid model applies. All forces are in global coordinates.
Format:
dict[body_name: dict[geom_id: dict['fA': 3-vec, 'fD': 3-vec, ...]]]
qfrc_fluid: Calculated mjData.qfrc_fluid. Only includes contributions
from the ellipsoid fluid model, the inertia fluid model is ignored.
"""

physics = physics.copy(share_model=True)
m = physics.model
d = physics.data
d.qfrc_fluid[:] = 0. # Clean up before calculation.

# Results for all bodies will be stored in this dict.
fluid_forces = {}

for i in range(m.nbody):

use_ellipsoid_model = False

for j in range(m.body_geomnum[i]):
geomid = m.body_geomadr[i] + j
if m.geom_fluid[geomid][0]:
use_ellipsoid_model = True

if use_ellipsoid_model:
# The returned forces are vectors fA, fD, fM, fK, fV, gA, gD, gV
# in global coordinates.
_fluid_forces_for_body_geoms = mj_ellipsoidFluidModel(m, d, i)
# Store results.
body_name = m.id2name(i, 'body')
fluid_forces[body_name] = _fluid_forces_for_body_geoms

return fluid_forces, physics.data.qfrc_fluid


def mji_ellipsoid_max_moment(size, dir):
d0 = size[dir]
d1 = size[(dir+1) % 3]
d2 = size[(dir+2) % 3]
return 8.0 / 15.0 * np.pi * d0 * (np.max((d1, d2)))**4


def mj_addedMassForces(local_vels, local_accels, fluid_density,
virtual_mass, virtual_inertia, local_force):
lin_vel = local_vels[3:]
ang_vel = local_vels[:3]
virtual_lin_mom = fluid_density * virtual_mass * lin_vel
virtual_ang_mom = fluid_density * virtual_inertia * ang_vel

# Here, a block of the original C code skips a calculation that depends on
# acceleration `qacc` because passive forces don't involve acceleration.
# // disabled due to dependency on qacc but included for completeness

added_mass_force = np.cross(virtual_lin_mom, ang_vel)
added_mass_torque1 = np.cross(virtual_lin_mom, lin_vel)
added_mass_torque2 = np.cross(virtual_ang_mom, ang_vel)

local_force[:3] += added_mass_torque1 # This is first term of g_A.
local_force[:3] += added_mass_torque2 # This is second term of g_A.
local_force[3:] += added_mass_force # This is f_A.

# Return results (python addition to C code).
fA = added_mass_force
gA = added_mass_torque1 + added_mass_torque2
return fA, gA


def mj_viscousForces(local_vels, fluid_density, fluid_viscosity, size,
magnus_lift_coef, kutta_lift_coef, blunt_drag_coef,
slender_drag_coef, ang_drag_coef, local_force):

lin_vel = local_vels[3:]
ang_vel = local_vels[:3]
volume = 4.0 / 3.0 * np.pi * size[0] * size[1] * size[2]
d_max = np.max(size)
d_min = np.min(size)
d_mid = size[0] + size[1] + size[2] - d_max - d_min
A_max = np.pi * d_max * d_mid

# This is f_M.
magnus_force = np.cross(ang_vel, lin_vel)
magnus_force *= magnus_lift_coef * fluid_density * volume

# The dot product between velocity and the normal to the cross-section that
# defines the body's projection along velocity is proj_num/sqrt(proj_denom)
proj_denom = (
((size[1] * size[2])**4) * (lin_vel[0]**2) +
((size[2] * size[0])**4) * (lin_vel[1]**2) +
((size[0] * size[1])**4) * (lin_vel[2]**2)
)
proj_num = (
(size[1] * size[2] * lin_vel[0])**2 +
(size[2] * size[0] * lin_vel[1])**2 +
(size[0] * size[1] * lin_vel[2])**2
)
# Projected surface in the direction of the velocity.
A_proj = np.pi * np.sqrt(proj_denom / np.max((mjMINVAL, proj_num)))

# Not-unit normal to ellipsoid's projected area in the direction of velocity.
norm = np.array([
(size[1] * size[2])**2 * lin_vel[0],
(size[2] * size[0])**2 * lin_vel[1],
(size[0] * size[1])**2 * lin_vel[2],
])

# Cosine between velocity and normal to the surface divided by proj_denom
# instead of sqrt(proj_denom) to account for skipped normalization in norm.
cos_alpha = proj_num / np.max((mjMINVAL, np.linalg.norm(lin_vel) * proj_denom))
kutta_circ = np.cross(norm, lin_vel)
kutta_circ *= kutta_lift_coef * fluid_density * cos_alpha * A_proj
kutta_force = np.cross(kutta_circ, lin_vel) # This is f_K.

# Viscous force and torque in Stokes flow, analytical for spherical bodies.
eq_sphere_D = 2.0 / 3.0 * (size[0] + size[1] + size[2])
lin_visc_force_coef = 3.0 * np.pi * eq_sphere_D
lin_visc_torq_coef = np.pi * eq_sphere_D * eq_sphere_D * eq_sphere_D

# Moments of inertia used to compute angular quadratic drag.
I_max = 8.0 / 15.0 * np.pi * d_mid * d_max**4
II = np.array([
mji_ellipsoid_max_moment(size, 0),
mji_ellipsoid_max_moment(size, 1),
mji_ellipsoid_max_moment(size, 2),
])
mom_visc = np.array([
ang_vel[0] * (ang_drag_coef*II[0] + slender_drag_coef*(I_max - II[0])),
ang_vel[1] * (ang_drag_coef*II[1] + slender_drag_coef*(I_max - II[1])),
ang_vel[2] * (ang_drag_coef*II[2] + slender_drag_coef*(I_max - II[2])),
])

# Linear plus quadratic.
drag_lin_coef = (
fluid_viscosity * lin_visc_force_coef +
fluid_density * np.linalg.norm(lin_vel) * (
A_proj * blunt_drag_coef + slender_drag_coef * (A_max - A_proj)
)
)
# Linear plus quadratic.
drag_ang_coef = (
fluid_viscosity * lin_visc_torq_coef + fluid_density * np.linalg.norm(mom_visc)
)

# local_force[:3] is (g_D + g_V).
# local_force[3:] is ... + ... - (f_D + f_V).
local_force[0] -= drag_ang_coef * ang_vel[0]
local_force[1] -= drag_ang_coef * ang_vel[1]
local_force[2] -= drag_ang_coef * ang_vel[2]
local_force[3] += magnus_force[0] + kutta_force[0] - drag_lin_coef*lin_vel[0]
local_force[4] += magnus_force[1] + kutta_force[1] - drag_lin_coef*lin_vel[1]
local_force[5] += magnus_force[2] + kutta_force[2] - drag_lin_coef*lin_vel[2]

# Return results.
fM = magnus_force
fK = kutta_force
fD = - fluid_density * np.linalg.norm(lin_vel) * (
A_proj*blunt_drag_coef + slender_drag_coef*(A_max - A_proj)) * lin_vel
fV = - fluid_viscosity * lin_visc_force_coef * lin_vel
assert np.all(np.isclose(fD + fV, - drag_lin_coef * lin_vel))

gD = - fluid_density * np.linalg.norm(mom_visc) * ang_vel
gV = - fluid_viscosity * lin_visc_torq_coef * ang_vel
assert np.all(np.isclose(gD + gV, - drag_ang_coef * ang_vel))

return fM, fK, fD, fV, gD, gV


def mj_ellipsoidFluidModel(m, d, bodyid):

lvel = np.zeros(6)
lwind = np.zeros(6)
bfrc = np.zeros(6)

_fluid_forces_for_body_geoms = {}

for j in range(m.body_geomnum[bodyid]):
geomid = m.body_geomadr[bodyid] + j

# Results for this geom will be stored here.
_fluid_forces_for_current_geom = {}

semiaxes = m.geom_size[geomid]

# void readFluidGeomInteraction
geom_fluid_coefs = m.geom_fluid[geomid]
geom_interaction_coef = geom_fluid_coefs[0]
blunt_drag_coef = geom_fluid_coefs[1]
slender_drag_coef = geom_fluid_coefs[2]
ang_drag_coef = geom_fluid_coefs[3]
kutta_lift_coef = geom_fluid_coefs[4]
magnus_lift_coef = geom_fluid_coefs[5]
virtual_mass = geom_fluid_coefs[6:9]
virtual_inertia = geom_fluid_coefs[9:12]

if geom_interaction_coef == 0.0:
continue

# Map from CoM-centered to local body-centered 6D velocity.
mujoco.mj_objectVelocity(m.ptr, d.ptr, 5, geomid, lvel, 1)

# Compute wind in local coordinates.
wind = np.zeros(6)
wind[3:] = m.opt.wind
mujoco.mju_transformSpatial(
lwind, wind, 0,
d.geom_xpos[geomid], # Frame of ref's origin.
d.subtree_com[m.body_rootid[bodyid]],
d.geom_xmat[geomid]) # Frame of ref's orientation.
# Subtract translational component from grom velocity.
mujoco.mju_subFrom3(lvel[3:], lwind[3:])

# Initialize viscous force and torque
lfrc = np.zeros(6)

# Added-mass forces and torques.
fA, gA = mj_addedMassForces(
lvel, None, m.opt.density, virtual_mass, virtual_inertia, lfrc)
_fluid_forces_for_current_geom['fA'] = fA
_fluid_forces_for_current_geom['gA'] = gA

# Lift force orthogonal to lvel from Kutta-Joukowski theorem.
fM, fK, fD, fV, gD, gV = mj_viscousForces(
lvel, m.opt.density, m.opt.viscosity, semiaxes, magnus_lift_coef,
kutta_lift_coef, blunt_drag_coef, slender_drag_coef, ang_drag_coef, lfrc)
_fluid_forces_for_current_geom['fM'] = fM
_fluid_forces_for_current_geom['fK'] = fK
_fluid_forces_for_current_geom['fD'] = fD
_fluid_forces_for_current_geom['fV'] = fV
_fluid_forces_for_current_geom['gD'] = gD
_fluid_forces_for_current_geom['gV'] = gV
# Transform all forces and torques to global coordinates and scale
# by geom_interaction_coef.
for k in _fluid_forces_for_current_geom.keys():
vec = _fluid_forces_for_current_geom[k] * geom_interaction_coef
mujoco.mju_mulMatVec3(
_fluid_forces_for_current_geom[k], d.geom_xmat[geomid], vec)

# Scale by geom_interaction_coef (1.0 by default)
# mju_scl(lfrc, lfrc, geom_interaction_coef, 6);
lfrc = lfrc * geom_interaction_coef # This is the same as mju_scl above.

# Rotate to global orientation: lfrc -> bfrc
mujoco.mju_mulMatVec3(bfrc[:3], d.geom_xmat[geomid], lfrc[:3])
mujoco.mju_mulMatVec3(bfrc[3:], d.geom_xmat[geomid], lfrc[3:])

# Sanity check.
total_force = np.zeros(3)
total_torque = np.zeros(3)
for k, v in _fluid_forces_for_current_geom.items():
if 'f' in k:
total_force += v
else:
total_torque += v
assert np.all(np.isclose(total_torque, bfrc[:3]))
assert np.all(np.isclose(total_force, bfrc[3:]))

# Apply force and torque to body com.
mujoco.mj_applyFT(
m.ptr, d.ptr, bfrc[3:], bfrc[:3], # model, data, force, torque
d.geom_xpos[geomid], # Point where FT is generated
bodyid, d.qfrc_fluid)

_fluid_forces_for_body_geoms[geomid] = _fluid_forces_for_current_geom

# Return forces and torques for current body.
return _fluid_forces_for_body_geoms

0 comments on commit 479a2e6

Please sign in to comment.