Skip to content
LIBRARY
RotorCraft.md

RotorCraft

Open-loop quadrotor airframe: a central Body with Euler-angle orientation, four BodyCylinder arms evenly spaced in the x-z plane, and a Thruster at the tip of each arm. Alternating thrusters spin clockwise/counter-clockwise so reaction torques cancel under symmetric thrust.

Usage

MultibodyComponents.RotorCraft(arm_length=0.2, arm_outer_diameter=0.03, arm_inner_diameter=0.02, arm_density=800, body_mass=0.2, body_inertia=0.01, cable_length=1, cable_mass=5, cable_diameter=0.01, load_mass=0.1)

Parameters:

NameDescriptionUnitsDefault value
add_loadAttach a Cable + suspended Body load to the airframefalse
number_of_linksNumber of rigid segments in the cable (only used if add_load = true)3
end_jointsInclude Universal joints at both ends of cable(frame_a and frame_b)true
arm_lengthLength of each arm from the body center to the thrusterm0.2
arm_outer_diameterOuter diameter of the arm tubem0.03
arm_inner_diameterInner diameter of the arm tubem0.02
arm_densityDensity of the arm material [kg/m³]800
body_massMass of the central bodykg0.2
body_inertiaDiagonal moment of inertia of the central body (used for I_11, I_22, I_33)kg.m20.01
cable_lengthTotal length of the suspension cable (only used if add_load = true)m1
cable_massTotal mass of the suspension cable (only used if add_load = true)kg5
cable_diameterDiameter of the cable, used for the rendering radius (only used if add_load = true)m0.01
load_massMass of the suspended load (only used if add_load = true)kg0.1

Connectors

  • u - This connector represents a real signal as an input to a component (RealInput)

  • y_pos - This connector represents a real signal as an output from a component (RealOutput)

  • y_v - This connector represents a real signal as an output from a component (RealOutput)

  • y_Ie_alt - This connector represents a real signal as an output from a component (RealOutput)

Variables

NameDescriptionUnits
Ie_altIntegral of altitude error (state of integral-action augmentation for LQR)

Behavior

Source

