Skip to content
LIBRARY
RollingWheelJoint.md

RollingWheelJoint

Joint (no mass, no inertia) for an ideal rolling wheel on the flat plane y = 0.

The rolling contact is considered ideal: no slip between the wheel and the ground. This is enforced by two non-holonomic velocity-level constraints (longitudinal and lateral) plus one holonomic position-level constraint (the wheel stays in contact with the ground).

The origin of frame_a is placed at the intersection of the wheel spin axis with the wheel midplane and rotates with the wheel. The z-axis of frame_a is the wheel spin axis. A wheel body collecting the mass and inertia should be connected to frame_a.

For a wheel with mass and inertia attached, see RollingWheel.

This component extends from Renderable

Usage

MultibodyComponents.RollingWheelJoint(render=true, color=[1, 0, 0, 1], specular_coefficient=1.5, radius=0.3, width=0.035)

Parameters:

NameDescriptionUnitsDefault value
sequenceRotation axis sequence for the Cardan angles that orient frame_a (default y, z, x)[2, 3, 1]
rendertrue
color[1, 0, 0, 1]
specular_coefficient1.5
radiusRadius of the wheelm0.3
widthWidth of the wheel in animations0.035

Connectors

  • frame_a - Frame3D is the fundamental 3D connector used for 6DOF motion. Most components have one or several Frame

connectors that can be connected together (Frame3D)

Variables

NameDescriptionUnits
xx-position of the wheel axism
yy-position of the wheel axism
zz-position of the wheel axism
anglesAngles that rotate the world frame into frame_a around the y-, z-, x-axisrad
der_anglesTime derivatives of anglesrad/s
r_road_0Position vector from world frame to contact point on the road, resolved in world framem
f_wheel_0Force vector on the wheel at the contact point, resolved in world frame
f_nContact force acting on the wheel in the normal directionN
f_latContact force acting on the wheel in the lateral directionN
f_longContact force acting on the wheel in the longitudinal directionN
e_axis_0Unit vector along the wheel axis, resolved in world frame
delta_0Distance vector from wheel center to contact point, resolved in world frame
e_n_0Unit vector normal to the road at the contact point, resolved in world frame
e_lat_0Unit vector in the lateral direction of the road at the contact point, resolved in world frame
e_long_0Unit vector in the longitudinal direction of the road at the contact point, resolved in world frame
e_s_0Road heading at (s, w), resolved in world frame (unit vector)
sRoad surface parameter 1
wRoad surface parameter 2
v_0Velocity of wheel center, resolved in world framem/s
w_0Angular velocity of the wheel, resolved in world framerad/s
vContact_0Velocity of the contact point, resolved in world framem/s
auxAuxiliary vector for building the contact frame

Behavior

Source

