PlanarMechanics.examples.excavator.Excavator
Planar excavator digging through soil with hydraulically actuated boom, stick, and bucket.
The machine stands 5 m tall with the arm extended straight up. Three revolute joints connect the boom, stick, and bucket links. Each joint is driven by a ControlledCylinder — a self-contained actuator bundling a proportional valve, a double-acting cylinder, flange sensors, and a position-velocity-force CascadeServoController — whose flange acts on the linkage through an AnchoredCylinderMount that places the cylinder ends on the adjacent links.
A KinematicPTPBoundedJerk trajectory generator issues per-axis position, velocity, acceleration, and jerk references; one TrajectorySelector per axis picks that axis's references out of the vector-valued trajectory outputs and feeds them to the corresponding actuator. The dig cycle lowers the bucket into the soil ahead of the machine, drags it towards the machine, curls it, and lifts it out. While the bucket tip is below the soil surface, the SoilContact component applies penetration and cutting resistance forces, and the swept soil accumulates as a payload mass whose weight remains on the bucket after it leaves the ground.
The machine is rendered with one detailed mesh per rigid body (chassis, boom, stick, bucket), attached to the corresponding frame through a PlanarToMultiBody adapter and a ShapefileVisualizer. The tracks are a separate mesh sharing the chassis frame so they can be colored gray while the chassis body stays orange (STL meshes carry no color information of their own). Each body shares a uniform mesh_scale and carries a 4x4 placement transform (T_chassis/T_boom/T_stick/T_bucket) that seats its mesh on the link. The meshes are body-grouped exports of the excavator CAD assembly, derived from the "DIY RC Excavator" model (https://www.printables.com/model/833291-diy-rc-excavator/files), licensed CC BY 4.0.
Usage
MultibodyComponents.PlanarMechanics.examples.excavator.Excavator(main_color=[0.9, 0.6, 0.1, 1.0], mesh_scale=0.388810, T_chassis=[[-1.0, 0.0, 0.0, 142.98847], [0.0, 0.0, 1.0, 0.021192], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]], T_boom=[[-0.850327, 0.0, 0.526254, 121.08507], [0.526254, 0.0, 0.850327, -76.05823], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]], T_stick=[[-0.763897, 0.0, -0.645339, 109.22320], [-0.645339, 0.0, 0.763897, 89.64590], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]], T_bucket=[[-0.446973, 0.0, -0.894548, 63.56536], [-0.894548, 0.0, 0.446973, 124.89768], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]], pivot_height=0.9742, L_boom=2.0, L_stick=1.5020, L_bucket=0.6454, m_boom=300, m_stick=150, m_bucket=100, q1_0=deg2rad(30.237), q2_0=deg2rad(-84.449), q3_0=deg2rad(-40.788), p_pump=1.6e7, p_tank=1e5, r_boom_base=[0.1621, 0.8960], r_boom_rod=[0.7703, 0.1904], r_stick_base=[1.0965, 0.2446], r_stick_rod=[0.0599, -0.0450], r_bucket_base=[0.4353, 0.1855], r_bucket_rod=[-0.26311, 0.09577], s_off_boom=0.4055, s_off_stick=0.7592, s_off_bucket=0.87, stroke_boom=0.70, stroke_stick=0.35, stroke_bucket=0.50, s_wp_boom=[0.3453, 0.3233, 0.3353, 0.3353, 0.3668], s_wp_stick=[0.1526, 0.1699, 0.1393, 0.14, 0.141], s_wp_bucket=[0.0619, 0.1291, 0.1364, 0.3380, 0.4522])
Parameters:
| Name | Description | Units | Default value |
|---|---|---|---|
main_color | – | [0.9, 0.6, 0.1, 1.0] | |
mesh_scale | Uniform scale applied to every mesh asset (DWG assembly units to meters) | – | 0.388810 |
T_chassis | Placement of the static chassis mesh group (mesh-local to chassis frame) | – | [-1.0 0.0 0....0 0.0 1.0] |
T_boom | Placement of the boom mesh group (mesh-local to boom frame) | – | [-0.850327 ....0 0.0 1.0] |
T_stick | Placement of the stick mesh group (mesh-local to stick frame) | – | [-0.763897 ....0 0.0 1.0] |
T_bucket | Placement of the bucket mesh group (mesh-local to bucket frame) | – | [-0.446973 ....0 0.0 1.0] |
pivot_height | Height of the boom pivot above the ground | m | 0.9742 |
L_boom | Length of the boom link | m | 2.0 |
L_stick | Length of the stick link | m | 1.5020 |
L_bucket | Length of the bucket link | m | 0.6454 |
m_boom | Mass of the boom link | kg | 300 |
m_stick | Mass of the stick link | kg | 150 |
m_bucket | Mass of the bucket | kg | 100 |
q1_0 | Initial boom joint angle | rad | deg2rad(30.237) |
q2_0 | Initial stick joint angle | rad | deg2rad(-84.449) |
q3_0 | Initial bucket joint angle | rad | deg2rad(-40.788) |
p_pump | Supply pressure | – | 1.6e7 |
p_tank | Tank pressure | – | 1e5 |
r_boom_base | Boom cylinder base anchor on the chassis, world frame [forward, up] (m) | m | [0.1621, 0.8960] |
r_boom_rod | Boom cylinder rod anchor on the boom, boom frame [along, perp] (m) | m | [0.7703, 0.1904] |
r_stick_base | Stick cylinder base anchor on the boom, boom frame [along, perp] (m) | m | [1.0965, 0.2446] |
r_stick_rod | Stick cylinder rod anchor on the stick, stick frame [along, perp] (m) | m | [0.0599, -0.0450] |
r_bucket_base | Bucket cylinder base anchor on the stick, stick frame [along, perp] (m) | m | [0.4353, 0.1855] |
r_bucket_rod | Bucket cylinder rod anchor on the bucket crank, bucket frame [along, perp] (m) | m | [-0.26311, 0.09577] |
s_off_boom | Boom cylinder anchor distance at zero piston position | m | 0.4055 |
s_off_stick | Stick cylinder anchor distance at zero piston position | m | 0.7592 |
s_off_bucket | Bucket cylinder anchor distance at zero piston position | m | 0.87 |
stroke_boom | Stroke of the boom cylinder | m | 0.70 |
stroke_stick | Stroke of the stick cylinder | m | 0.35 |
stroke_bucket | Stroke of the bucket cylinder | m | 0.50 |
s_wp_boom | Boom cylinder piston-position waypoints for the dig trajectory | m | [0.3453, 0....53, 0.3668] |
s_wp_stick | Stick cylinder piston-position waypoints for the dig trajectory | m | [0.1526, 0.....14, 0.141] |
s_wp_bucket | Bucket cylinder piston-position waypoints for the dig trajectory | m | [0.0619, 0....80, 0.4522] |
Behavior
Behavior of this component cannot be rendered because it includes path variables.
Source
"""
Planar excavator digging through soil with hydraulically actuated boom, stick,
and bucket.
The machine stands 5 m tall with the arm extended straight up. Three revolute
joints connect the boom, stick, and bucket links. Each joint is driven by a
`ControlledCylinder` — a self-contained actuator bundling a proportional valve,
a double-acting cylinder, flange sensors, and a position-velocity-force
`CascadeServoController` — whose flange acts on the linkage through an
`AnchoredCylinderMount` that places the cylinder ends on the adjacent links.
A `KinematicPTPBoundedJerk` trajectory generator issues per-axis position,
velocity, acceleration, and jerk references; one `TrajectorySelector` per axis
picks that axis's references out of the vector-valued trajectory outputs and
feeds them to the corresponding actuator. The dig cycle lowers the bucket into
the soil ahead of the machine, drags it towards the machine, curls it, and
lifts it out. While the bucket tip is below the soil surface, the `SoilContact`
component applies penetration and cutting resistance forces, and the swept soil
accumulates as a payload mass whose weight remains on the bucket after it
leaves the ground.
The machine is rendered with one detailed mesh per rigid body (chassis, boom,
stick, bucket), attached to the corresponding frame through a `PlanarToMultiBody`
adapter and a `ShapefileVisualizer`. The tracks are a separate mesh sharing the
chassis frame so they can be colored gray while the chassis body stays orange
(STL meshes carry no color information of their own). Each body shares a uniform `mesh_scale` and
carries a 4x4 placement transform (`T_chassis`/`T_boom`/`T_stick`/`T_bucket`) that
seats its mesh on the link. The meshes are body-grouped exports of the excavator
CAD assembly, derived from the "DIY RC Excavator" model
(https://www.printables.com/model/833291-diy-rc-excavator/files), licensed CC BY 4.0.
"""
example component Excavator
world = MultibodyComponents.PlanarMechanics.World(render_ground = false, nominal_length = 2, default_joint_color = main_color, default_body_color = main_color, default_rod_color = main_color) {
"Dyad": {
"placement": {"diagram": {"x1": 1440, "y1": -10, "x2": 1340, "y2": 90, "rot": 270}}
}
}
# --- chassis (rigidly fixed to the world, masses are cosmetic) ---
track_mount = MultibodyComponents.PlanarMechanics.FixedTranslation(r = [-1.1, 0.3], render = false) {
"Dyad": {"placement": {"diagram": {"x1": 1380, "y1": 250, "x2": 1480, "y2": 350}}}
}
track = MultibodyComponents.PlanarMechanics.BodyShape(r = [2.2, 0], m = 2500, I = 1000, radius = 0.3, render = false) {
"Dyad": {"placement": {"diagram": {"x1": 1490, "y1": 190, "x2": 1710, "y2": 410}}}
}
cab = MultibodyComponents.PlanarMechanics.BodyShape(r = [0, 0.9], m = 500, I = 200, render = false) {
"Dyad": {"placement": {"diagram": {"x1": 1490, "y1": 40, "x2": 1710, "y2": 260}}}
}
"Rigid post from the world origin to the boom pivot"
pedestal = MultibodyComponents.PlanarMechanics.FixedTranslation(r = [0, pivot_height], render = false) {
"Dyad": {"placement": {"diagram": {"x1": 1340, "y1": 190, "x2": 1240, "y2": 290}}}
}
# --- mesh visuals: detailed STL model grouped by rigid body ---
adapter_chassis = MultibodyComponents.PlanarMechanics.PlanarToMultiBody() {
"Dyad": {"placement": {"diagram": {"x1": 1500, "y1": 330, "x2": 1600, "y2": 430}}}
}
adapter_boom = MultibodyComponents.PlanarMechanics.PlanarToMultiBody() {
"Dyad": {"placement": {"diagram": {"x1": 1170, "x2": 1270, "y1": 310, "y2": 410}}}
}
adapter_stick = MultibodyComponents.PlanarMechanics.PlanarToMultiBody() {
"Dyad": {"placement": {"diagram": {"x1": 1190, "x2": 1290, "y1": 630, "y2": 730}}}
}
adapter_bucket = MultibodyComponents.PlanarMechanics.PlanarToMultiBody() {
"Dyad": {"placement": {"diagram": {"x1": 1200, "y1": 940, "x2": 1300, "y2": 1040}}}
}
vis_chassis = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_chassis.stl"), color = main_color, shape_scale = mesh_scale, shape_transform = T_chassis) {
"Dyad": {"placement": {"diagram": {"x1": 1590, "x2": 1690, "y1": 330, "y2": 430}}}
}
vis_tracks = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_tracks.stl"), color = [0.35, 0.35, 0.38, 1.0], shape_scale = mesh_scale, shape_transform = T_chassis) {
"Dyad": {"placement": {"diagram": {"x1": 1590, "x2": 1690, "y1": 410, "y2": 510}}}
}
vis_boom = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_boom.stl"), color = main_color, shape_scale = mesh_scale, shape_transform = T_boom) {
"Dyad": {"placement": {"diagram": {"x1": 1260, "x2": 1360, "y1": 310, "y2": 410}}}
}
vis_stick = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_stick.stl"), color = main_color, shape_scale = mesh_scale, shape_transform = T_stick) {
"Dyad": {"placement": {"diagram": {"x1": 1280, "x2": 1380, "y1": 630, "y2": 730}}}
}
vis_bucket = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_bucket.stl"), color = [0.5, 0.5, 0.55, 1.0], shape_scale = mesh_scale, shape_transform = T_bucket, render = true) {
"Dyad": {"placement": {"diagram": {"x1": 1290, "x2": 1390, "y1": 940, "y2": 1040}}}
}
# --- boom / stick / bucket linkage ---
j_boom = MultibodyComponents.PlanarMechanics.Revolute(phi(initial = q1_0), w(initial = 0)) {
"Dyad": {"placement": {"diagram": {"x1": 1230, "y1": 190, "x2": 1130, "y2": 290}}}
}
boom = MultibodyComponents.PlanarMechanics.BodyShape(r = [L_boom, 0], m = m_boom, I = m_boom * L_boom ^ 2 / 12, radius = 0.09, render = false) {
"Dyad": {
"placement": {"diagram": {"x1": 1190, "y1": 350, "x2": 1090, "y2": 450, "rot": 270}}
}
}
j_stick = MultibodyComponents.PlanarMechanics.Revolute(phi(initial = q2_0), w(initial = 0)) {
"Dyad": {
"placement": {"diagram": {"x1": 1090, "y1": 500, "x2": 1190, "y2": 600, "rot": 90}}
}
}
stick = MultibodyComponents.PlanarMechanics.BodyShape(r = [L_stick, 0], m = m_stick, I = m_stick * L_stick ^ 2 / 12, radius = 0.07, render = false) {
"Dyad": {
"placement": {"diagram": {"x1": 1090, "y1": 670, "x2": 1190, "y2": 770, "rot": 90}}
}
}
j_bucket = MultibodyComponents.PlanarMechanics.Revolute(phi(initial = q3_0), w(initial = 0)) {
"Dyad": {
"placement": {"diagram": {"x1": 1090, "y1": 810, "x2": 1190, "y2": 910, "rot": 90}}
}
}
bucket = MultibodyComponents.PlanarMechanics.BodyShape(r = [L_bucket, 0], m = m_bucket, I = m_bucket * L_bucket ^ 2 / 12, render = false) {
"Dyad": {
"placement": {"diagram": {"x1": 1090, "y1": 980, "x2": 1190, "y2": 1080, "rot": 90}}
}
}
# --- hydraulic supply ---
pump = HydraulicComponents.Sources.BoundaryPressure() {
"Dyad": {"placement": {"diagram": {"x1": 480, "y1": -100, "x2": 580, "y2": 0}}}
}
tank = HydraulicComponents.Sources.BoundaryPressure() {
"Dyad": {"placement": {"diagram": {"x1": 750, "y1": -100, "x2": 650, "y2": 0}}}
}
c_pump = BlockComponents.Sources.Constant(k = p_pump) {
"Dyad": {"placement": {"diagram": {"x1": 330, "y1": -100, "x2": 430, "y2": 0}}}
}
c_tank = BlockComponents.Sources.Constant(k = p_tank) {
"Dyad": {"placement": {"diagram": {"x1": 900, "y1": -100, "x2": 800, "y2": 0}}}
}
# --- boom actuator, mount, and reference selector ---
cc_boom = ControlledCylinder(Kv = 9e-7, stroke = stroke_boom, M = 20, area_a = 6.36e-3, area_b = 4.40e-3, p_a0 = 2.4e6, p_b0 = 5e5, k_pos = 6, v_max = 0.5, k_vel = 18.75, Ti_vel = 1.5, f_max = 8e4, ka_ff = 5e4, k_force = 0.08, Ti_force = 0.15) {
"Dyad": {
"placement": {
"diagram": {"iconName": "default", "x1": 360, "y1": 130, "x2": 560, "y2": 330, "rot": 0}
},
"tags": []
}
}
mnt_boom = AnchoredCylinderMount(r_base = r_boom_base, r_rod = r_boom_rod, s_offset = s_off_boom, stroke = stroke_boom, barrel_radius = 0.055) {
"Dyad": {
"placement": {"diagram": {"x1": 780, "y1": 130, "x2": 980, "y2": 330, "rot": 270}}
}
}
sel_boom = MultibodyComponents.TrajectorySelector(nin = 3, index = 1) {
"Dyad": {"placement": {"diagram": {"x1": 120, "y1": 190, "x2": 220, "y2": 290}}}
}
# --- stick actuator, mount, and reference selector ---
cc_stick = ControlledCylinder(Kv = 5.5e-7, stroke = stroke_stick, M = 12, area_a = 3.85e-3, area_b = 2.59e-3, p_a0 = 2.0e6, p_b0 = 5e5, k_pos = 6, v_max = 0.5, k_vel = 23.0, Ti_vel = 1.5, f_max = 5e4, ka_ff = 4e4, k_force = 0.05, Ti_force = 0.15) {
"Dyad": {"placement": {"diagram": {"x1": 370, "y1": 450, "x2": 570, "y2": 650}}}
}
mnt_stick = AnchoredCylinderMount(r_base = r_stick_base, r_rod = r_stick_rod, s_offset = s_off_stick, stroke = stroke_stick, barrel_radius = 0.045) {
"Dyad": {
"placement": {"diagram": {"x1": 780, "y1": 450, "x2": 980, "y2": 650, "rot": 270}}
}
}
sel_stick = MultibodyComponents.TrajectorySelector(nin = 3, index = 2) {
"Dyad": {"placement": {"diagram": {"x1": 120, "y1": 510, "x2": 220, "y2": 610}}}
}
# --- bucket actuator, mount, and reference selector ---
cc_bucket = ControlledCylinder(Kv = 4e-7, stroke = stroke_bucket, M = 8, area_a = 2.83e-3, area_b = 1.86e-3, p_a0 = 4e5, p_b0 = 5e5, k_pos = 6, v_max = 0.5, k_vel = 2.0, Ti_vel = 1.0, f_max = 3.5e4, ka_ff = 250, k_force = 0.3, Ti_force = 0.05) {
"Dyad": {
"placement": {
"diagram": {"iconName": "default", "x1": 360, "y1": 770, "x2": 560, "y2": 970, "rot": 0}
},
"tags": []
}
}
mnt_bucket = AnchoredCylinderMount(r_base = r_bucket_base, r_rod = r_bucket_rod, s_offset = s_off_bucket, stroke = stroke_bucket, barrel_radius = 0.04) {
"Dyad": {
"placement": {"diagram": {"x1": 780, "y1": 770, "x2": 980, "y2": 970, "rot": 270}}
}
}
sel_bucket = MultibodyComponents.TrajectorySelector(nin = 3, index = 3) {
"Dyad": {"placement": {"diagram": {"x1": 120, "y1": 830, "x2": 220, "y2": 930}}}
}
# --- reference trajectory and soil ---
traj = MultibodyComponents.KinematicPTPBoundedJerk(waypoints = transpose(hcat(s_wp_boom, s_wp_stick, s_wp_bucket)), nout = 3, nwp = 5, qd_max = fill(0.3, 3), qdd_max = fill(0.3, 3), qddd_max = fill(1.0, 3), phase_synchronization = true) {
"Dyad": {"placement": {"diagram": {"x1": -80, "y1": 400, "x2": 20, "y2": 500}}}
}
soil = SoilContact() {
"Dyad": {"placement": {"diagram": {"x1": 1090, "y1": 1120, "x2": 1190, "y2": 1220}}}
}
parameter main_color::Real[4] = [0.9, 0.6, 0.1, 1.0]
"Uniform scale applied to every mesh asset (DWG assembly units to meters)"
parameter mesh_scale::Real = 0.388810
"Placement of the static chassis mesh group (mesh-local to chassis frame)"
parameter T_chassis::Real[4, 4] = [[-1.0, 0.0, 0.0, 142.98847], [0.0, 0.0, 1.0, 0.021192], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]]
"Placement of the boom mesh group (mesh-local to boom frame)"
parameter T_boom::Real[4, 4] = [[-0.850327, 0.0, 0.526254, 121.08507], [0.526254, 0.0, 0.850327, -76.05823], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]]
"Placement of the stick mesh group (mesh-local to stick frame)"
parameter T_stick::Real[4, 4] = [[-0.763897, 0.0, -0.645339, 109.22320], [-0.645339, 0.0, 0.763897, 89.64590], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]]
"Placement of the bucket mesh group (mesh-local to bucket frame)"
parameter T_bucket::Real[4, 4] = [[-0.446973, 0.0, -0.894548, 63.56536], [-0.894548, 0.0, 0.446973, 124.89768], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]]
"Height of the boom pivot above the ground"
parameter pivot_height::Length = 0.9742
"Length of the boom link"
parameter L_boom::Length = 2.0
"Length of the stick link"
parameter L_stick::Length = 1.5020
"Length of the bucket link"
parameter L_bucket::Length = 0.6454
"Mass of the boom link"
parameter m_boom::Mass = 300
"Mass of the stick link"
parameter m_stick::Mass = 150
"Mass of the bucket"
parameter m_bucket::Mass = 100
"Initial boom joint angle"
parameter q1_0::Angle = deg2rad(30.237)
"Initial stick joint angle"
parameter q2_0::Angle = deg2rad(-84.449)
"Initial bucket joint angle"
parameter q3_0::Angle = deg2rad(-40.788)
"Supply pressure"
parameter p_pump::Real = 1.6e7
"Tank pressure"
parameter p_tank::Real = 1e5
"Boom cylinder base anchor on the chassis, world frame [forward, up] (m)"
parameter r_boom_base::Length[2] = [0.1621, 0.8960]
"Boom cylinder rod anchor on the boom, boom frame [along, perp] (m)"
parameter r_boom_rod::Length[2] = [0.7703, 0.1904]
"Stick cylinder base anchor on the boom, boom frame [along, perp] (m)"
parameter r_stick_base::Length[2] = [1.0965, 0.2446]
"Stick cylinder rod anchor on the stick, stick frame [along, perp] (m)"
parameter r_stick_rod::Length[2] = [0.0599, -0.0450]
"Bucket cylinder base anchor on the stick, stick frame [along, perp] (m)"
parameter r_bucket_base::Length[2] = [0.4353, 0.1855]
"Bucket cylinder rod anchor on the bucket crank, bucket frame [along, perp] (m)"
parameter r_bucket_rod::Length[2] = [-0.26311, 0.09577]
"Boom cylinder anchor distance at zero piston position"
parameter s_off_boom::Length = 0.4055
"Stick cylinder anchor distance at zero piston position"
parameter s_off_stick::Length = 0.7592
"Bucket cylinder anchor distance at zero piston position"
parameter s_off_bucket::Length = 0.87
"Stroke of the boom cylinder"
parameter stroke_boom::Length = 0.70
"Stroke of the stick cylinder"
parameter stroke_stick::Length = 0.35
"Stroke of the bucket cylinder"
parameter stroke_bucket::Length = 0.50
"Boom cylinder piston-position waypoints for the dig trajectory"
parameter s_wp_boom::Length[5] = [0.3453, 0.3233, 0.3353, 0.3353, 0.3668]
"Stick cylinder piston-position waypoints for the dig trajectory"
parameter s_wp_stick::Length[5] = [0.1526, 0.1699, 0.1393, 0.14, 0.141]
"Bucket cylinder piston-position waypoints for the dig trajectory"
parameter s_wp_bucket::Length[5] = [0.0619, 0.1291, 0.1364, 0.3380, 0.4522]
"Working fluid of the hydraulic circuits"
path medium::HydraulicComponents.Interfaces.AbstractMedium = HydraulicComponents.IsothermalCompressible(bulk_modulus = 1e9, let_gas = false, density = 870, viscosity = 0.03)
relations
# --- kinematic chain ---
connect(pedestal.frame_a, track_mount.frame_a, world.frame_b, cab.frame_a) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": 2, "M": [], "E": -1},
{"S": 3, "M": [], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [], "E": -2}
],
"junctions": [{"x": 1390, "y": 240}, {"x": 1390, "y": 150}]
}
}
connect(track_mount.frame_b, track.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(pedestal.frame_b, j_boom.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(j_boom.frame_b, boom.frame_a, mnt_boom.frame_rod) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [], "E": 2},
{"S": 3, "M": [], "E": -1}
],
"junctions": [{"x": 1140, "y": 330}]
}
}
connect(boom.frame_b, j_stick.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(j_stick.frame_b, stick.frame_a, mnt_stick.frame_rod) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [], "E": 2},
{"S": 3, "M": [], "E": -1}
],
"junctions": [{"x": 1140, "y": 650}]
}
}
connect(stick.frame_b, j_bucket.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(j_bucket.frame_b, bucket.frame_a, mnt_bucket.frame_rod) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [], "E": 2},
{"S": 3, "M": [], "E": -1}
],
"junctions": [{"x": 1140, "y": 970}]
}
}
connect(bucket.frame_b, soil.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
# --- mesh visuals: each rigid body carries its STL parts through an adapter ---
connect(vis_tracks.frame_a, vis_chassis.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_boom.frame_b, vis_boom.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_stick.frame_b, vis_stick.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_bucket.frame_b, vis_bucket.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
# --- hydraulic supply network ---
connect(c_pump.y, pump.p) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(c_tank.y, tank.p) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
continuity(medium, pump.port.medium)
connect(cc_bucket.port_t, cc_stick.port_t, tank.port, cc_boom.port_t) {
"Dyad": {
"edges": [
{"S": -1, "M": [{"x": 630, "y": 970}], "E": 1},
{"S": 2, "M": [], "E": -1},
{"S": 3, "M": [{"x": 630, "y": -50}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [], "E": -2}
],
"junctions": [{"x": 630, "y": 650}, {"x": 630, "y": 330}]
}
}
connect(cc_bucket.port_p, cc_stick.port_p, pump.port, cc_boom.port_p) {
"Dyad": {
"edges": [
{"S": -1, "M": [{"x": 600, "y": 1020}, {"x": 430, "y": 1020}], "E": 1},
{"S": 2, "M": [{"x": 440, "y": 700}], "E": -1},
{"S": 3, "M": [{"x": 600, "y": -50}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 430, "y": 380}], "E": -2}
],
"junctions": [{"x": 600, "y": 700}, {"x": 600, "y": 380}]
}
}
connect(sel_boom.q, traj.q, sel_bucket.q, sel_stick.q) {
"Dyad": {
"edges": [
{"S": 1, "M": [{"x": 40, "y": 205}], "E": -1},
{"S": -1, "M": [], "E": 2},
{"S": 3, "M": [{"x": 40, "y": 845}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 80, "y": 525}, {"x": 80, "y": 520}], "E": -2}
],
"junctions": [{"x": 40, "y": 415}, {"x": 40, "y": 520}]
}
}
connect(traj.qd, sel_boom.qd, sel_bucket.qd, sel_stick.qd) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [{"x": 50, "y": 225}], "E": 2},
{"S": 3, "M": [{"x": 50, "y": 865}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 85, "y": 545}, {"x": 85, "y": 540}], "E": -2}
],
"junctions": [{"x": 50, "y": 438}, {"x": 50, "y": 540}]
}
}
connect(traj.qdd, sel_boom.qdd, sel_bucket.qdd, sel_stick.qdd) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [{"x": 60, "y": 252}], "E": 2},
{"S": 3, "M": [{"x": 60, "y": 892}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 90, "y": 572}, {"x": 90, "y": 570}], "E": -2}
],
"junctions": [{"x": 60, "y": 464}, {"x": 60, "y": 570}]
}
}
connect(traj.qddd, sel_boom.qddd, sel_bucket.qddd, sel_stick.qddd) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [{"x": 70.5, "y": 484}, {"x": 70.5, "y": 273}], "E": 2},
{"S": 3, "M": [{"x": 70, "y": 912}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 95, "y": 592}, {"x": 95, "y": 590}], "E": -2}
],
"junctions": [{"x": 70, "y": 484}, {"x": 70, "y": 590}]
}
}
connect(cc_boom.flange, mnt_boom.flange) {
"Dyad": {
"edges": [
{
"S": 1,
"M": [{"x": 655, "y": 230}, {"x": 655, "y": 230.00000000000003}],
"E": 2
}
]
}
}
connect(cc_stick.flange, mnt_stick.flange) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(cc_bucket.flange, mnt_bucket.flange) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(mnt_stick.frame_rod, mnt_bucket.frame_base) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(mnt_boom.frame_rod, mnt_stick.frame_base) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(world.frame_b, mnt_boom.frame_base) {
"Dyad": {
"edges": [{"S": 1, "M": [{"x": 1390, "y": 110}, {"x": 880, "y": 110}], "E": 2}]
}
}
connect(adapter_chassis.frame_a, track_mount.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [{"x": 1390, "y": 380}], "E": 2}]}}
connect(adapter_boom.frame_a, boom.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_stick.frame_a, stick.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_bucket.frame_a, bucket.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(sel_boom.s, cc_boom.s_ref) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 205}, {"x": 290, "y": 168}], "E": 2}]}
}
connect(sel_boom.v, cc_boom.v_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 225}, {"x": 290, "y": 230}], "E": 2}]}
}
connect(sel_boom.a, cc_boom.a_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 245}, {"x": 290, "y": 280}], "E": 2}]}
}
connect(sel_stick.s, cc_stick.s_ref) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 525}, {"x": 290, "y": 488}], "E": 2}]}
}
connect(sel_stick.v, cc_stick.v_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 310, "y": 545}, {"x": 310, "y": 550}], "E": 2}]}
}
connect(sel_stick.a, cc_stick.a_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 565}, {"x": 290, "y": 600}], "E": 2}]}
}
connect(sel_bucket.s, cc_bucket.s_ref) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 845}, {"x": 290, "y": 808}], "E": 2}]}
}
connect(sel_bucket.v, cc_bucket.v_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 865}, {"x": 290, "y": 870}], "E": 2}]}
}
connect(sel_bucket.a, cc_bucket.a_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 885}, {"x": 290, "y": 920}], "E": 2}]}
}
connect(vis_chassis.frame_a, adapter_chassis.frame_b) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
metadata {"Dyad": {"tests": {"case1": {"stop": 13}}}}
endFlattened Source
"""
Planar excavator digging through soil with hydraulically actuated boom, stick,
and bucket.
The machine stands 5 m tall with the arm extended straight up. Three revolute
joints connect the boom, stick, and bucket links. Each joint is driven by a
`ControlledCylinder` — a self-contained actuator bundling a proportional valve,
a double-acting cylinder, flange sensors, and a position-velocity-force
`CascadeServoController` — whose flange acts on the linkage through an
`AnchoredCylinderMount` that places the cylinder ends on the adjacent links.
A `KinematicPTPBoundedJerk` trajectory generator issues per-axis position,
velocity, acceleration, and jerk references; one `TrajectorySelector` per axis
picks that axis's references out of the vector-valued trajectory outputs and
feeds them to the corresponding actuator. The dig cycle lowers the bucket into
the soil ahead of the machine, drags it towards the machine, curls it, and
lifts it out. While the bucket tip is below the soil surface, the `SoilContact`
component applies penetration and cutting resistance forces, and the swept soil
accumulates as a payload mass whose weight remains on the bucket after it
leaves the ground.
The machine is rendered with one detailed mesh per rigid body (chassis, boom,
stick, bucket), attached to the corresponding frame through a `PlanarToMultiBody`
adapter and a `ShapefileVisualizer`. The tracks are a separate mesh sharing the
chassis frame so they can be colored gray while the chassis body stays orange
(STL meshes carry no color information of their own). Each body shares a uniform `mesh_scale` and
carries a 4x4 placement transform (`T_chassis`/`T_boom`/`T_stick`/`T_bucket`) that
seats its mesh on the link. The meshes are body-grouped exports of the excavator
CAD assembly, derived from the "DIY RC Excavator" model
(https://www.printables.com/model/833291-diy-rc-excavator/files), licensed CC BY 4.0.
"""
example component Excavator
world = MultibodyComponents.PlanarMechanics.World(render_ground = false, nominal_length = 2, default_joint_color = main_color, default_body_color = main_color, default_rod_color = main_color) {
"Dyad": {
"placement": {"diagram": {"x1": 1440, "y1": -10, "x2": 1340, "y2": 90, "rot": 270}}
}
}
# --- chassis (rigidly fixed to the world, masses are cosmetic) ---
track_mount = MultibodyComponents.PlanarMechanics.FixedTranslation(r = [-1.1, 0.3], render = false) {
"Dyad": {"placement": {"diagram": {"x1": 1380, "y1": 250, "x2": 1480, "y2": 350}}}
}
track = MultibodyComponents.PlanarMechanics.BodyShape(r = [2.2, 0], m = 2500, I = 1000, radius = 0.3, render = false) {
"Dyad": {"placement": {"diagram": {"x1": 1490, "y1": 190, "x2": 1710, "y2": 410}}}
}
cab = MultibodyComponents.PlanarMechanics.BodyShape(r = [0, 0.9], m = 500, I = 200, render = false) {
"Dyad": {"placement": {"diagram": {"x1": 1490, "y1": 40, "x2": 1710, "y2": 260}}}
}
"Rigid post from the world origin to the boom pivot"
pedestal = MultibodyComponents.PlanarMechanics.FixedTranslation(r = [0, pivot_height], render = false) {
"Dyad": {"placement": {"diagram": {"x1": 1340, "y1": 190, "x2": 1240, "y2": 290}}}
}
# --- mesh visuals: detailed STL model grouped by rigid body ---
adapter_chassis = MultibodyComponents.PlanarMechanics.PlanarToMultiBody() {
"Dyad": {"placement": {"diagram": {"x1": 1500, "y1": 330, "x2": 1600, "y2": 430}}}
}
adapter_boom = MultibodyComponents.PlanarMechanics.PlanarToMultiBody() {
"Dyad": {"placement": {"diagram": {"x1": 1170, "x2": 1270, "y1": 310, "y2": 410}}}
}
adapter_stick = MultibodyComponents.PlanarMechanics.PlanarToMultiBody() {
"Dyad": {"placement": {"diagram": {"x1": 1190, "x2": 1290, "y1": 630, "y2": 730}}}
}
adapter_bucket = MultibodyComponents.PlanarMechanics.PlanarToMultiBody() {
"Dyad": {"placement": {"diagram": {"x1": 1200, "y1": 940, "x2": 1300, "y2": 1040}}}
}
vis_chassis = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_chassis.stl"), color = main_color, shape_scale = mesh_scale, shape_transform = T_chassis) {
"Dyad": {"placement": {"diagram": {"x1": 1590, "x2": 1690, "y1": 330, "y2": 430}}}
}
vis_tracks = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_tracks.stl"), color = [0.35, 0.35, 0.38, 1.0], shape_scale = mesh_scale, shape_transform = T_chassis) {
"Dyad": {"placement": {"diagram": {"x1": 1590, "x2": 1690, "y1": 410, "y2": 510}}}
}
vis_boom = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_boom.stl"), color = main_color, shape_scale = mesh_scale, shape_transform = T_boom) {
"Dyad": {"placement": {"diagram": {"x1": 1260, "x2": 1360, "y1": 310, "y2": 410}}}
}
vis_stick = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_stick.stl"), color = main_color, shape_scale = mesh_scale, shape_transform = T_stick) {
"Dyad": {"placement": {"diagram": {"x1": 1280, "x2": 1380, "y1": 630, "y2": 730}}}
}
vis_bucket = MultibodyComponents.ShapefileVisualizer(shapefile = excavator_asset("excavator_bucket.stl"), color = [0.5, 0.5, 0.55, 1.0], shape_scale = mesh_scale, shape_transform = T_bucket, render = true) {
"Dyad": {"placement": {"diagram": {"x1": 1290, "x2": 1390, "y1": 940, "y2": 1040}}}
}
# --- boom / stick / bucket linkage ---
j_boom = MultibodyComponents.PlanarMechanics.Revolute(phi(initial = q1_0), w(initial = 0)) {
"Dyad": {"placement": {"diagram": {"x1": 1230, "y1": 190, "x2": 1130, "y2": 290}}}
}
boom = MultibodyComponents.PlanarMechanics.BodyShape(r = [L_boom, 0], m = m_boom, I = m_boom * L_boom ^ 2 / 12, radius = 0.09, render = false) {
"Dyad": {
"placement": {"diagram": {"x1": 1190, "y1": 350, "x2": 1090, "y2": 450, "rot": 270}}
}
}
j_stick = MultibodyComponents.PlanarMechanics.Revolute(phi(initial = q2_0), w(initial = 0)) {
"Dyad": {
"placement": {"diagram": {"x1": 1090, "y1": 500, "x2": 1190, "y2": 600, "rot": 90}}
}
}
stick = MultibodyComponents.PlanarMechanics.BodyShape(r = [L_stick, 0], m = m_stick, I = m_stick * L_stick ^ 2 / 12, radius = 0.07, render = false) {
"Dyad": {
"placement": {"diagram": {"x1": 1090, "y1": 670, "x2": 1190, "y2": 770, "rot": 90}}
}
}
j_bucket = MultibodyComponents.PlanarMechanics.Revolute(phi(initial = q3_0), w(initial = 0)) {
"Dyad": {
"placement": {"diagram": {"x1": 1090, "y1": 810, "x2": 1190, "y2": 910, "rot": 90}}
}
}
bucket = MultibodyComponents.PlanarMechanics.BodyShape(r = [L_bucket, 0], m = m_bucket, I = m_bucket * L_bucket ^ 2 / 12, render = false) {
"Dyad": {
"placement": {"diagram": {"x1": 1090, "y1": 980, "x2": 1190, "y2": 1080, "rot": 90}}
}
}
# --- hydraulic supply ---
pump = HydraulicComponents.Sources.BoundaryPressure() {
"Dyad": {"placement": {"diagram": {"x1": 480, "y1": -100, "x2": 580, "y2": 0}}}
}
tank = HydraulicComponents.Sources.BoundaryPressure() {
"Dyad": {"placement": {"diagram": {"x1": 750, "y1": -100, "x2": 650, "y2": 0}}}
}
c_pump = BlockComponents.Sources.Constant(k = p_pump) {
"Dyad": {"placement": {"diagram": {"x1": 330, "y1": -100, "x2": 430, "y2": 0}}}
}
c_tank = BlockComponents.Sources.Constant(k = p_tank) {
"Dyad": {"placement": {"diagram": {"x1": 900, "y1": -100, "x2": 800, "y2": 0}}}
}
# --- boom actuator, mount, and reference selector ---
cc_boom = ControlledCylinder(Kv = 9e-7, stroke = stroke_boom, M = 20, area_a = 6.36e-3, area_b = 4.40e-3, p_a0 = 2.4e6, p_b0 = 5e5, k_pos = 6, v_max = 0.5, k_vel = 18.75, Ti_vel = 1.5, f_max = 8e4, ka_ff = 5e4, k_force = 0.08, Ti_force = 0.15) {
"Dyad": {
"placement": {
"diagram": {"iconName": "default", "x1": 360, "y1": 130, "x2": 560, "y2": 330, "rot": 0}
},
"tags": []
}
}
mnt_boom = AnchoredCylinderMount(r_base = r_boom_base, r_rod = r_boom_rod, s_offset = s_off_boom, stroke = stroke_boom, barrel_radius = 0.055) {
"Dyad": {
"placement": {"diagram": {"x1": 780, "y1": 130, "x2": 980, "y2": 330, "rot": 270}}
}
}
sel_boom = MultibodyComponents.TrajectorySelector(nin = 3, index = 1) {
"Dyad": {"placement": {"diagram": {"x1": 120, "y1": 190, "x2": 220, "y2": 290}}}
}
# --- stick actuator, mount, and reference selector ---
cc_stick = ControlledCylinder(Kv = 5.5e-7, stroke = stroke_stick, M = 12, area_a = 3.85e-3, area_b = 2.59e-3, p_a0 = 2.0e6, p_b0 = 5e5, k_pos = 6, v_max = 0.5, k_vel = 23.0, Ti_vel = 1.5, f_max = 5e4, ka_ff = 4e4, k_force = 0.05, Ti_force = 0.15) {
"Dyad": {"placement": {"diagram": {"x1": 370, "y1": 450, "x2": 570, "y2": 650}}}
}
mnt_stick = AnchoredCylinderMount(r_base = r_stick_base, r_rod = r_stick_rod, s_offset = s_off_stick, stroke = stroke_stick, barrel_radius = 0.045) {
"Dyad": {
"placement": {"diagram": {"x1": 780, "y1": 450, "x2": 980, "y2": 650, "rot": 270}}
}
}
sel_stick = MultibodyComponents.TrajectorySelector(nin = 3, index = 2) {
"Dyad": {"placement": {"diagram": {"x1": 120, "y1": 510, "x2": 220, "y2": 610}}}
}
# --- bucket actuator, mount, and reference selector ---
cc_bucket = ControlledCylinder(Kv = 4e-7, stroke = stroke_bucket, M = 8, area_a = 2.83e-3, area_b = 1.86e-3, p_a0 = 4e5, p_b0 = 5e5, k_pos = 6, v_max = 0.5, k_vel = 2.0, Ti_vel = 1.0, f_max = 3.5e4, ka_ff = 250, k_force = 0.3, Ti_force = 0.05) {
"Dyad": {
"placement": {
"diagram": {"iconName": "default", "x1": 360, "y1": 770, "x2": 560, "y2": 970, "rot": 0}
},
"tags": []
}
}
mnt_bucket = AnchoredCylinderMount(r_base = r_bucket_base, r_rod = r_bucket_rod, s_offset = s_off_bucket, stroke = stroke_bucket, barrel_radius = 0.04) {
"Dyad": {
"placement": {"diagram": {"x1": 780, "y1": 770, "x2": 980, "y2": 970, "rot": 270}}
}
}
sel_bucket = MultibodyComponents.TrajectorySelector(nin = 3, index = 3) {
"Dyad": {"placement": {"diagram": {"x1": 120, "y1": 830, "x2": 220, "y2": 930}}}
}
# --- reference trajectory and soil ---
traj = MultibodyComponents.KinematicPTPBoundedJerk(waypoints = transpose(hcat(s_wp_boom, s_wp_stick, s_wp_bucket)), nout = 3, nwp = 5, qd_max = fill(0.3, 3), qdd_max = fill(0.3, 3), qddd_max = fill(1.0, 3), phase_synchronization = true) {
"Dyad": {"placement": {"diagram": {"x1": -80, "y1": 400, "x2": 20, "y2": 500}}}
}
soil = SoilContact() {
"Dyad": {"placement": {"diagram": {"x1": 1090, "y1": 1120, "x2": 1190, "y2": 1220}}}
}
parameter main_color::Real[4] = [0.9, 0.6, 0.1, 1.0]
"Uniform scale applied to every mesh asset (DWG assembly units to meters)"
parameter mesh_scale::Real = 0.388810
"Placement of the static chassis mesh group (mesh-local to chassis frame)"
parameter T_chassis::Real[4, 4] = [[-1.0, 0.0, 0.0, 142.98847], [0.0, 0.0, 1.0, 0.021192], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]]
"Placement of the boom mesh group (mesh-local to boom frame)"
parameter T_boom::Real[4, 4] = [[-0.850327, 0.0, 0.526254, 121.08507], [0.526254, 0.0, 0.850327, -76.05823], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]]
"Placement of the stick mesh group (mesh-local to stick frame)"
parameter T_stick::Real[4, 4] = [[-0.763897, 0.0, -0.645339, 109.22320], [-0.645339, 0.0, 0.763897, 89.64590], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]]
"Placement of the bucket mesh group (mesh-local to bucket frame)"
parameter T_bucket::Real[4, 4] = [[-0.446973, 0.0, -0.894548, 63.56536], [-0.894548, 0.0, 0.446973, 124.89768], [0.0, 1.0, 0.0, -198.93546], [0.0, 0.0, 0.0, 1.0]]
"Height of the boom pivot above the ground"
parameter pivot_height::Length = 0.9742
"Length of the boom link"
parameter L_boom::Length = 2.0
"Length of the stick link"
parameter L_stick::Length = 1.5020
"Length of the bucket link"
parameter L_bucket::Length = 0.6454
"Mass of the boom link"
parameter m_boom::Mass = 300
"Mass of the stick link"
parameter m_stick::Mass = 150
"Mass of the bucket"
parameter m_bucket::Mass = 100
"Initial boom joint angle"
parameter q1_0::Angle = deg2rad(30.237)
"Initial stick joint angle"
parameter q2_0::Angle = deg2rad(-84.449)
"Initial bucket joint angle"
parameter q3_0::Angle = deg2rad(-40.788)
"Supply pressure"
parameter p_pump::Real = 1.6e7
"Tank pressure"
parameter p_tank::Real = 1e5
"Boom cylinder base anchor on the chassis, world frame [forward, up] (m)"
parameter r_boom_base::Length[2] = [0.1621, 0.8960]
"Boom cylinder rod anchor on the boom, boom frame [along, perp] (m)"
parameter r_boom_rod::Length[2] = [0.7703, 0.1904]
"Stick cylinder base anchor on the boom, boom frame [along, perp] (m)"
parameter r_stick_base::Length[2] = [1.0965, 0.2446]
"Stick cylinder rod anchor on the stick, stick frame [along, perp] (m)"
parameter r_stick_rod::Length[2] = [0.0599, -0.0450]
"Bucket cylinder base anchor on the stick, stick frame [along, perp] (m)"
parameter r_bucket_base::Length[2] = [0.4353, 0.1855]
"Bucket cylinder rod anchor on the bucket crank, bucket frame [along, perp] (m)"
parameter r_bucket_rod::Length[2] = [-0.26311, 0.09577]
"Boom cylinder anchor distance at zero piston position"
parameter s_off_boom::Length = 0.4055
"Stick cylinder anchor distance at zero piston position"
parameter s_off_stick::Length = 0.7592
"Bucket cylinder anchor distance at zero piston position"
parameter s_off_bucket::Length = 0.87
"Stroke of the boom cylinder"
parameter stroke_boom::Length = 0.70
"Stroke of the stick cylinder"
parameter stroke_stick::Length = 0.35
"Stroke of the bucket cylinder"
parameter stroke_bucket::Length = 0.50
"Boom cylinder piston-position waypoints for the dig trajectory"
parameter s_wp_boom::Length[5] = [0.3453, 0.3233, 0.3353, 0.3353, 0.3668]
"Stick cylinder piston-position waypoints for the dig trajectory"
parameter s_wp_stick::Length[5] = [0.1526, 0.1699, 0.1393, 0.14, 0.141]
"Bucket cylinder piston-position waypoints for the dig trajectory"
parameter s_wp_bucket::Length[5] = [0.0619, 0.1291, 0.1364, 0.3380, 0.4522]
"Working fluid of the hydraulic circuits"
path medium::HydraulicComponents.Interfaces.AbstractMedium = HydraulicComponents.IsothermalCompressible(bulk_modulus = 1e9, let_gas = false, density = 870, viscosity = 0.03)
relations
# --- kinematic chain ---
connect(pedestal.frame_a, track_mount.frame_a, world.frame_b, cab.frame_a) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": 2, "M": [], "E": -1},
{"S": 3, "M": [], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [], "E": -2}
],
"junctions": [{"x": 1390, "y": 240}, {"x": 1390, "y": 150}]
}
}
connect(track_mount.frame_b, track.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(pedestal.frame_b, j_boom.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(j_boom.frame_b, boom.frame_a, mnt_boom.frame_rod) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [], "E": 2},
{"S": 3, "M": [], "E": -1}
],
"junctions": [{"x": 1140, "y": 330}]
}
}
connect(boom.frame_b, j_stick.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(j_stick.frame_b, stick.frame_a, mnt_stick.frame_rod) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [], "E": 2},
{"S": 3, "M": [], "E": -1}
],
"junctions": [{"x": 1140, "y": 650}]
}
}
connect(stick.frame_b, j_bucket.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(j_bucket.frame_b, bucket.frame_a, mnt_bucket.frame_rod) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [], "E": 2},
{"S": 3, "M": [], "E": -1}
],
"junctions": [{"x": 1140, "y": 970}]
}
}
connect(bucket.frame_b, soil.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
# --- mesh visuals: each rigid body carries its STL parts through an adapter ---
connect(vis_tracks.frame_a, vis_chassis.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_boom.frame_b, vis_boom.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_stick.frame_b, vis_stick.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_bucket.frame_b, vis_bucket.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
# --- hydraulic supply network ---
connect(c_pump.y, pump.p) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(c_tank.y, tank.p) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
continuity(medium, pump.port.medium)
connect(cc_bucket.port_t, cc_stick.port_t, tank.port, cc_boom.port_t) {
"Dyad": {
"edges": [
{"S": -1, "M": [{"x": 630, "y": 970}], "E": 1},
{"S": 2, "M": [], "E": -1},
{"S": 3, "M": [{"x": 630, "y": -50}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [], "E": -2}
],
"junctions": [{"x": 630, "y": 650}, {"x": 630, "y": 330}]
}
}
connect(cc_bucket.port_p, cc_stick.port_p, pump.port, cc_boom.port_p) {
"Dyad": {
"edges": [
{"S": -1, "M": [{"x": 600, "y": 1020}, {"x": 430, "y": 1020}], "E": 1},
{"S": 2, "M": [{"x": 440, "y": 700}], "E": -1},
{"S": 3, "M": [{"x": 600, "y": -50}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 430, "y": 380}], "E": -2}
],
"junctions": [{"x": 600, "y": 700}, {"x": 600, "y": 380}]
}
}
connect(sel_boom.q, traj.q, sel_bucket.q, sel_stick.q) {
"Dyad": {
"edges": [
{"S": 1, "M": [{"x": 40, "y": 205}], "E": -1},
{"S": -1, "M": [], "E": 2},
{"S": 3, "M": [{"x": 40, "y": 845}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 80, "y": 525}, {"x": 80, "y": 520}], "E": -2}
],
"junctions": [{"x": 40, "y": 415}, {"x": 40, "y": 520}]
}
}
connect(traj.qd, sel_boom.qd, sel_bucket.qd, sel_stick.qd) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [{"x": 50, "y": 225}], "E": 2},
{"S": 3, "M": [{"x": 50, "y": 865}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 85, "y": 545}, {"x": 85, "y": 540}], "E": -2}
],
"junctions": [{"x": 50, "y": 438}, {"x": 50, "y": 540}]
}
}
connect(traj.qdd, sel_boom.qdd, sel_bucket.qdd, sel_stick.qdd) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [{"x": 60, "y": 252}], "E": 2},
{"S": 3, "M": [{"x": 60, "y": 892}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 90, "y": 572}, {"x": 90, "y": 570}], "E": -2}
],
"junctions": [{"x": 60, "y": 464}, {"x": 60, "y": 570}]
}
}
connect(traj.qddd, sel_boom.qddd, sel_bucket.qddd, sel_stick.qddd) {
"Dyad": {
"edges": [
{"S": 1, "M": [], "E": -1},
{"S": -1, "M": [{"x": 70.5, "y": 484}, {"x": 70.5, "y": 273}], "E": 2},
{"S": 3, "M": [{"x": 70, "y": 912}], "E": -2},
{"S": -2, "M": [], "E": -1},
{"S": 4, "M": [{"x": 95, "y": 592}, {"x": 95, "y": 590}], "E": -2}
],
"junctions": [{"x": 70, "y": 484}, {"x": 70, "y": 590}]
}
}
connect(cc_boom.flange, mnt_boom.flange) {
"Dyad": {
"edges": [
{
"S": 1,
"M": [{"x": 655, "y": 230}, {"x": 655, "y": 230.00000000000003}],
"E": 2
}
]
}
}
connect(cc_stick.flange, mnt_stick.flange) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(cc_bucket.flange, mnt_bucket.flange) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(mnt_stick.frame_rod, mnt_bucket.frame_base) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(mnt_boom.frame_rod, mnt_stick.frame_base) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(world.frame_b, mnt_boom.frame_base) {
"Dyad": {
"edges": [{"S": 1, "M": [{"x": 1390, "y": 110}, {"x": 880, "y": 110}], "E": 2}]
}
}
connect(adapter_chassis.frame_a, track_mount.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [{"x": 1390, "y": 380}], "E": 2}]}}
connect(adapter_boom.frame_a, boom.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_stick.frame_a, stick.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(adapter_bucket.frame_a, bucket.frame_a) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
connect(sel_boom.s, cc_boom.s_ref) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 205}, {"x": 290, "y": 168}], "E": 2}]}
}
connect(sel_boom.v, cc_boom.v_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 225}, {"x": 290, "y": 230}], "E": 2}]}
}
connect(sel_boom.a, cc_boom.a_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 245}, {"x": 290, "y": 280}], "E": 2}]}
}
connect(sel_stick.s, cc_stick.s_ref) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 525}, {"x": 290, "y": 488}], "E": 2}]}
}
connect(sel_stick.v, cc_stick.v_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 310, "y": 545}, {"x": 310, "y": 550}], "E": 2}]}
}
connect(sel_stick.a, cc_stick.a_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 565}, {"x": 290, "y": 600}], "E": 2}]}
}
connect(sel_bucket.s, cc_bucket.s_ref) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 845}, {"x": 290, "y": 808}], "E": 2}]}
}
connect(sel_bucket.v, cc_bucket.v_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 865}, {"x": 290, "y": 870}], "E": 2}]}
}
connect(sel_bucket.a, cc_bucket.a_ff) {
"Dyad": {"edges": [{"S": 1, "M": [{"x": 290, "y": 885}, {"x": 290, "y": 920}], "E": 2}]}
}
connect(vis_chassis.frame_a, adapter_chassis.frame_b) {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
metadata {"Dyad": {"tests": {"case1": {"stop": 13}}}}
endTest Cases
using MultibodyComponents
using DyadInterface: TransientAnalysis, rebuild_sol, ODEAlg
using ModelingToolkit: toggle_namespacing, get_initial_conditions, @named
using CSV, DataFrames, Plots
snapshotsdir = joinpath(dirname(dirname(pathof(MultibodyComponents))), "test", "snapshots")<< @setup-block not executed in draft mode >>Test Case case1
@named model_case1 = MultibodyComponents.PlanarMechanics.examples.excavator.Excavator()
model_case1 = toggle_namespacing(model_case1, false)
model_case1 = toggle_namespacing(model_case1, true)
result_case1 = TransientAnalysis(; model = model_case1, alg = ODEAlg.Auto(), start = 0e+0, stop = 1.3e+1, abstol=1e-6, reltol=1e-6)
sol_case1 = rebuild_sol(result_case1)<< @setup-block not executed in draft mode >>Related
Examples
Experiments
Analyses
<< @example-block not executed in draft mode >>