dyad
"""
Open-loop quadrotor airframe: a central `Body` with Euler-angle orientation,
four `BodyCylinder` arms evenly spaced in the x-z plane, and a `Thruster` at
the tip of each arm. Alternating thrusters spin clockwise/counter-clockwise
so reaction torques cancel under symmetric thrust.
"""
component RotorCraft
  "Thrust commands for thrusters 1..4 (in order: +x, +z, -x, -z arms)"
  u = [RealInput() for i in 1:4] {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": -100, "y1": 450, "x2": 0, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  "Body pose measurements: [altitude, roll, pitch, yaw, forward, sideways]. y_pos[1]=body.r_0[2], y_pos[2]=body.phi[3], y_pos[3]=body.phi[1], y_pos[4]=body.phi[2], y_pos[5]=body.r_0[1], y_pos[6]=body.r_0[3]."
  y_pos = [RealOutput() for i in 1:6] {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 1000, "y1": 160, "x2": 1100, "y2": 260, "rot": 0}
      },
      "tags": []
    }
  }
  "Body velocity measurements in the same component order as `y_pos`: [v_alt, v_roll, v_pitch, v_yaw, v_forward, v_sideways]."
  y_v = [RealOutput() for i in 1:6] {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 1000, "y1": 450, "x2": 1100, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  "Integral of altitude (integral-action state: der(Ie_alt) = y_pos[1])"
  y_Ie_alt = RealOutput() {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 1000, "y1": 740, "x2": 1100, "y2": 840, "rot": 0}
      },
      "tags": []
    }
  }
  body = MultibodyComponents.Body(m = body_mass, I_11 = body_inertia, I_22 = body_inertia, I_33 = body_inertia, orientation_state = OrientationState.Euler(), r_0(initial = [0, 0, 0]), v_0(initial = [0, 0, 0]), statePriority = 100, linearStatePriority = 100) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 500, "y1": 450, "x2": 600, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  arm1 = MultibodyComponents.BodyCylinder(r = [arm_length, 0, 0], diameter = arm_outer_diameter, inner_diameter = arm_inner_diameter, density = arm_density) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 640, "y1": 450, "x2": 740, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  arm2 = MultibodyComponents.BodyCylinder(r = [0, 0, arm_length], diameter = arm_outer_diameter, inner_diameter = arm_inner_diameter, density = arm_density) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 550, "y1": 350, "x2": 450, "y2": 450, "rot": 90}
      },
      "tags": []
    }
  }
  arm3 = MultibodyComponents.BodyCylinder(r = [-arm_length, 0, 0], diameter = arm_outer_diameter, inner_diameter = arm_inner_diameter, density = arm_density) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 410, "y1": 450, "x2": 310, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  arm4 = MultibodyComponents.BodyCylinder(r = [0, 0, -arm_length], diameter = arm_outer_diameter, inner_diameter = arm_inner_diameter, density = arm_density) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 550, "y1": 570, "x2": 450, "y2": 670, "rot": 270}
      },
      "tags": []
    }
  }
  thruster1 = Thruster(clockwise = true) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 920, "y1": 450, "x2": 820, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  thruster2 = Thruster(clockwise = false) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 450, "y1": 200, "x2": 550, "y2": 300, "rot": 90}
      },
      "tags": []
    }
  }
  thruster3 = Thruster(clockwise = true) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 150, "y1": 450, "x2": 250, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  thruster4 = Thruster(clockwise = false) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 450, "y1": 720, "x2": 550, "y2": 820, "rot": 270}
      },
      "tags": []
    }
  }
  u_demux = Demux4() {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 0, "y1": 440, "x2": 100, "y2": 540, "rot": 0}
      },
      "tags": []
    }
  }
  cable = MultibodyComponents.Cable(n = number_of_links, end_joints = end_joints, l = cable_length, m = cable_mass, dir = [0, -1, 0], radius = cable_diameter / 2) if add_load {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 640, "y1": 720, "x2": 740, "y2": 820, "rot": 0}
      },
      "tags": []
    }
  }
  load = MultibodyComponents.Body(m = load_mass, r_cm = [0, 0.01, 0], radius = 0.1) if add_load {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 820, "y1": 720, "x2": 920, "y2": 820, "rot": 0}
      },
      "tags": []
    }
  }
  "Attach a `Cable` + suspended `Body` load to the airframe"
  structural parameter add_load::Boolean = false
  "Number of rigid segments in the cable (only used if add_load = true)"
  structural parameter number_of_links::Integer = 3
  "Include Universal joints at both ends of cable(frame_a and frame_b)"
  structural parameter end_joints::Boolean = true
  "Length of each arm from the body center to the thruster"
  parameter arm_length::Length = 0.2
  "Outer diameter of the arm tube"
  parameter arm_outer_diameter::Length = 0.03
  "Inner diameter of the arm tube"
  parameter arm_inner_diameter::Length = 0.02
  "Density of the arm material [kg/m³]"
  parameter arm_density::Real = 800
  "Mass of the central body"
  parameter body_mass::Mass = 0.2
  "Diagonal moment of inertia of the central body (used for I_11, I_22, I_33)"
  parameter body_inertia::Inertia = 0.01
  "Total length of the suspension cable (only used if add_load = true)"
  parameter cable_length::Length = 1
  "Total mass of the suspension cable (only used if add_load = true)"
  parameter cable_mass::Mass = 5
  "Diameter of the cable, used for the rendering radius (only used if add_load = true)"
  parameter cable_diameter::Length = 0.01
  "Mass of the suspended load (only used if add_load = true)"
  parameter load_mass::Mass = 0.1
  "Integral of altitude error (state of integral-action augmentation for LQR)"
  variable Ie_alt::Real