dyad
"""
Joint (no mass, no inertia) for an ideal rolling wheel on the flat plane y = 0.

The rolling contact is considered ideal: no slip between the wheel and the
ground. This is enforced by two non-holonomic velocity-level constraints
(longitudinal and lateral) plus one holonomic position-level constraint
(the wheel stays in contact with the ground).

The origin of `frame_a` is placed at the intersection of the wheel spin axis
with the wheel midplane and rotates with the wheel. The z-axis of `frame_a`
is the wheel spin axis. A wheel body collecting the mass and inertia should
be connected to `frame_a`.

For a wheel with mass and inertia attached, see `RollingWheel`.
"""
component RollingWheelJoint
  extends Renderable(color = [1, 0, 0, 1])
  frame_a = Frame3D() {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 140, "y1": 20, "x2": 240, "y2": 120, "rot": 0}
      },
      "tags": []
    }
  }
  # Tire visualization: cylinder centered on frame_a with axis along z
  tire_shape = CylinderShape(render = render, color = color, r = frame_a.r_0, R = transpose(frame_a.R), r_shape = [0, 0, -width / 2], length_direction = [0, 0, 1], length = width, width = 2 * radius, height = 2 * radius) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 20, "y1": 20, "x2": 120, "y2": 120, "rot": 0}
      },
      "tags": []
    }
  }
  "Rim cross-cylinder 1: thin spoke spanning the wheel diameter; rotates with the wheel to make the spin visible"
  rim1_shape = CylinderShape(render = render, color = [0.8, 0.8, 0.8, 1.0], r = frame_a.r_0, R = transpose(frame_a.R), r_shape = [-radius, 0, 0], length_direction = [1, 0, 0], length = 2 * radius, width = radius / 3, height = radius / 3)
  "Rim cross-cylinder 2: second spoke, perpendicular to the first"
  rim2_shape = CylinderShape(render = render, color = [0.8, 0.8, 0.8, 1.0], r = frame_a.r_0, R = transpose(frame_a.R), r_shape = [0, -radius, 0], length_direction = [0, 1, 0], length = 2 * radius, width = radius / 3, height = radius / 3)
  "Rotation axis sequence for the Cardan angles that orient `frame_a` (default y, z, x)"
  structural parameter sequence::Integer[3] = [2, 3, 1]
  "Radius of the wheel"
  parameter radius::Length = 0.3
  "Width of the wheel in animations"
  parameter width::Real = 0.035
  "x-position of the wheel axis"
  variable x::Length(statePriority = 20)
  "y-position of the wheel axis"
  variable y::Length(statePriority = 0)
  "z-position of the wheel axis"
  variable z::Length(statePriority = 20)
  "Angles that rotate the world frame into `frame_a` around the y-, z-, x-axis"
  variable angles::Angle(statePriority = 30)[3]
  "Time derivatives of `angles`"
  variable der_angles::AngularVelocity(statePriority = 30)[3]
  "Position vector from world frame to contact point on the road, resolved in world frame"
  variable r_road_0::Position[3]
  "Force vector on the wheel at the contact point, resolved in world frame"
  variable f_wheel_0::Real[3]
  "Contact force acting on the wheel in the normal direction"
  variable f_n::Dyad.Force
  "Contact force acting on the wheel in the lateral direction"
  variable f_lat::Dyad.Force
  "Contact force acting on the wheel in the longitudinal direction"
  variable f_long::Dyad.Force
  "Unit vector along the wheel axis, resolved in world frame"
  variable e_axis_0::Real[3]
  "Distance vector from wheel center to contact point, resolved in world frame"
  variable delta_0::Real[3]
  "Unit vector normal to the road at the contact point, resolved in world frame"
  variable e_n_0::Real[3]
  "Unit vector in the lateral direction of the road at the contact point, resolved in world frame"
  variable e_lat_0::Real[3]
  "Unit vector in the longitudinal direction of the road at the contact point, resolved in world frame"
  variable e_long_0::Real[3]
  "Road heading at (s, w), resolved in world frame (unit vector)"
  variable e_s_0::Real[3]
  "Road surface parameter 1"
  variable s::Real
  "Road surface parameter 2"
  variable w::Real
  "Velocity of wheel center, resolved in world frame"
  variable v_0::Velocity[3]
  "Angular velocity of the wheel, resolved in world frame"
  variable w_0::AngularVelocity[3]
  "Velocity of the contact point, resolved in world frame"
  variable vContact_0::Velocity[3]
  "Auxiliary vector for building the contact frame"
  variable aux::Real[3]
relations
  # Solver guesses for algebraic intermediates (mirrors Multibody.jl defaults)
  guess x = 0
  guess y = radius
  guess z = 0
  guess angles = [0, 0, 0]
  guess der_angles = [0, 0, 0]
  guess delta_0 = [0, -radius, 0]
  guess e_n_0 = [0, 1, 0]
  guess aux = [1, 0, 0]
  # Flat road description
  r_road_0 = [s, 0, w]
  e_n_0 = [0, 1, 0]
  e_s_0 = [1, 0, 0]
  # Pose
  frame_a.r_0 = [x, y, z]
  RotationMatrix(frame_a.R) = axes_rotations(sequence, angles, der_angles)
  der_angles = der(angles)
  # Contact-point coordinate frame (e_long_0, e_lat_0, e_n_0)
  e_axis_0 = resolve1(frame_a.R, [0, 0, 1])
  assert(abs(dot(e_n_0, e_axis_0)) < 0.99, "Wheel lays nearly on the ground (which is a singularity)")
  aux = cross(e_n_0, e_axis_0)
  e_long_0 = aux / norm_(aux)
  e_lat_0 = cross(e_long_0, e_n_0)
  # Contact-point geometry
  delta_0 = r_road_0 - frame_a.r_0
  0 = dot(delta_0, e_axis_0)
  0 = dot(delta_0, e_long_0)
  # Holonomic constraint: wheel touches road, no penetration
  0 = radius - dot(delta_0, cross(e_long_0, e_axis_0))
  # Velocities — derive angular velocity directly from der_angles via axes_rotations
  # (mirrors Multibody.jl's explicit `Ra.w ~ Rarot.w`, avoids state selection picking D(R)-based states)
  v_0 = der(frame_a.r_0)
  w_0 = resolve1(frame_a.R, angular_velocity2(axes_rotations(sequence, angles, der_angles)))
  vContact_0 = v_0 + cross(w_0, delta_0)
  # Non-holonomic constraints: ideal rolling, no slip
  0 = dot(vContact_0, e_long_0)
  0 = dot(vContact_0, e_lat_0)
  # Contact force decomposition
  f_wheel_0 = f_n * e_n_0 + f_lat * e_lat_0 + f_long * e_long_0
  # Force and torque balance at the wheel center
  [0, 0, 0] = frame_a.f + resolve2(frame_a.R, f_wheel_0)
  [0, 0, 0] = frame_a.tau + resolve2(frame_a.R, cross(delta_0, f_wheel_0))
