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:
| Name | Description | Units | Default value |
|---|---|---|---|
add_load | Attach a Cable + suspended Body load to the airframe | – | false |
number_of_links | Number of rigid segments in the cable (only used if add_load = true) | – | 3 |
end_joints | Include Universal joints at both ends of cable(frame_a and frame_b) | – | true |
arm_length | Length of each arm from the body center to the thruster | m | 0.2 |
arm_outer_diameter | Outer diameter of the arm tube | m | 0.03 |
arm_inner_diameter | Inner diameter of the arm tube | m | 0.02 |
arm_density | Density of the arm material [kg/m³] | – | 800 |
body_mass | Mass of the central body | kg | 0.2 |
body_inertia | Diagonal moment of inertia of the central body (used for I_11, I_22, I_33) | kg.m2 | 0.01 |
cable_length | Total length of the suspension cable (only used if add_load = true) | m | 1 |
cable_mass | Total mass of the suspension cable (only used if add_load = true) | kg | 5 |
cable_diameter | Diameter of the cable, used for the rendering radius (only used if add_load = true) | m | 0.01 |
load_mass | Mass of the suspended load (only used if add_load = true) | kg | 0.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
| Name | Description | Units |
|---|---|---|
Ie_alt | Integral of altitude error (state of integral-action augmentation for LQR) | – |
Behavior
Source
"""
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"}
}
]
}
}
endFlattened Source
"""
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"}
}
]
}
}
endTest Cases
No test cases defined.
Related
Examples
Experiments
Analyses