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 materials, 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, converts string enum values, resolves material references, 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
materialsMaterial property lookup table
pointsPoint masses (nodes in the system)
segmentsSpring-damper connections
pulleysEqual-tension constraints
groupsDeformable wing sections
tethersWinch-controlled segment groups
winchesTorque-controlled motors
wingsAerodynamic bodies
transformsSpherical coordinate positioning

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.

Materials and references

Materials allow you to define physical properties once and reference them across segments. When a segment's unit_stiffness column contains a string instead of a number, it is treated as a material reference.

materials:
  headers: [name, youngs_modulus, density, damping_per_stiffness]
  data:
    - [dyneema, 55e9, 724, 0.00077]

segments:
  headers: [idx, point_i, point_j, l0, diameter_mm,
            unit_stiffness, unit_damping, compression_frac]
  data:
    # 'dyneema' in unit_stiffness triggers material lookup
    - [1, 1, 2, 5.0, 5.0, dyneema, nothing, 0.01]
    # Explicit stiffness (no material lookup)
    - [2, 2, 3, 5.0, 1.0, 100000, 50.0, 0.01]

When a material is referenced, derived properties are calculated automatically:

  • unit_stiffness = youngs_modulus * pi * (diameter_mm/2000)^2
  • unit_damping = damping_per_stiffness * unit_stiffness

Component reference

Points

points:
  headers: [idx, pos_cad, type, wing_idx, transform_idx,
            extra_mass, body_frame_damping, world_frame_damping,
            area, drag_coeff]
FieldTypeDefaultDescription
idxIntrequiredPoint identifier
pos_cad[x,y,z]requiredPosition in CAD frame [m]
typeStringrequiredSTATIC, DYNAMIC, QUASI_STATIC, or WING
wing_idxInt/nothing1Wing this point belongs to
transform_idxInt/nothingnothingTransform for initial positioning
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

Segments

Two formats are supported:

With explicit stiffness:

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]

With material reference:

segments:
  headers: [idx, point_i, point_j, l0, diameter_mm,
            unit_stiffness, unit_damping, compression_frac]
  data:
    - [1, 1, 2, 5.0, 5.0, dyneema, nothing, 0.01]
FieldTypeDescription
idxIntSegment identifier
point_i, point_jIntEndpoint point indices
l0FloatUnstretched length [m] (0 = calculate from points)
diameter_mmFloatDiameter [mm]
unit_stiffnessFloat/StringPer-unit-length stiffness [N], or material name
unit_dampingFloat/nothingPer-unit-length damping [Ns], or nothing for auto
compression_fracFloatCompressive/tensile stiffness ratio (0-1)

Pulleys

pulleys:
  headers: [idx, segment_i, segment_j, type]
  data:
    - [1, 3, 4, DYNAMIC]

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]

Winches

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

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(),
    "quat_struc_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 remake_cache=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.