Compilation pipeline

SymbolicAWEModels works like a compiler: it takes a structural description and transforms it through several stages into an efficient numerical ODE solver. This page explains each stage.

Overview

    Stage 1            Stage 2              Stage 3               Stage 4
┌──────────────┐   ┌──────────────┐   ┌──────────────────┐   ┌──────────────┐
│ Component    │──▶│ System       │──▶│ Symbolic Eqs     │──▶│ ODEProblem   │
│ Definition   │   │ Structure    │   │ ModelingToolkit  │   │ + Integrator │
│              │   │              │   │                  │   │              │
│ Point()      │   │ resolve refs │   │ point_eqs!()     │   │ init!()      │
│ Segment()    │   │ validate     │   │ segment_eqs!()   │   │ cache to .bin│
│ Wing()       │   │ compute COM  │   │ wing_eqs!()      │   │              │
│ Winch()      │   │              │   │ winch_eqs!()     │   │              │
│ ...          │   │              │   │ aero_eqs!()      │   │              │
└──────────────┘   └──────────────┘   └──────────────────┘   └──────────────┘
                                            │
                                            ▼
                                        mtkcompile

Stage 1: component definition

Components are created using constructors (Point, Segment, etc.) with symbolic name references. At this stage, references are unresolved — a segment knows it connects :anchor to :mass, but doesn't know their numeric indices yet.

points = [
    Point(:anchor, [0, 0, 0], STATIC),
    Point(:mass, [0, 0, -50], DYNAMIC; extra_mass=1.0),
]
# :anchor and :mass are just names here, not yet resolved
segments = [Segment(:spring, :anchor, :mass,
    614600.0, 473.0, 0.004)]

Alternatively, components can be parsed from a YAML file using load_sys_struct_from_yaml, which calls the same constructors internally.

Stage 2: SystemStructure assembly

The SystemStructure constructor takes the component vectors and:

  1. Assigns indices — each component gets an idx based on its position in the vector (1, 2, 3, ...)
  2. Resolves references — symbolic names like :anchor are mapped to indices via assign_indices_and_resolve!()
  3. Computes derived properties:
    • Segment l0 from point positions (if zero)
    • Wing center of mass and inertia tensor — from the .obj mesh when one is supplied, otherwise from the mass-weighted point masses
    • Body and principal frames (PrincipalFrameMethod)
    • VSM panel geometry adjustments
  4. Validates — checks for NaN masses, zero stiffness, invalid pulley constraints, etc. via validate_sys_struct()
sys = SystemStructure("my_model", set; points, segments, transforms)
# All references now resolved: segments[1].point_idxs == (1, 2)

Stage 3: symbolic equation generation

create_sys!() generates the full set of differential-algebraic equations (DAEs) using ModelingToolkit.jl. It calls specialized equation builders for each subsystem:

FunctionSource filePurpose
point_eqs!()src/generate_system/point_eqs.jlNewton's law for each point mass
segment_eqs!()src/generate_system/segment_eqs.jlSpring-damper forces with drag
wing_eqs!()src/generate_system/wing_eqs.jlQuaternion dynamics, angular momentum
winch_eqs!()src/generate_system/winch_eqs.jlMotor dynamics, Coulomb/viscous friction
tether_eqs!()src/generate_system/tether_eqs.jlTether length kinematics
pulley_eqs!()src/generate_system/pulley_eqs.jlEqual-tension constraints
station_eqs!()src/generate_system/station_eqs.jlTwist deformation dynamics
rigid_body_eqs!()src/generate_system/rigid_body_eqs.jl6-DOF body dynamics in the principal frame
body_eqs!()src/generate_system/body_eqs.jlBody-frame pose outputs
joint_eqs!()src/generate_system/joint_eqs.jlElasticJoint 6-DOF springs
timoshenko_joint_eqs!()src/generate_system/timoshenko_joint_eqs.jlCorotational beam elements
scalar_eqs!()src/generate_system/scalar_eqs.jlWinch dynamics, kinematics
aero_eqs!()src/generate_system/aero_eqs.jlWires each wing's aero component (mode-agnostic)

After generating all equations, mtkcompile from ModelingToolkit reduces the DAE system by eliminating algebraic constraints and identifying the minimal set of independent variables.

Stage 4: compilation and caching

init! creates the ODEProblem from the simplified symbolic system and initializes the ODE integrator. This stage is expensive on first run because Julia JIT-compiles the generated code.

The compiled system is serialized to a binary cache file (model_<pkg_ver>_<julia_ver>_<name>_<dynamics_type>_<aero_tag>_...bin) in the data directory. On subsequent runs, the cached model is deserialized instead of recompiled, reducing startup from minutes to seconds. The package version is part of the filename, so upgrading invalidates stale caches automatically.

Force a rebuild by deleting the cache file or passing init!(sam; remake=true). A model carrying a custom aero or winch component rebuilds by default — its equations are not captured by the model hash.

Stage 5: time-stepping

Once compiled, the simulation loop consists of:

  1. next_step!(sam) — advances the ODE integrator by one time step
  2. update_sys_struct!() — copies the integrator state back to the mutable component structs (point positions, wing orientation, etc.)
  3. refresh_aero!() — periodically calls the Vortex Step Method to update aerodynamic forces (controlled by vsm_interval)
using KiteUtils: next_step!

for i in 1:1000
    next_step!(sam; set_values=[torque])
end

sim! wraps this loop with a matrix of control inputs.

Runtime parameter changes

Numeric fields of the component structs are emitted as flat MTK parameters and synced from the live SystemStructure once per step, so many parameters can be changed at runtime without recompiling:

  • Winch parameters: inertia_total, coulomb_friction, viscous_coefficient, gear_ratio
  • Segment properties: l0 (via tether/winch control)
  • Point damping: body_frame_damping, world_frame_damping
  • Aero state: aero_jac, aero_x, aero_y, aero_force_b (updated by refresh_aero!())

Mutate the struct field between steps and it is picked up at the next sync — no init! or remake call is needed. Flat parameters compile to direct buffer loads, which is why the generated RHS is allocation-free.

Changes that do require recompilation (rebuilding the symbolic system):

  • Adding or removing components (points, segments, wings, bodies, joints)
  • Changing the system topology (which points connect to which)
  • Changing dynamics types (STATIC ↔ DYNAMIC)
  • Changing an aero mode, or any structural field it reports via aero_hash_id

These require creating a new SystemStructure and SymbolicAWEModel.