Skip to content
LIBRARY
PlanarMechanics.examples.excavator.Excavator.md

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:

NameDescriptionUnitsDefault value
main_color[0.9, 0.6, 0.1, 1.0]
mesh_scaleUniform scale applied to every mesh asset (DWG assembly units to meters)0.388810
T_chassisPlacement of the static chassis mesh group (mesh-local to chassis frame)[-1.0 0.0 0....0 0.0 1.0]
T_boomPlacement of the boom mesh group (mesh-local to boom frame)[-0.850327 ....0 0.0 1.0]
T_stickPlacement of the stick mesh group (mesh-local to stick frame)[-0.763897 ....0 0.0 1.0]
T_bucketPlacement of the bucket mesh group (mesh-local to bucket frame)[-0.446973 ....0 0.0 1.0]
pivot_heightHeight of the boom pivot above the groundm0.9742
L_boomLength of the boom linkm2.0
L_stickLength of the stick linkm1.5020
L_bucketLength of the bucket linkm0.6454
m_boomMass of the boom linkkg300
m_stickMass of the stick linkkg150
m_bucketMass of the bucketkg100
q1_0Initial boom joint angleraddeg2rad(30.237)
q2_0Initial stick joint angleraddeg2rad(-84.449)
q3_0Initial bucket joint angleraddeg2rad(-40.788)
p_pumpSupply pressure1.6e7
p_tankTank pressure1e5
r_boom_baseBoom cylinder base anchor on the chassis, world frame [forward, up] (m)m[0.1621, 0.8960]
r_boom_rodBoom cylinder rod anchor on the boom, boom frame [along, perp] (m)m[0.7703, 0.1904]
r_stick_baseStick cylinder base anchor on the boom, boom frame [along, perp] (m)m[1.0965, 0.2446]
r_stick_rodStick cylinder rod anchor on the stick, stick frame [along, perp] (m)m[0.0599, -0.0450]
r_bucket_baseBucket cylinder base anchor on the stick, stick frame [along, perp] (m)m[0.4353, 0.1855]
r_bucket_rodBucket cylinder rod anchor on the bucket crank, bucket frame [along, perp] (m)m[-0.26311, 0.09577]
s_off_boomBoom cylinder anchor distance at zero piston positionm0.4055
s_off_stickStick cylinder anchor distance at zero piston positionm0.7592
s_off_bucketBucket cylinder anchor distance at zero piston positionm0.87
stroke_boomStroke of the boom cylinderm0.70
stroke_stickStroke of the stick cylinderm0.35
stroke_bucketStroke of the bucket cylinderm0.50
s_wp_boomBoom cylinder piston-position waypoints for the dig trajectorym[0.3453, 0....53, 0.3668]
s_wp_stickStick cylinder piston-position waypoints for the dig trajectorym[0.1526, 0.....14, 0.141]
s_wp_bucketBucket cylinder piston-position waypoints for the dig trajectorym[0.0619, 0....80, 0.4522]

Behavior

Behavior of this component cannot be rendered because it includes path variables.

Source

dyad
"""
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}}}}
end
Flattened Source
dyad
"""
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}}}}
end


Test Cases

julia
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

julia
@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 >>
  • Examples

  • Experiments

  • Analyses

julia
<< @example-block not executed in draft mode >>