Building a system using YAML

This tutorial explains how to define mechanical systems using YAML configuration files. YAML is the recommended approach for complex models with many components, since it separates geometry data from simulation code.

Overview

The YAML workflow has three steps:

  1. Write a YAML file — define points, segments, and other components in a structured text file
  2. Load with load_sys_struct_from_yaml — parses the YAML and calls the same Julia constructors used in the Julia tutorial
  3. Compile and simulate — same as the Julia path: SymbolicAWEModelinit!next_step!

The YAML loader does as little as possible: it parses YAML, applies the variables block, converts string enum values, and calls constructors. All defaults and derived calculations happen in the component constructors.

YAML file structure

A YAML file can contain any of these top-level blocks:

BlockPurpose
variablesNamed values and property sets reused across the file
pointsPoint masses (nodes in the system)
segmentsSpring-damper connections
pulleysEqual-tension constraints
stationsDeformable wing sections (twist DOF)
tethersWinch-controlled segment groups
winchesTorque-controlled motors
wingsAerodynamic bodies
bodiesPlain rigid bodies
elastic_jointsLumped 6-DOF springs between two bodies
timoshenko_jointsBeam elements between two bodies
transformsSpherical coordinate positioning

A file needs at least a points or a bodies block; everything else is optional.

Each block uses a headers + data format:

points:
  headers: [name, pos_cad, type, extra_mass]
  data:
    - [anchor, [0, 0, 0], STATIC, 0.0]
    - [mass, [0, 0, -50], DYNAMIC, 1.0]

The headers row defines column names. Each data row is a list of values matching those headers. Missing trailing columns default to nothing.

Alternatively, you can use a dict format where each row is a dictionary:

points:
  data:
    - {name: anchor, pos_cad: [0, 0, 0], type: STATIC}
    - {name: mass, pos_cad: [0, 0, -50], type: DYNAMIC, extra_mass: 1.0}

Minimal example

Here is a complete YAML file for a simple two-point tether:

# simple_tether.yaml

points:
  headers: [idx, pos_cad, type, wing_idx, transform_idx, extra_mass]
  data:
    - [1, [0, 0, 0], STATIC, nothing, 1, 0.0]
    - [2, [0, 0, -50], DYNAMIC, nothing, 1, 1.0]

segments:
  headers: [idx, point_i, point_j, l0, diameter_mm,
            unit_stiffness, unit_damping, compression_frac]
  data:
    - [1, 1, 2, 50.0, 5.0, 100000, 50.0, 0.001]

transforms:
  headers: [idx, elevation, azimuth, heading,
            base_pos, base_point_idx, rot_point_idx]
  data:
    - [1, -80, 0, 0, [0, 0, 50], 1, 2]

Load and simulate:

using SymbolicAWEModels
using KiteUtils: init!, next_step!, update_sys_state!

set = Settings("system.yaml")
set.v_wind = 0.0

sys = load_sys_struct_from_yaml("simple_tether.yaml";
    system_name="simple_tether", set=set)
sam = SymbolicAWEModel(set, sys)
init!(sam)

for _ in 1:100
    next_step!(sam)
end
Transform angles

Transform elevation and azimuth values in YAML are specified in degrees (converted automatically), unlike the Julia constructor which takes radians.

Variables and materials

The variables block gives names to values that are reused across the file. A name written in any column of any block is replaced by its value:

variables:
  bridle_comp: 0.01
  bridle_diameter: 2.5

segments:
  headers: [name, point_i, point_j, l0, diameter_mm,
            unit_stiffness, unit_damping, compression_frac]
  data:
    - [bridle_left, 1, 2, 5.0, bridle_diameter, 100000, 50.0, bridle_comp]
    - [bridle_right, 1, 3, 5.0, bridle_diameter, 100000, 50.0, bridle_comp]

A variable may hold a number, a string or a list (kcu_pos: [0.0, 0.0, 12.0]), and may be written in terms of another variable. Column names in headers are never substituted, but a variable may not share its name with a component — that is an error, since references to the component would resolve to the variable.

Multi-variables and materials

A variable holding a mapping fills several columns at once: it stands for the columns it names, so the row is written with one entry for the whole group.

variables:
  dyneema:
    youngs_modulus: 55.0e9
    damping_per_stiffness: 0.00077
    density: 724.0

segments:
  headers: [name, point_i, point_j, l0, diameter_mm,
            youngs_modulus, damping_per_stiffness, density, compression_frac]
  data:
    # 'dyneema' fills youngs_modulus, damping_per_stiffness and density
    - [bridle, 1, 2, 5.0, 5.0, dyneema, 0.01]
    - [thin_bridle, 1, 3, 5.0, 1.0, dyneema, 0.01]
    # Written out (no multi-variable)
    - [strut, 2, 3, 5.0, 1.0, 6.4e9, 0.002, 724.0, 0.01]