metadata {
  "Dyad": {
    "icons": {"default": "dyad://MultibodyComponents/RollingWheelJoint.svg"},
    "labels": [
      {
        "label": "$(instance)",
        "x": 500,
        "y": 200,
        "rot": 0,
        "attrs": {"font-size": "160"}
      }
    ]
  }
}
end
Flattened Source
dyad
"""
Joint (no mass, no inertia) for an ideal rolling wheel on the flat plane y = 0.

The rolling contact is considered ideal: no slip between the wheel and the
ground. This is enforced by two non-holonomic velocity-level constraints
(longitudinal and lateral) plus one holonomic position-level constraint
(the wheel stays in contact with the ground).

The origin of `frame_a` is placed at the intersection of the wheel spin axis
with the wheel midplane and rotates with the wheel. The z-axis of `frame_a`
is the wheel spin axis. A wheel body collecting the mass and inertia should
be connected to `frame_a`.

For a wheel with mass and inertia attached, see `RollingWheel`.
"""
component RollingWheelJoint
  parameter render::Boolean = true
  parameter color::Real[4] = [0.5, 0.5, 0.5, 1.0]
  parameter specular_coefficient::Real = 1.5
  frame_a = Frame3D() {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 140, "y1": 20, "x2": 240, "y2": 120, "rot": 0}
      },
      "tags": []
    }
  }
  # Tire visualization: cylinder centered on frame_a with axis along z
  tire_shape = CylinderShape(render = render, color = color, r = frame_a.r_0, R = transpose(frame_a.R), r_shape = [0, 0, -width / 2], length_direction = [0, 0, 1], length = width, width = 2 * radius, height = 2 * radius) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 20, "y1": 20, "x2": 120, "y2": 120, "rot": 0}
      },
      "tags": []
    }
  }
  "Rim cross-cylinder 1: thin spoke spanning the wheel diameter; rotates with the wheel to make the spin visible"
  rim1_shape = CylinderShape(render = render, color = [0.8, 0.8, 0.8, 1.0], r = frame_a.r_0, R = transpose(frame_a.R), r_shape = [-radius, 0, 0], length_direction = [1, 0, 0], length = 2 * radius, width = radius / 3, height = radius / 3)
  "Rim cross-cylinder 2: second spoke, perpendicular to the first"
  rim2_shape = CylinderShape(render = render, color = [0.8, 0.8, 0.8, 1.0], r = frame_a.r_0, R = transpose(frame_a.R), r_shape = [0, -radius, 0], length_direction = [0, 1, 0], length = 2 * radius, width = radius / 3, height = radius / 3)
  "Rotation axis sequence for the Cardan angles that orient `frame_a` (default y, z, x)"
  structural parameter sequence::Integer[3] = [2, 3, 1]
  "Radius of the wheel"
  parameter radius::Length = 0.3
  "Width of the wheel in animations"
  parameter width::Real = 0.035
  "x-position of the wheel axis"
  variable x::Length(statePriority = 20)
  "y-position of the wheel axis"
  variable y::Length(statePriority = 0)
  "z-position of the wheel axis"
  variable z::Length(statePriority = 20)
  "Angles that rotate the world frame into `frame_a` around the y-, z-, x-axis"
  variable angles::Angle(statePriority = 30)[3]
  "Time derivatives of `angles`"
  variable der_angles::AngularVelocity(statePriority = 30)[3]
  "Position vector from world frame to contact point on the road, resolved in world frame"
  variable r_road_0::Position[3]
  "Force vector on the wheel at the contact point, resolved in world frame"
  variable f_wheel_0::Real[3]
  "Contact force acting on the wheel in the normal direction"
  variable f_n::Dyad.Force
  "Contact force acting on the wheel in the lateral direction"
  variable f_lat::Dyad.Force
  "Contact force acting on the wheel in the longitudinal direction"
  variable f_long::Dyad.Force
  "Unit vector along the wheel axis, resolved in world frame"
  variable e_axis_0::Real[3]
  "Distance vector from wheel center to contact point, resolved in world frame"
  variable delta_0::Real[3]
  "Unit vector normal to the road at the contact point, resolved in world frame"
  variable e_n_0::Real[3]
  "Unit vector in the lateral direction of the road at the contact point, resolved in world frame"
  variable e_lat_0::Real[3]
  "Unit vector in the longitudinal direction of the road at the contact point, resolved in world frame"
  variable e_long_0::Real[3]
  "Road heading at (s, w), resolved in world frame (unit vector)"
  variable e_s_0::Real[3]
  "Road surface parameter 1"
  variable s::Real
  "Road surface parameter 2"
  variable w::Real
  "Velocity of wheel center, resolved in world frame"
  variable v_0::Velocity[3]
  "Angular velocity of the wheel, resolved in world frame"
  variable w_0::AngularVelocity[3]
  "Velocity of the contact point, resolved in world frame"
  variable vContact_0::Velocity[3]
  "Auxiliary vector for building the contact frame"
  variable aux::Real[3]