relations
  y_pos[1] = body.r_0[2]
  y_pos[2] = body.phi[3]
  y_pos[3] = body.phi[1]
  y_pos[4] = body.phi[2]
  y_pos[5] = body.r_0[1]
  y_pos[6] = body.r_0[3]
  y_v[1] = der(body.r_0[2])
  y_v[2] = der(body.phi[3])
  y_v[3] = der(body.phi[1])
  y_v[4] = der(body.phi[2])
  y_v[5] = der(body.r_0[1])
  y_v[6] = der(body.r_0[3])
  der(Ie_alt) = y_pos[1]
  y_Ie_alt = Ie_alt
  connect(arm1.frame_b, thruster1.frame_b)
  connect(arm2.frame_b, thruster2.frame_b)
  connect(arm3.frame_b, thruster3.frame_b)
  connect(arm4.frame_b, thruster4.frame_b)
  if add_load
    connect(load.frame_a, cable.frame_b) {"Dyad": {"renderStyle": "standard", "edges": [{"S": 1, "E": 2, "M": []}]}}
    connect(cable.frame_a, body.frame_a) {
      "Dyad": {
        "renderStyle": "standard",
        "edges": [{"S": 1, "M": [{"x": 557.5, "y": 730}, {"x": 557.5, "y": 500}], "E": 2}]
      }
    }
  end
  connect(arm1.frame_a, body.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}}
  connect(arm2.frame_a, body.frame_a) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 500, "y": 500}], "E": 2}],
      "renderStyle": "standard"
    }
  }
  connect(arm3.frame_a, body.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}}
  connect(arm4.frame_a, body.frame_a) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 500, "y": 500}], "E": 2}],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.u_in, u) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": -50, "y": 490}], "E": 2}],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.y1, thruster1.thrust) {
    "Dyad": {
      "edges": [
        {
          "S": 1,
          "M": [
            {"x": 110, "y": 453},
            {"x": 110, "y": 130},
            {"x": 960, "y": 130},
            {"x": 960, "y": 500.5}
          ],
          "E": 2
        }
      ],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.y3, thruster3.thrust) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 123, "y": 502}, {"x": 123, "y": 500.5}], "E": 2}],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.y4, thruster4.thrust) {
    "Dyad": {
      "edges": [
        {
          "S": 1,
          "M": [{"x": 145, "y": 525}, {"x": 145, "y": 870}, {"x": 500.5, "y": 870}],
          "E": 2
        }
      ],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.y2, thruster2.thrust) {
    "Dyad": {
      "edges": [
        {
          "S": 1,
          "M": [{"x": 130, "y": 479}, {"x": 130, "y": 160}, {"x": 499.5, "y": 160}],
          "E": 2
        }
      ],
      "renderStyle": "standard"
    }
  }
metadata {
  "Dyad": {
    "icons": {"default": "dyad://MultibodyComponents/RotorCraft.svg"},
    "labels": [
      {
        "label": "$(instance)",
        "x": 500,
        "y": 150,
        "rot": 0,
        "attrs": {"font-size": "160"}
      }
    ]
  }
}
end
Flattened Source
dyad
"""
Open-loop quadrotor airframe: a central `Body` with Euler-angle orientation,
four `BodyCylinder` arms evenly spaced in the x-z plane, and a `Thruster` at
the tip of each arm. Alternating thrusters spin clockwise/counter-clockwise
so reaction torques cancel under symmetric thrust.
"""
component RotorCraft
  "Thrust commands for thrusters 1..4 (in order: +x, +z, -x, -z arms)"
  u = [RealInput() for i in 1:4] {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": -100, "y1": 450, "x2": 0, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  "Body pose measurements: [altitude, roll, pitch, yaw, forward, sideways]. y_pos[1]=body.r_0[2], y_pos[2]=body.phi[3], y_pos[3]=body.phi[1], y_pos[4]=body.phi[2], y_pos[5]=body.r_0[1], y_pos[6]=body.r_0[3]."
  y_pos = [RealOutput() for i in 1:6] {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 1000, "y1": 160, "x2": 1100, "y2": 260, "rot": 0}
      },
      "tags": []
    }
  }
  "Body velocity measurements in the same component order as `y_pos`: [v_alt, v_roll, v_pitch, v_yaw, v_forward, v_sideways]."
  y_v = [RealOutput() for i in 1:6] {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 1000, "y1": 450, "x2": 1100, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  "Integral of altitude (integral-action state: der(Ie_alt) = y_pos[1])"
  y_Ie_alt = RealOutput() {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 1000, "y1": 740, "x2": 1100, "y2": 840, "rot": 0}
      },
      "tags": []
    }
  }
  body = MultibodyComponents.Body(m = body_mass, I_11 = body_inertia, I_22 = body_inertia, I_33 = body_inertia, orientation_state = OrientationState.Euler(), r_0(initial = [0, 0, 0]), v_0(initial = [0, 0, 0]), statePriority = 100, linearStatePriority = 100) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 500, "y1": 450, "x2": 600, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  arm1 = MultibodyComponents.BodyCylinder(r = [arm_length, 0, 0], diameter = arm_outer_diameter, inner_diameter = arm_inner_diameter, density = arm_density) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 640, "y1": 450, "x2": 740, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  arm2 = MultibodyComponents.BodyCylinder(r = [0, 0, arm_length], diameter = arm_outer_diameter, inner_diameter = arm_inner_diameter, density = arm_density) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 550, "y1": 350, "x2": 450, "y2": 450, "rot": 90}
      },
      "tags": []
    }
  }
  arm3 = MultibodyComponents.BodyCylinder(r = [-arm_length, 0, 0], diameter = arm_outer_diameter, inner_diameter = arm_inner_diameter, density = arm_density) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 410, "y1": 450, "x2": 310, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  arm4 = MultibodyComponents.BodyCylinder(r = [0, 0, -arm_length], diameter = arm_outer_diameter, inner_diameter = arm_inner_diameter, density = arm_density) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 550, "y1": 570, "x2": 450, "y2": 670, "rot": 270}
      },
      "tags": []
    }
  }
  thruster1 = Thruster(clockwise = true) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 920, "y1": 450, "x2": 820, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  thruster2 = Thruster(clockwise = false) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 450, "y1": 200, "x2": 550, "y2": 300, "rot": 90}
      },
      "tags": []
    }
  }
  thruster3 = Thruster(clockwise = true) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 150, "y1": 450, "x2": 250, "y2": 550, "rot": 0}
      },
      "tags": []
    }
  }
  thruster4 = Thruster(clockwise = false) {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 450, "y1": 720, "x2": 550, "y2": 820, "rot": 270}
      },
      "tags": []
    }
  }
  u_demux = Demux4() {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 0, "y1": 440, "x2": 100, "y2": 540, "rot": 0}
      },
      "tags": []
    }
  }
  cable = MultibodyComponents.Cable(n = number_of_links, end_joints = end_joints, l = cable_length, m = cable_mass, dir = [0, -1, 0], radius = cable_diameter / 2) if add_load {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 640, "y1": 720, "x2": 740, "y2": 820, "rot": 0}
      },
      "tags": []
    }
  }
  load = MultibodyComponents.Body(m = load_mass, r_cm = [0, 0.01, 0], radius = 0.1) if add_load {
    "Dyad": {
      "placement": {
        "diagram": {"iconName": "default", "x1": 820, "y1": 720, "x2": 920, "y2": 820, "rot": 0}
      },
      "tags": []
    }
  }
  "Attach a `Cable` + suspended `Body` load to the airframe"
  structural parameter add_load::Boolean = false
  "Number of rigid segments in the cable (only used if add_load = true)"
  structural parameter number_of_links::Integer = 3
  "Include Universal joints at both ends of cable(frame_a and frame_b)"
  structural parameter end_joints::Boolean = true
  "Length of each arm from the body center to the thruster"
  parameter arm_length::Length = 0.2
  "Outer diameter of the arm tube"
  parameter arm_outer_diameter::Length = 0.03
  "Inner diameter of the arm tube"
  parameter arm_inner_diameter::Length = 0.02
  "Density of the arm material [kg/m³]"
  parameter arm_density::Real = 800
  "Mass of the central body"
  parameter body_mass::Mass = 0.2
  "Diagonal moment of inertia of the central body (used for I_11, I_22, I_33)"
  parameter body_inertia::Inertia = 0.01
  "Total length of the suspension cable (only used if add_load = true)"
  parameter cable_length::Length = 1
  "Total mass of the suspension cable (only used if add_load = true)"
  parameter cable_mass::Mass = 5
  "Diameter of the cable, used for the rendering radius (only used if add_load = true)"
  parameter cable_diameter::Length = 0.01
  "Mass of the suspended load (only used if add_load = true)"
  parameter load_mass::Mass = 0.1
  "Integral of altitude error (state of integral-action augmentation for LQR)"
  variable Ie_alt::Real