The fields must match the columns starting at that position, in any order — a mismatch is an error naming both sides. This replaces the old materials table: each material defines only the fields it needs, with no shared headers row to keep in sync, and no nothing padding for the columns it fills.

In a dict row the fields are merged instead, and the row wins — override a field by naming it:

segments:
  data:
    - {name: bridle, point_i: 1, point_j: 2, l0: 5.0, diameter_mm: 5.0,
       material: dyneema, damping_per_stiffness: 0.001}

A segment's density [kg/m³] is used for its mass, so different tethers can use different materials. Without one, the global set.rho_tether applies.

Material properties versus element properties

unit_stiffness [N] and unit_damping [Ns] describe one element: both scale with its cross section, so a material shared by segments of different diameter must not fix them. Use the diameter-independent form instead:

MaterialElementRelation
youngs_modulus [Pa]unit_stiffness [N]unit_stiffness = youngs_modulus * pi * (diameter_mm/2000)^2
damping_per_stiffness [s]unit_damping [Ns]unit_damping = damping_per_stiffness * unit_stiffness

Either form may be given per row; giving both forms of the same quantity is an error. What is left out comes from the settings (e_tether, rel_damping, d_tether, rho_tether).

Removed in v0.14

The materials, elements and segment_properties tables were removed. A material is now a multi-variable, and its youngs_modulus, damping_per_stiffness and density are ordinary columns.

Component reference

Points

points:
  headers: [name, pos_cad, type, wing_idx, transform_idx, body, joint,
            vel_w, extra_mass, body_frame_damping, world_frame_damping,
            area, drag_coeff, fix_sphere, fix_static]
FieldTypeDefaultDescription
nameString/IntrequiredPoint identifier (idx also accepted)
pos_cad[x,y,z]requiredPosition in CAD frame [m]
typeStringrequiredSTATIC, DYNAMIC, or BODY_STATIC
wing_idxInt/nothing1Wing this point belongs to
transform_idxInt/nothingnothingTransform for initial positioning
bodyRef/nothingnothingBODY_STATIC: body the point rides
jointRef/nothingnothingBODY_STATIC: beam element the point rides
vel_w[x,y,z]zerosInitial world-frame velocity [m/s]
extra_massFloat0.0Additional mass [kg]
body_frame_dampingFloat0.0Damping in body frame [Ns/m]
world_frame_dampingFloat0.0Damping in world frame [Ns/m]
areaFloat0.0Cross-sectional area for drag [m^2]
drag_coeffFloat0.0Drag coefficient
fix_sphereBoolfalseConstrain the point to a sphere
fix_staticBoolfalseDynamically freeze the point position

A BODY_STATIC point rides a rigid body (body:, or wing: for a wing body) or a beam element (joint:); its body-frame offset is derived from pos_cad.

Segments

segments:
  headers: [idx, point_i, point_j, l0, diameter_mm,
            unit_stiffness, unit_damping, compression_frac]
  data:
    - [1, 1, 2, 5.0, 5.0, 100000, 50.0, 0.01]

The material columns can also come from a multi-variable, see Variables and materials.

FieldTypeDescription
idxIntSegment identifier
point_i, point_jIntEndpoint point indices
l0FloatUnstretched length [m] (0 = calculate from points)
diameter_mmFloatDiameter [mm]
unit_stiffnessFloatPer-unit-length stiffness [N]
unit_dampingFloat/nothingPer-unit-length damping [Ns], or nothing for the settings default
youngs_modulusFloat/nothingDiameter-independent alternative to unit_stiffness [Pa]
damping_per_stiffnessFloat/nothingDiameter-independent alternative to unit_damping [s]
compression_fracFloatCompressive/tensile stiffness ratio (0-1)
compression_damping_fracFloatFraction of unit_damping still acting under compression (0-1, default 1)
densityFloat/nothingMaterial density [kg/m³]; falls back to set.rho_tether

Pulleys

pulleys:
  headers: [idx, segment_i, segment_j, type, efficiency]
  data:
    - [1, 3, 4, DYNAMIC, 0.95]
FieldTypeDefaultDescription
idxIntPulley identifier
segment_i, segment_jIntThe two segments sharing the pulley point
typeEnumDYNAMIC
efficiencyFloat0.95Fraction of line tension the sheave passes on (1.0 = ideal)
dampingFloat0.0Artificial damping on rope travel [Ns/m], for debugging
brakeBoolfalseFreeze the rope split where it is, for debugging
friction_epsilonFloat0.1Friction smoothing width [m/s]

Tethers

Route 1 (explicit segments):

tethers:
  headers: [idx, segment_idxs]
  data:
    - [1, [1, 2, 3]]

Route 2 (auto-generated segments):

tethers:
  headers: [name, start_point, end_point, n_segments]
  data:
    - [main, kite, ground, 5]