relations
  # Solver guesses for algebraic intermediates (mirrors Multibody.jl defaults)
  guess x = 0
  guess y = radius
  guess z = 0
  guess angles = [0, 0, 0]
  guess der_angles = [0, 0, 0]
  guess delta_0 = [0, -radius, 0]
  guess e_n_0 = [0, 1, 0]
  guess aux = [1, 0, 0]
  # Flat road description
  r_road_0 = [s, 0, w]
  e_n_0 = [0, 1, 0]
  e_s_0 = [1, 0, 0]
  # Pose
  frame_a.r_0 = [x, y, z]
  RotationMatrix(frame_a.R) = axes_rotations(sequence, angles, der_angles)
  der_angles = der(angles)
  # Contact-point coordinate frame (e_long_0, e_lat_0, e_n_0)
  e_axis_0 = resolve1(frame_a.R, [0, 0, 1])
  assert(abs(dot(e_n_0, e_axis_0)) < 0.99, "Wheel lays nearly on the ground (which is a singularity)")
  aux = cross(e_n_0, e_axis_0)
  e_long_0 = aux / norm_(aux)
  e_lat_0 = cross(e_long_0, e_n_0)
  # Contact-point geometry
  delta_0 = r_road_0 - frame_a.r_0
  0 = dot(delta_0, e_axis_0)
  0 = dot(delta_0, e_long_0)
  # Holonomic constraint: wheel touches road, no penetration
  0 = radius - dot(delta_0, cross(e_long_0, e_axis_0))
  # Velocities — derive angular velocity directly from der_angles via axes_rotations
  # (mirrors Multibody.jl's explicit `Ra.w ~ Rarot.w`, avoids state selection picking D(R)-based states)
  v_0 = der(frame_a.r_0)
  w_0 = resolve1(frame_a.R, angular_velocity2(axes_rotations(sequence, angles, der_angles)))
  vContact_0 = v_0 + cross(w_0, delta_0)
  # Non-holonomic constraints: ideal rolling, no slip
  0 = dot(vContact_0, e_long_0)
  0 = dot(vContact_0, e_lat_0)
  # Contact force decomposition
  f_wheel_0 = f_n * e_n_0 + f_lat * e_lat_0 + f_long * e_long_0
  # Force and torque balance at the wheel center
  [0, 0, 0] = frame_a.f + resolve2(frame_a.R, f_wheel_0)
  [0, 0, 0] = frame_a.tau + resolve2(frame_a.R, cross(delta_0, f_wheel_0))
metadata {
  "Dyad": {
    "icons": {"default": "dyad://MultibodyComponents/RollingWheelJoint.svg"},
    "labels": [
      {
        "label": "$(instance)",
        "x": 500,
        "y": 200,
        "rot": 0,
        "attrs": {"font-size": "160"}
      }
    ]
  }
}
end


Test Cases

No test cases defined.

  • Examples

  • Experiments

  • Analyses