relations
  y_pos[1] = body.r_0[2]
  y_pos[2] = body.phi[3]
  y_pos[3] = body.phi[1]
  y_pos[4] = body.phi[2]
  y_pos[5] = body.r_0[1]
  y_pos[6] = body.r_0[3]
  y_v[1] = der(body.r_0[2])
  y_v[2] = der(body.phi[3])
  y_v[3] = der(body.phi[1])
  y_v[4] = der(body.phi[2])
  y_v[5] = der(body.r_0[1])
  y_v[6] = der(body.r_0[3])
  der(Ie_alt) = y_pos[1]
  y_Ie_alt = Ie_alt
  connect(arm1.frame_b, thruster1.frame_b)
  connect(arm2.frame_b, thruster2.frame_b)
  connect(arm3.frame_b, thruster3.frame_b)
  connect(arm4.frame_b, thruster4.frame_b)
  if add_load
    connect(load.frame_a, cable.frame_b) {"Dyad": {"renderStyle": "standard", "edges": [{"S": 1, "E": 2, "M": []}]}}
    connect(cable.frame_a, body.frame_a) {
      "Dyad": {
        "renderStyle": "standard",
        "edges": [{"S": 1, "M": [{"x": 557.5, "y": 730}, {"x": 557.5, "y": 500}], "E": 2}]
      }
    }
  end
  connect(arm1.frame_a, body.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}}
  connect(arm2.frame_a, body.frame_a) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 500, "y": 500}], "E": 2}],
      "renderStyle": "standard"
    }
  }
  connect(arm3.frame_a, body.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}}
  connect(arm4.frame_a, body.frame_a) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 500, "y": 500}], "E": 2}],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.u_in, u) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": -50, "y": 490}], "E": 2}],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.y1, thruster1.thrust) {
    "Dyad": {
      "edges": [
        {
          "S": 1,
          "M": [
            {"x": 110, "y": 453},
            {"x": 110, "y": 130},
            {"x": 960, "y": 130},
            {"x": 960, "y": 500.5}
          ],
          "E": 2
        }
      ],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.y3, thruster3.thrust) {
    "Dyad": {
      "edges": [{"S": 1, "M": [{"x": 123, "y": 502}, {"x": 123, "y": 500.5}], "E": 2}],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.y4, thruster4.thrust) {
    "Dyad": {
      "edges": [
        {
          "S": 1,
          "M": [{"x": 145, "y": 525}, {"x": 145, "y": 870}, {"x": 500.5, "y": 870}],
          "E": 2
        }
      ],
      "renderStyle": "standard"
    }
  }
  connect(u_demux.y2, thruster2.thrust) {
    "Dyad": {
      "edges": [
        {
          "S": 1,
          "M": [{"x": 130, "y": 479}, {"x": 130, "y": 160}, {"x": 499.5, "y": 160}],
          "E": 2
        }
      ],
      "renderStyle": "standard"
    }
  }
metadata {
  "Dyad": {
    "icons": {"default": "dyad://MultibodyComponents/RotorCraft.svg"},
    "labels": [
      {
        "label": "$(instance)",
        "x": 500,
        "y": 150,
        "rot": 0,
        "attrs": {"font-size": "160"}
      }
    ]
  }
}
end


Test Cases

No test cases defined.

  • Examples

  • Experiments

  • Analyses