The generated points are DYNAMIC and the generated segments take the tether's material columns, which are the Segments ones (diameter_mm, unit_stiffness, unit_damping, youngs_modulus, damping_per_stiffness, density, compression_frac, compression_damping_frac) and may equally come from a multi-variable. A tether without a winch keeps its length fixed, which is how a plain line is split into several segments.

FieldTypeDescription
init_stretched_lengthFloat/nothingPlaced (stretched) standoff [m]; reinit! moves the free end to span it. nothing = keep the point geometry
init_tether_forceFloat/nothingTarget initial spring force [N], default 0
init_stretch_fracFloat/nothingInitial unstretched/stretched ratio; 1.0 is untensioned, > 1 slack. Excludes init_tether_force

Winches

winches:
  headers: [idx, tether_idxs, winch_point]
  data:
    - [1, [1], ground]

Stations

A Station is a wing section with a twist degree of freedom. point_idxs (or points) lists its structural points; type is DYNAMIC (twist solves its equilibrium), STATIC (twist is a prescribed control input) or KINEMATIC (a flap hinge whose deflection follows two bodies).

stations:
  headers: [name, point_idxs, type, moment_frac, damping, stiffness]
  data:
    - [left,   [le_left, te_left],     DYNAMIC, 0.25, 100.0, 0.0]
    - [center, [le_center, te_center], DYNAMIC, 0.25, 100.0, 0.0]

Wings

wings:
  data:
    - name: main_wing
      dynamics_type: RIGID_DYNAMICS   # or PARTICLE_DYNAMICS
      aero_mode: linearized           # direct | continuous | pressure | plate | none
      stations: [left, center, right]
      origin_idx: kcu
      z_ref_points: [kcu, le_center]
      y_ref_points: [le_right, le_left]
      aero_z_offset: 0.0

Mass properties (mass, com, unit_inertia) are optional columns; when omitted they are computed from the wing's .obj mesh if one is supplied, and otherwise fall back to point-mass inertia.

Bodies and joints

A Body is a plain rigid body — no aerodynamics, no structural points of its own. Bodies are linked by ElasticJoints (a lumped 6-DOF spring) or TimoshenkoJoints (a 2-node beam element); a chain of the latter forms a beam. BODY_STATIC points ride a body (body_idx) or a beam element (joint).

bodies:
  headers: [name, mass, inertia_principal, pos, type]
  data:
    - [nodeA, 1.0, [0.01, 0.01, 0.01], [0.0, 0.0, 0.0], STATIC]
    - [nodeB, 1.0, [0.01, 0.01, 0.01], [1.0, 0.0, 0.0], DYNAMIC]

timoshenko_joints:
  headers: [name, body_a, body_b, EA, GA, GJ, EIy, EIz, shear_coeff,
            damping_trans, damping_rot]
  data:
    - [joint, nodeA, nodeB, 10000.0, 1500.0, 50.0, 100.0, 100.0, 0.8333,
       200.0, 3.0]

points:
  headers: [name, pos_cad, type, body_idx]
  data:
    - [tip_anchor, [1.0, 0.0, 0.0], BODY_STATIC, nodeB]

Scalar joint columns are linear laws; nonlinear (callable) stiffness laws are supplied programmatically, not from YAML.

Transforms

transforms:
  headers: [idx, elevation, azimuth, heading,
            base_pos, base_point_idx, rot_point_idx]
  data:
    - [1, -80, 0, 0, [0, 0, 50], 1, 2]

Loading workflow

The full loading workflow for a model with aerodynamics:

using SymbolicAWEModels, VortexStepMethod

set_data_path("data/2plate_kite")
set = Settings("system.yaml")
vsm_set = VortexStepMethod.VSMSettings(
    joinpath(get_data_path(), "vsm_settings.yaml"); data_prefix=false)

struc_yaml = joinpath(get_data_path(),
    "rigid_structural_geometry.yaml")
sys = load_sys_struct_from_yaml(struc_yaml;
    system_name="2plate_kite",
    set=set,
    vsm_set=vsm_set)

sam = SymbolicAWEModel(set, sys)
init!(sam)

2-plate kite structure

After compilation, a cache file (model_*.bin) is saved. Subsequent loads skip the expensive symbolic compilation and deserialize the cached model instead. Force a rebuild with init!(sam; remake=true).

YAML vs Julia

AspectYAMLJulia constructors
Best forComplex models, data from CAD/measurementsSimple models, programmatic generation
ReadabilityEasy to scan geometry at a glanceBetter for computed geometry (loops, formulas)
Material refsBuilt-in: reference by nameManual: pass stiffness/damping directly
Version controlClean diffs for parameter changesCode diffs mix logic and parameters

Both paths produce the same SystemStructure type and are equally capable. They can be freely mixed — for example, load a YAML model and then modify component fields in Julia before simulation.