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!() │ │ │
└──────────────┘ └──────────────┘ └──────────────────┘ └──────────────┘
│
▼
mtkcompileStage 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:
- Assigns indices — each component gets an
idxbased on its position in the vector (1, 2, 3, ...) - Resolves references — symbolic names like
:anchorare mapped to indices viaassign_indices_and_resolve!() - Computes derived properties:
- Segment
l0from point positions (if zero) - Wing center of mass and inertia tensor — from the
.objmesh when one is supplied, otherwise from the mass-weighted point masses - Body and principal frames (
PrincipalFrameMethod) - VSM panel geometry adjustments
- Segment
- 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:
| Function | Source file | Purpose |
|---|---|---|
point_eqs!() | src/generate_system/point_eqs.jl | Newton's law for each point mass |
segment_eqs!() | src/generate_system/segment_eqs.jl | Spring-damper forces with drag |
wing_eqs!() | src/generate_system/wing_eqs.jl | Quaternion dynamics, angular momentum |
winch_eqs!() | src/generate_system/winch_eqs.jl | Motor dynamics, Coulomb/viscous friction |
tether_eqs!() | src/generate_system/tether_eqs.jl | Tether length kinematics |
pulley_eqs!() | src/generate_system/pulley_eqs.jl | Equal-tension constraints |
station_eqs!() | src/generate_system/station_eqs.jl | Twist deformation dynamics |
rigid_body_eqs!() | src/generate_system/rigid_body_eqs.jl | 6-DOF body dynamics in the principal frame |
body_eqs!() | src/generate_system/body_eqs.jl | Body-frame pose outputs |
joint_eqs!() | src/generate_system/joint_eqs.jl | ElasticJoint 6-DOF springs |
timoshenko_joint_eqs!() | src/generate_system/timoshenko_joint_eqs.jl | Corotational beam elements |
scalar_eqs!() | src/generate_system/scalar_eqs.jl | Winch dynamics, kinematics |
aero_eqs!() | src/generate_system/aero_eqs.jl | Wires 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:
next_step!(sam)— advances the ODE integrator by one time stepupdate_sys_struct!()— copies the integrator state back to the mutable component structs (point positions, wing orientation, etc.)refresh_aero!()— periodically calls the Vortex Step Method to update aerodynamic forces (controlled byvsm_interval)
using KiteUtils: next_step!
for i in 1:1000
next_step!(sam; set_values=[torque])
endsim! 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 byrefresh_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.